diff --git a/examples/properties-example.md b/examples/properties-example.md new file mode 100644 index 0000000..0d14020 --- /dev/null +++ b/examples/properties-example.md @@ -0,0 +1,23 @@ +# Test DB item 1 + +| Property | Value | +| --- | --- | +| CheckboxField | ✓ | +| Type | type-1 | +| Email | [test@gogmail.com](mailto:test@gogmail.com) | +| Date | 2025-09-02 | +| Status | In progress | +| Priority Level | 3, 2 | +| Files & media | [ChatGPT Image Sep 3, 2025, 09_59_04 PM.png](https://prod-files-secure.s3.us-west-2.amazonaws.com/0d31a281-d71e-4bb9-9031-a5256095b873/641f2fcb-cf13-4c62-8d9d-2871f34a1a6a/ChatGPT_Image_Sep_3_2025_09_59_04_PM.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466TSVKJSYL%2F20251113%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251113T142329Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEIb%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLXdlc3QtMiJHMEUCIFRgCBhDrbx1BLqDYCSDBN0nKqfqOJQCDDgcaAlS7FlFAiEA61lIOYodqWb175B59GZ0I5UZAEVGESLENFu7u63l7yQq%2FwMITxAAGgw2Mzc0MjMxODM4MDUiDOjdkbEDAHH2feRgTCrcA52bxAy4SCWSISHqMfTFKV2hIXhxNoozmqaLUe2h2J4ZyCkPy7PoUDxaQ50Lz22Ef4qtAfpfuhfjDxG7U31MZuOVyVL8FHY7HLgkfG9xa%2Fmde0rvJRbVB0jVDJ6IPUYXnIVFt2i99jm20Y69%2Frxqdpojjf8qYXZ%2BFhLKyvEQMVtkf8JoGs5d%2BdzA8%2FKW1tfyTArdYlr9hwPge8%2BtBiFVGbtCtUzmYsQ6DgE8AQSvdaymhlyWZLGMXLM8teiVvygXfj1UmBgphObTgP5bHpzMAUkcGKr2Tgy6oeGmRPru2iajdVZanlXbliixd%2B89y0cYy%2F2TW7EI7qQTbb71hyoG72k4pb9Fe%2B4wNXcX42cC%2FuURsVdmfhjxLP%2FwmGuuJuKZAaIvPQBBvBQZR8uX4kcXHA5EP8B4H5Q16%2BpImFcREddY4dV4ge4zdQ7bRkfvtKDqB5GZOao3fYB59gow222KG1AEuf4q0m68QmeSye9tleUxG21mLpYRwd97Hb%2BZUQd%2F8YETrqMcQgJPla%2FIuR71zpRaMTt%2FCdsbhH0qudsDOMJO1cEvDnfF1dhA3ktdyMfDWcNplz5Y0XG8FvkJ1TvNAODiTC%2F9ToHstPj76i%2BUdP9wQFlVoEEoNwLnWeoSMPHC18gGOqUB1DXrEJu41npTQQ2drVOjvBH0Ce2PT4mEx7SJpUJYL1o53dsHF3vvXPvlivx1pp6azmVDYwsAlFncBNTwBBm6rkytMfi4vC9GgB5ovSwCGNuWgQpYm8ZQuy5SYZ4Rgq1pI6Sqz75B1fnUgOa7bHAJrTfZ%2FdaHISPNwbsWULzDE44jpADZk9mxdmROHT1S6Urgn38UlZlo9iAqH2ffG4YjK%2B3HMKWR&X-Amz-Signature=6f39e1d0620132a3ed8b664b8b99adf82929e79cf1dd8f561a2a1153803f36be&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject) | +| Person | Maestro Error | +| Name | Test DB item 1 | + + + + +> 💪 Test callout + + + + + diff --git a/examples/properties-fetch-example.php b/examples/properties-fetch-example.php new file mode 100644 index 0000000..ab9ae9d --- /dev/null +++ b/examples/properties-fetch-example.php @@ -0,0 +1,125 @@ +singleton('files', fn () => new Filesystem); + +// Register blade compiler +$container->singleton('blade.compiler', function ($app) { + return new BladeCompiler( + $app['files'], + __DIR__.'/../storage/views' + ); +}); + +// Set up view finder +$viewFinder = new FileViewFinder( + $container['files'], + [__DIR__.'/../resources/views'] +); + +// Add namespace for our views +$viewFinder->addNamespace('md-notion', __DIR__.'/../resources/views'); + +// Set up view factory +$resolver = new EngineResolver; +$resolver->register('blade', function () use ($container) { + return new CompilerEngine($container['blade.compiler']); +}); + +$factory = new Factory( + $resolver, + $viewFinder, + new Dispatcher($container) +); + +// Bind view factory to container +$container->instance('view', $factory); +View::setFacadeApplication($container); + +// Initialize the real Notion SDK with token +$token = include __DIR__.'/../notion-token.php'; +$notion = new Notion($token, '2025-09-03'); + +// Create services +$mdNotionConfig = include __DIR__.'/../config/md-notion.php'; +$blockRegistry = new BlockRegistry(new BlockAdapterFactory($notion, $mdNotionConfig['adapters'] ?? [])); +$databaseTable = new DatabaseTable; +$pageReader = new PageReader($notion, $blockRegistry); +$databaseReader = new DatabaseReader($notion, $databaseTable); +$propertiesTable = new PropertiesTable; + +// Bind services to container so Page objects can use them +$container->instance(PropertiesTable::class, $propertiesTable); + +// Use a page ID that has properties (replace with your actual page ID) +// This should be a page from your Notion workspace that has various properties +$pageId = '263d9316605a80c0a8fbfd152e79b9d8'; // Replace with a page that has properties + +echo "Fetching page content with properties...\n"; + +try { + // Read page + $page = $pageReader->read($pageId); + + echo "Page fetched successfully!\n"; + echo 'Page title: '.$page->getTitle()."\n"; + echo 'Has properties: '.($page->hasProperties() ? 'Yes ('.count($page->getProperties()).')' : 'No')."\n"; + echo 'Content length: '.strlen($page->getContent() ?? '')." characters\n\n"; + + // Build markdown content + $markdown = ''; + + // Add page title + $markdown .= $page->renderTitle(1)."\n\n"; + + // Add properties table if available + if ($page->hasProperties()) { + $markdown .= $page->renderPropertiesTable()."\n"; + } + + // Add page content + if ($page->hasContent()) { + $markdown .= $page->getContent()."\n\n"; + } + + // Save to file + file_put_contents(__DIR__.'/properties-example.md', $markdown); + + echo "Page with properties table converted and saved to examples/properties-example.md\n"; + echo "\nGenerated markdown:\n"; + echo "---\n"; + echo $markdown; + echo "---\n"; + +} catch (\Exception $e) { + echo 'Error: '.$e->getMessage()."\n"; + echo "Stack trace:\n"; + echo $e->getTraceAsString()."\n"; +} diff --git a/resources/views/full-md.blade.php b/resources/views/full-md.blade.php index 03a80af..c2193b6 100644 --- a/resources/views/full-md.blade.php +++ b/resources/views/full-md.blade.php @@ -1,6 +1,10 @@ {!! $current_page['title'] !!} +@if($current_page['hasPropertiesTable']) + +{!! $current_page['properties_table'] !!} +@endif @if($current_page['hasContent']) {!! $current_page['content'] !!} diff --git a/resources/views/page-md.blade.php b/resources/views/page-md.blade.php index 5e8a1ac..b68be8e 100644 --- a/resources/views/page-md.blade.php +++ b/resources/views/page-md.blade.php @@ -1,5 +1,9 @@ {!! $current_page['title'] !!} +@if($current_page['hasPropertiesTable']) + +{!! $current_page['properties_table'] !!} +@endif @if($current_page['hasContent']) {!! $current_page['content'] !!} @@ -25,6 +29,10 @@ @foreach($child_pages as $childPage) {!! $childPage['title'] !!} +@if($childPage['hasPropertiesTable']) + +{!! $childPage['properties_table'] !!} +@endif @if($childPage['hasContent']) {!! $childPage['content'] !!} diff --git a/src/ContentBuilder.php b/src/ContentBuilder.php index 29a00cb..1988717 100644 --- a/src/ContentBuilder.php +++ b/src/ContentBuilder.php @@ -81,6 +81,8 @@ public function read(): string // Build structured data for template $currentPage = [ 'title' => $page->renderTitle(1), + 'properties_table' => $page->hasProperties() ? $page->renderPropertiesTable() : null, + 'hasPropertiesTable' => $page->hasProperties(), 'content' => $page->hasContent() ? $page->getContent() : null, 'hasContent' => $page->hasContent(), ]; @@ -101,6 +103,8 @@ public function read(): string foreach ($page->getChildPages() as $childPage) { $childPages[] = [ 'title' => $childPage->renderTitle(3), + 'properties_table' => $childPage->hasProperties() ? $childPage->renderPropertiesTable() : null, + 'hasPropertiesTable' => $childPage->hasProperties(), 'content' => $childPage->hasContent() ? $childPage->getContent() : null, 'hasContent' => $childPage->hasContent(), ]; diff --git a/src/MdNotion.php b/src/MdNotion.php index be36534..c6c52e8 100644 --- a/src/MdNotion.php +++ b/src/MdNotion.php @@ -115,6 +115,8 @@ private function buildFullMarkdownData($page, $level = 1): array { $currentPage = [ 'title' => $page->renderTitle($level), + 'properties_table' => $page->hasProperties() ? $page->renderPropertiesTable() : null, + 'hasPropertiesTable' => $page->hasProperties(), 'content' => $page->hasContent() ? $page->getContent() : null, 'hasContent' => $page->hasContent(), ]; diff --git a/src/Objects/Page.php b/src/Objects/Page.php index c1fbe57..8d4aad4 100644 --- a/src/Objects/Page.php +++ b/src/Objects/Page.php @@ -170,6 +170,22 @@ public function fetch(): static return $this; } + /** + * Render properties as markdown table + * + * @return string The properties table markdown + */ + public function renderPropertiesTable(): string + { + if (! $this->hasProperties()) { + return ''; + } + + $propertiesTable = app(\Redberry\MdNotion\Services\PropertiesTable::class); + + return $propertiesTable->convertPropertiesToMarkdownTable($this->properties); + } + /** * Convert the page to an array */ diff --git a/src/Services/PropertiesTable.php b/src/Services/PropertiesTable.php new file mode 100644 index 0000000..30f00a3 --- /dev/null +++ b/src/Services/PropertiesTable.php @@ -0,0 +1,313 @@ + $property) { + $value = $this->extractPropertyValue($property); + + // Skip properties with no value + if ($value === '') { + continue; + } + + // Escape special characters in property name and value + $escapedName = $this->escapeTableCellCharacters($name); + $escapedValue = $this->escapeTableCellCharacters($value); + + $markdown .= "| {$escapedName} | {$escapedValue} |\n"; + } + + return $markdown."\n"; + } + + /** + * Extract and format property value for table display + * + * @param array $property Property data + * @return string Formatted value + */ + private function extractPropertyValue(array $property): string + { + $type = $property['type'] ?? 'unknown'; + + switch ($type) { + case 'title': + return $this->extractRichText($property['title'] ?? []); + + case 'rich_text': + return $this->extractRichText($property['rich_text'] ?? []); + + case 'url': + $url = $property['url'] ?? ''; + + return $url ? "[{$url}]({$url})" : ''; + + case 'email': + $email = $property['email'] ?? ''; + + return $email ? "[{$email}](mailto:{$email})" : ''; + + case 'phone_number': + return $property['phone_number'] ?? ''; + + case 'date': + return $this->formatDate($property['date'] ?? null); + + case 'number': + return $property['number'] !== null ? (string) $property['number'] : ''; + + case 'checkbox': + return ($property['checkbox'] ?? false) ? '✓' : '✗'; + + case 'select': + return $property['select']['name'] ?? ''; + + case 'multi_select': + $options = $property['multi_select'] ?? []; + + return implode(', ', array_column($options, 'name')); + + case 'status': + return $property['status']['name'] ?? ''; + + case 'people': + return $this->formatPeople($property['people'] ?? []); + + case 'files': + return $this->formatFiles($property['files'] ?? []); + + case 'relation': + return $this->formatRelation($property['relation'] ?? []); + + case 'rollup': + return $this->formatRollup($property['rollup'] ?? null); + + case 'formula': + return $this->formatFormula($property['formula'] ?? null); + + case 'created_time': + return $property['created_time'] ?? ''; + + case 'created_by': + return $this->formatUser($property['created_by'] ?? null); + + case 'last_edited_time': + return $property['last_edited_time'] ?? ''; + + case 'last_edited_by': + return $this->formatUser($property['last_edited_by'] ?? null); + + default: + return ''; + } + } + + /** + * Extract plain text from rich text array + * + * @param array $richText Rich text array + * @return string Plain text + */ + private function extractRichText(array $richText): string + { + $text = ''; + foreach ($richText as $item) { + $text .= $item['plain_text'] ?? ''; + } + + return $text; + } + + /** + * Format date property + * + * @param array|null $date Date data + * @return string Formatted date + */ + private function formatDate(?array $date): string + { + if (! $date) { + return ''; + } + + $start = $date['start'] ?? ''; + $end = $date['end'] ?? null; + + if ($end) { + return "{$start} → {$end}"; + } + + return $start; + } + + /** + * Format people property + * + * @param array $people People array + * @return string Formatted people + */ + private function formatPeople(array $people): string + { + if (empty($people)) { + return ''; + } + + $names = []; + foreach ($people as $person) { + $names[] = $person['name'] ?? 'Unknown'; + } + + return implode(', ', $names); + } + + /** + * Format files property + * + * @param array $files Files array + * @return string Formatted files + */ + private function formatFiles(array $files): string + { + if (empty($files)) { + return ''; + } + + $links = []; + foreach ($files as $file) { + $name = $file['name'] ?? 'File'; + + // Handle both external and uploaded files + if (isset($file['external']['url'])) { + $url = $file['external']['url']; + $links[] = "[{$name}]({$url})"; + } elseif (isset($file['file']['url'])) { + $url = $file['file']['url']; + $links[] = "[{$name}]({$url})"; + } else { + $links[] = $name; + } + } + + return implode(', ', $links); + } + + /** + * Format relation property + * + * @param array $relation Relation array + * @return string Formatted relation + */ + private function formatRelation(array $relation): string + { + if (empty($relation)) { + return ''; + } + + return count($relation).' related item(s)'; + } + + /** + * Format rollup property + * + * @param array|null $rollup Rollup data + * @return string Formatted rollup + */ + private function formatRollup(?array $rollup): string + { + if (! $rollup) { + return ''; + } + + $type = $rollup['type'] ?? ''; + + switch ($type) { + case 'number': + return (string) ($rollup['number'] ?? ''); + case 'date': + return $this->formatDate($rollup['date'] ?? null); + case 'array': + return count($rollup['array'] ?? []).' item(s)'; + default: + return ''; + } + } + + /** + * Format formula property + * + * @param array|null $formula Formula data + * @return string Formatted formula + */ + private function formatFormula(?array $formula): string + { + if (! $formula) { + return ''; + } + + $type = $formula['type'] ?? ''; + + switch ($type) { + case 'string': + return $formula['string'] ?? ''; + case 'number': + return (string) ($formula['number'] ?? ''); + case 'boolean': + return ($formula['boolean'] ?? false) ? 'Yes' : 'No'; + case 'date': + return $this->formatDate($formula['date'] ?? null); + default: + return ''; + } + } + + /** + * Format user object + * + * @param array|null $user User data + * @return string Formatted user + */ + private function formatUser(?array $user): string + { + if (! $user) { + return ''; + } + + return $user['name'] ?? 'Unknown'; + } + + /** + * Escape special characters in markdown table cells + * + * @param string $text Text to escape + * @return string Escaped text + */ + private function escapeTableCellCharacters(string $text): string + { + // Escape pipe characters to prevent table structure corruption + $text = str_replace('|', '\|', $text); + + // Replace newline characters with spaces to maintain table structure + $text = str_replace(["\r\n", "\r", "\n"], ' ', $text); + + return $text; + } +} diff --git a/tests/BladeTemplateTest.php b/tests/BladeTemplateTest.php index d45fbfa..8d4967d 100644 --- a/tests/BladeTemplateTest.php +++ b/tests/BladeTemplateTest.php @@ -4,6 +4,8 @@ $page = [ 'id' => 'test-id', 'title' => '# Test Page', + 'properties_table' => '', + 'hasPropertiesTable' => false, 'content' => 'Test content', 'hasContent' => true, ]; @@ -14,6 +16,8 @@ 'withPages' => false, 'hasChildDatabases' => false, 'hasChildPages' => false, + 'child_databases' => [], + 'child_pages' => [], ])->render(); expect($rendered)->toContain('# Test Page'); @@ -24,6 +28,8 @@ $page = [ 'id' => 'test-id', 'title' => '# Test Page', + 'properties_table' => '', + 'hasPropertiesTable' => false, 'hasContent' => true, 'content' => 'Test content', ]; @@ -32,6 +38,8 @@ 'current_page' => $page, 'hasChildDatabases' => false, 'hasChildPages' => false, + 'child_databases' => [], + 'child_pages' => [], ])->render(); expect($rendered)->toContain('# Test Page'); diff --git a/tests/Objects/PropertiesTest.php b/tests/Objects/PropertiesTest.php index e69de29..f92172a 100644 --- a/tests/Objects/PropertiesTest.php +++ b/tests/Objects/PropertiesTest.php @@ -0,0 +1,127 @@ +app->singleton(PropertiesTable::class, function () { + return new PropertiesTable; + }); +}); + +test('page can check if it has properties', function () { + $page = new Page([ + 'id' => 'test-id', + 'properties' => [ + 'Name' => [ + 'type' => 'title', + 'title' => [['plain_text' => 'Test']], + ], + ], + ]); + + expect($page->hasProperties())->toBeTrue(); +}); + +test('page returns false when it has no properties', function () { + $page = new Page([ + 'id' => 'test-id', + 'properties' => [], + ]); + + expect($page->hasProperties())->toBeFalse(); +}); + +test('page can render properties table', function () { + $page = new Page([ + 'id' => 'test-id', + 'properties' => [ + 'Name' => [ + 'id' => 'title', + 'type' => 'title', + 'title' => [ + ['plain_text' => 'Test Page'], + ], + ], + 'Status' => [ + 'id' => 'status', + 'type' => 'status', + 'status' => [ + 'name' => 'In Progress', + ], + ], + ], + ]); + + $result = $page->renderPropertiesTable(); + + expect($result)->toContain('| Property | Value |') + ->and($result)->toContain('| Name | Test Page |') + ->and($result)->toContain('| Status | In Progress |'); +}); + +test('page returns empty string when rendering properties table without properties', function () { + $page = new Page([ + 'id' => 'test-id', + 'properties' => [], + ]); + + $result = $page->renderPropertiesTable(); + + expect($result)->toBe(''); +}); + +test('page can get and set properties', function () { + $page = new Page(['id' => 'test-id']); + + $properties = [ + 'Name' => [ + 'type' => 'title', + 'title' => [['plain_text' => 'Test']], + ], + ]; + + $page->setProperties($properties); + + expect($page->getProperties())->toBe($properties); +}); + +test('page can get individual property', function () { + $page = new Page([ + 'id' => 'test-id', + 'properties' => [ + 'Name' => [ + 'type' => 'title', + 'title' => [['plain_text' => 'Test']], + ], + ], + ]); + + expect($page->getProperty('Name'))->toBe([ + 'type' => 'title', + 'title' => [['plain_text' => 'Test']], + ]); +}); + +test('page returns null for non-existent property', function () { + $page = new Page([ + 'id' => 'test-id', + 'properties' => [], + ]); + + expect($page->getProperty('NonExistent'))->toBeNull(); +}); + +test('page can set individual property', function () { + $page = new Page(['id' => 'test-id']); + + $property = [ + 'type' => 'title', + 'title' => [['plain_text' => 'Test']], + ]; + + $page->setProperty('Name', $property); + + expect($page->getProperty('Name'))->toBe($property); +}); diff --git a/tests/Services/PropertiesTableTest.php b/tests/Services/PropertiesTableTest.php new file mode 100644 index 0000000..2cda6d0 --- /dev/null +++ b/tests/Services/PropertiesTableTest.php @@ -0,0 +1,811 @@ +propertiesTable = new PropertiesTable; +}); + +test('returns empty string for empty properties', function () { + $result = $this->propertiesTable->convertPropertiesToMarkdownTable([]); + + expect($result)->toBe(''); +}); + +test('renders title property correctly', function () { + $properties = [ + 'Name' => [ + 'id' => 'title', + 'type' => 'title', + 'title' => [ + ['plain_text' => 'Test Page'], + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Property | Value |') + ->and($result)->toContain('| Name | Test Page |'); +}); + +test('renders url property correctly', function () { + $properties = [ + 'URL' => [ + 'id' => 'BLtm', + 'type' => 'url', + 'url' => 'https://github.com/example', + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| URL | [https://github.com/example](https://github.com/example) |'); +}); + +test('renders email property correctly', function () { + $properties = [ + 'Email' => [ + 'id' => 'YLWr', + 'type' => 'email', + 'email' => 'test@example.com', + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Email | [test@example.com](mailto:test@example.com) |'); +}); + +test('renders checkbox property correctly', function () { + $properties = [ + 'CheckboxField' => [ + 'id' => 'LPTO', + 'type' => 'checkbox', + 'checkbox' => true, + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| CheckboxField | ✓ |'); +}); + +test('renders unchecked checkbox correctly', function () { + $properties = [ + 'CheckboxField' => [ + 'id' => 'LPTO', + 'type' => 'checkbox', + 'checkbox' => false, + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| CheckboxField | ✗ |'); +}); + +test('renders number property correctly', function () { + $properties = [ + 'NumberField' => [ + 'id' => 'YP%3Ay', + 'type' => 'number', + 'number' => 343451234, + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| NumberField | 343451234 |'); +}); + +test('renders date property with start date only', function () { + $properties = [ + 'Date' => [ + 'id' => '%5Dm%3Ez', + 'type' => 'date', + 'date' => [ + 'start' => '2025-09-10', + 'end' => null, + 'time_zone' => null, + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Date | 2025-09-10 |'); +}); + +test('renders date property with date range', function () { + $properties = [ + 'Date' => [ + 'id' => '%5Dm%3Ez', + 'type' => 'date', + 'date' => [ + 'start' => '2025-09-10', + 'end' => '2025-09-15', + 'time_zone' => null, + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Date | 2025-09-10 → 2025-09-15 |'); +}); + +test('renders select property correctly', function () { + $properties = [ + 'Type' => [ + 'id' => 'MbTK', + 'type' => 'select', + 'select' => [ + 'id' => 'LOQu', + 'name' => 'type-2', + 'color' => 'pink', + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Type | type-2 |'); +}); + +test('renders multi_select property correctly', function () { + $properties = [ + 'Priority Level' => [ + 'id' => 'fvdf', + 'type' => 'multi_select', + 'multi_select' => [ + [ + 'id' => 'wSec', + 'name' => '4', + 'color' => 'gray', + ], + [ + 'id' => 'xyz', + 'name' => '5', + 'color' => 'blue', + ], + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Priority Level | 4, 5 |'); +}); + +test('renders status property correctly', function () { + $properties = [ + 'Status' => [ + 'id' => '%60DPS', + 'type' => 'status', + 'status' => [ + 'id' => 'd21e0a9d-bc3b-4a52-836a-a5925a07bf8f', + 'name' => 'Not started', + 'color' => 'default', + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Status | Not started |'); +}); + +test('skips properties with empty values', function () { + $properties = [ + 'Email' => [ + 'id' => 'YLWr', + 'type' => 'email', + 'email' => null, + ], + 'Name' => [ + 'id' => 'title', + 'type' => 'title', + 'title' => [ + ['plain_text' => 'Test Page'], + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->not->toContain('| Email |') + ->and($result)->toContain('| Name | Test Page |'); +}); + +test('renders people property correctly', function () { + $properties = [ + 'Person' => [ + 'id' => '%7BaiB', + 'type' => 'people', + 'people' => [ + ['name' => 'John Doe'], + ['name' => 'Jane Smith'], + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Person | John Doe, Jane Smith |'); +}); + +test('renders files property with external files', function () { + $properties = [ + 'Files & media' => [ + 'id' => 'iLcJ', + 'type' => 'files', + 'files' => [ + [ + 'name' => 'document.pdf', + 'type' => 'external', + 'external' => [ + 'url' => 'https://example.com/document.pdf', + ], + ], + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Files & media | [document.pdf](https://example.com/document.pdf) |'); +}); + +test('renders files property with uploaded files', function () { + $properties = [ + 'Files & media' => [ + 'id' => 'iLcJ', + 'type' => 'files', + 'files' => [ + [ + 'name' => 'image.png', + 'type' => 'file', + 'file' => [ + 'url' => 'https://s3.amazonaws.com/notion/image.png', + ], + ], + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Files & media | [image.png](https://s3.amazonaws.com/notion/image.png) |'); +}); + +test('renders phone_number property correctly', function () { + $properties = [ + 'Phone' => [ + 'id' => 'phone', + 'type' => 'phone_number', + 'phone_number' => '+1234567890', + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Phone | +1234567890 |'); +}); + +test('renders created_time property correctly', function () { + $properties = [ + 'Created' => [ + 'id' => 'created', + 'type' => 'created_time', + 'created_time' => '2025-09-10T10:30:00.000Z', + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Created | 2025-09-10T10:30:00.000Z |'); +}); + +test('renders created_by property correctly', function () { + $properties = [ + 'Created By' => [ + 'id' => 'created_by', + 'type' => 'created_by', + 'created_by' => [ + 'id' => 'user-id', + 'name' => 'John Doe', + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Created By | John Doe |'); +}); + +test('renders last_edited_time property correctly', function () { + $properties = [ + 'Last Edited' => [ + 'id' => 'last_edited', + 'type' => 'last_edited_time', + 'last_edited_time' => '2025-09-11T15:45:00.000Z', + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Last Edited | 2025-09-11T15:45:00.000Z |'); +}); + +test('renders last_edited_by property correctly', function () { + $properties = [ + 'Last Edited By' => [ + 'id' => 'last_edited_by', + 'type' => 'last_edited_by', + 'last_edited_by' => [ + 'id' => 'user-id', + 'name' => 'Jane Smith', + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Last Edited By | Jane Smith |'); +}); + +test('renders relation property correctly', function () { + $properties = [ + 'Related Pages' => [ + 'id' => 'relation', + 'type' => 'relation', + 'relation' => [ + ['id' => 'page-1'], + ['id' => 'page-2'], + ['id' => 'page-3'], + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Related Pages | 3 related item(s) |'); +}); + +test('renders rollup property with number type', function () { + $properties = [ + 'Total' => [ + 'id' => 'rollup', + 'type' => 'rollup', + 'rollup' => [ + 'type' => 'number', + 'number' => 42, + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Total | 42 |'); +}); + +test('renders rollup property with array type', function () { + $properties = [ + 'Items' => [ + 'id' => 'rollup', + 'type' => 'rollup', + 'rollup' => [ + 'type' => 'array', + 'array' => [1, 2, 3, 4, 5], + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Items | 5 item(s) |'); +}); + +test('renders formula property with string type', function () { + $properties = [ + 'Calculated' => [ + 'id' => 'formula', + 'type' => 'formula', + 'formula' => [ + 'type' => 'string', + 'string' => 'Result Text', + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Calculated | Result Text |'); +}); + +test('renders formula property with boolean type', function () { + $properties = [ + 'Is Valid' => [ + 'id' => 'formula', + 'type' => 'formula', + 'formula' => [ + 'type' => 'boolean', + 'boolean' => true, + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Is Valid | Yes |'); +}); + +test('renders multiple properties in correct table format', function () { + $properties = [ + 'Name' => [ + 'id' => 'title', + 'type' => 'title', + 'title' => [ + ['plain_text' => 'Test Page'], + ], + ], + 'URL' => [ + 'id' => 'url', + 'type' => 'url', + 'url' => 'https://example.com', + ], + 'Status' => [ + 'id' => 'status', + 'type' => 'status', + 'status' => [ + 'name' => 'In Progress', + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Property | Value |') + ->and($result)->toContain('| --- | --- |') + ->and($result)->toContain('| Name | Test Page |') + ->and($result)->toContain('| URL | [https://example.com](https://example.com) |') + ->and($result)->toContain('| Status | In Progress |'); +}); + +test('renders rich_text property correctly', function () { + $properties = [ + 'Description' => [ + 'id' => 'rich', + 'type' => 'rich_text', + 'rich_text' => [ + ['plain_text' => 'This is '], + ['plain_text' => 'a description'], + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Description | This is a description |'); +}); + +test('escapes pipe characters in property names', function () { + $properties = [ + 'Name | Title' => [ + 'id' => 'title', + 'type' => 'title', + 'title' => [ + ['plain_text' => 'Test Page'], + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Name \| Title | Test Page |'); +}); + +test('escapes pipe characters in property values', function () { + $properties = [ + 'Description' => [ + 'id' => 'rich', + 'type' => 'rich_text', + 'rich_text' => [ + ['plain_text' => 'Value with | pipe'], + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Description | Value with \| pipe |'); +}); + +test('escapes multiple pipe characters in property names and values', function () { + $properties = [ + 'A | B | C' => [ + 'id' => 'title', + 'type' => 'title', + 'title' => [ + ['plain_text' => 'X | Y | Z'], + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| A \| B \| C | X \| Y \| Z |'); +}); + +test('escapes pipe characters in select property values', function () { + $properties = [ + 'Type' => [ + 'id' => 'select', + 'type' => 'select', + 'select' => [ + 'id' => 'LOQu', + 'name' => 'option-1 | option-2', + 'color' => 'pink', + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Type | option-1 \| option-2 |'); +}); + +test('escapes pipe characters in multi_select property values', function () { + $properties = [ + 'Tags' => [ + 'id' => 'multi_select', + 'type' => 'multi_select', + 'multi_select' => [ + [ + 'id' => 'tag1', + 'name' => 'tag | 1', + 'color' => 'blue', + ], + [ + 'id' => 'tag2', + 'name' => 'tag | 2', + 'color' => 'green', + ], + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Tags | tag \| 1, tag \| 2 |'); +}); + +test('escapes pipe characters in people names', function () { + $properties = [ + 'Person' => [ + 'id' => 'people', + 'type' => 'people', + 'people' => [ + ['name' => 'John | Doe'], + ['name' => 'Jane | Smith'], + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Person | John \| Doe, Jane \| Smith |'); +}); + +test('escapes pipe characters in file names', function () { + $properties = [ + 'Files' => [ + 'id' => 'files', + 'type' => 'files', + 'files' => [ + [ + 'name' => 'file | name.pdf', + 'type' => 'external', + 'external' => [ + 'url' => 'https://example.com/file.pdf', + ], + ], + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Files | [file \| name.pdf](https://example.com/file.pdf) |'); +}); + +test('escapes pipe characters in user names', function () { + $properties = [ + 'Created By' => [ + 'id' => 'created_by', + 'type' => 'created_by', + 'created_by' => [ + 'id' => 'user-id', + 'name' => 'John | Doe', + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Created By | John \| Doe |'); +}); + +test('escapes pipe characters in formula string values', function () { + $properties = [ + 'Calculated' => [ + 'id' => 'formula', + 'type' => 'formula', + 'formula' => [ + 'type' => 'string', + 'string' => 'Result | Text', + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Calculated | Result \| Text |'); +}); + +test('replaces newline characters with spaces in property values', function () { + $properties = [ + 'Description' => [ + 'id' => 'rich', + 'type' => 'rich_text', + 'rich_text' => [ + ['plain_text' => "Line 1\nLine 2"], + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Description | Line 1 Line 2 |'); +}); + +test('replaces carriage return with spaces in property values', function () { + $properties = [ + 'Description' => [ + 'id' => 'rich', + 'type' => 'rich_text', + 'rich_text' => [ + ['plain_text' => "Line 1\rLine 2"], + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Description | Line 1 Line 2 |'); +}); + +test('replaces CRLF with spaces in property values', function () { + $properties = [ + 'Description' => [ + 'id' => 'rich', + 'type' => 'rich_text', + 'rich_text' => [ + ['plain_text' => "Line 1\r\nLine 2"], + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Description | Line 1 Line 2 |'); +}); + +test('replaces multiple newlines with spaces in property values', function () { + $properties = [ + 'Description' => [ + 'id' => 'rich', + 'type' => 'rich_text', + 'rich_text' => [ + ['plain_text' => "Line 1\n\nLine 2\n\nLine 3"], + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Description | Line 1 Line 2 Line 3 |'); +}); + +test('replaces newlines in property names', function () { + $properties = [ + "Multi\nLine\nName" => [ + 'id' => 'title', + 'type' => 'title', + 'title' => [ + ['plain_text' => 'Test Value'], + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Multi Line Name | Test Value |'); +}); + +test('handles both newlines and pipe characters together', function () { + $properties = [ + "Name | Title\nWith Newline" => [ + 'id' => 'rich', + 'type' => 'rich_text', + 'rich_text' => [ + ['plain_text' => "Value | with\npipe and\nnewlines"], + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Name \| Title With Newline | Value \| with pipe and newlines |'); +}); + +test('replaces newlines in select property values', function () { + $properties = [ + 'Type' => [ + 'id' => 'select', + 'type' => 'select', + 'select' => [ + 'id' => 'LOQu', + 'name' => "Option\nWith\nNewlines", + 'color' => 'pink', + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Type | Option With Newlines |'); +}); + +test('replaces newlines in multi_select property values', function () { + $properties = [ + 'Tags' => [ + 'id' => 'multi_select', + 'type' => 'multi_select', + 'multi_select' => [ + [ + 'id' => 'tag1', + 'name' => "Tag\n1", + 'color' => 'blue', + ], + [ + 'id' => 'tag2', + 'name' => "Tag\n2", + 'color' => 'green', + ], + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Tags | Tag 1, Tag 2 |'); +}); + +test('replaces newlines in people names', function () { + $properties = [ + 'Person' => [ + 'id' => 'people', + 'type' => 'people', + 'people' => [ + ['name' => "John\nDoe"], + ['name' => "Jane\nSmith"], + ], + ], + ]; + + $result = $this->propertiesTable->convertPropertiesToMarkdownTable($properties); + + expect($result)->toContain('| Person | John Doe, Jane Smith |'); +});