diff --git a/includes/parsedown.php b/includes/parsedown.php index d2d6c98..620930d 100644 --- a/includes/parsedown.php +++ b/includes/parsedown.php @@ -3,6 +3,7 @@ if( ! defined( 'ABSPATH' ) ) exit; use Symfony\Component\Yaml\Yaml; +use Symfony\Component\Yaml\Exception\ParseException; class GIW_Parsedown extends ParsedownExtra{ @@ -40,8 +41,18 @@ public function parse_content( $text ){ ); } - $front_matter = Yaml::parse( trim( $parts [1] ) ); - $front_matter = wp_parse_args( $front_matter, $this->default_front_matter ); + try { + $front_matter = Yaml::parse( trim( $parts[1] ) ); + if( !is_array( $front_matter ) ){ + GIW_Utils::log( 'YAML parsed to non-array, skipping file' ); + $front_matter = array_merge( $this->default_front_matter, array( 'skip_file' => 'yes' ) ); + } else { + $front_matter = wp_parse_args( $front_matter, $this->default_front_matter ); + } + } catch( ParseException $e ){ + GIW_Utils::log( 'YAML parse error: ' . $e->getMessage() . ' | Raw: ' . substr( trim( $parts[1] ), 0, 200 ) ); + $front_matter = array_merge( $this->default_front_matter, array( 'skip_file' => 'yes' ) ); + } $markdown = implode( PHP_EOL . '---' . PHP_EOL, array_slice( $parts, 2 ) ); diff --git a/includes/publisher.php b/includes/publisher.php index 15103db..e381216 100644 --- a/includes/publisher.php +++ b/includes/publisher.php @@ -100,7 +100,7 @@ public function get_post_meta( $post_id ){ public function create_post( $post_id, $item_slug, $item_props, $parent ){ - GIW_Utils::log( sprintf( '---------- Checking post [%s] under parent [%s] ----------', $post_id, $parent ) ); + GIW_Utils::log( sprintf( '---------- Checking post [%s] slug [%s] under parent [%s] ----------', $post_id, $item_slug, $parent ) ); // If post exists, check if it has changed and proceed further if( $post_id && $item_props ){ @@ -124,7 +124,7 @@ public function create_post( $post_id, $item_slug, $item_props, $parent ){ // Some error in getting the item content if( !$item_content ){ - GIW_Utils::log( 'Cannot retrieve post content, skipping this' ); + GIW_Utils::log( 'Cannot retrieve post content for [' . $item_slug . '] URL [' . $item_props[ 'raw_url' ] . '], skipping this' ); $this->stats[ 'posts' ][ 'failed' ]++; return false; } @@ -213,7 +213,8 @@ public function create_post( $post_id, $item_slug, $item_props, $parent ){ $new_post_id = wp_insert_post( $post_details ); if( is_wp_error( $new_post_id ) || empty( $new_post_id ) ){ - GIW_Utils::log( 'Failed to publish post - ' . $new_post_id ); + $error_msg = is_wp_error( $new_post_id ) ? $new_post_id->get_error_message() : 'empty post ID'; + GIW_Utils::log( 'Failed to publish post [' . $item_slug . '] - ' . $error_msg ); $this->stats[ 'posts' ][ 'failed' ]++; return false; }else{ @@ -230,7 +231,7 @@ public function create_post( $post_id, $item_slug, $item_props, $parent ){ $set_tax = wp_set_object_terms( $new_post_id, $terms, $tax_name ); if( is_wp_error( $set_tax ) ){ - GIW_Utils::log( 'Failed to set taxonomy [' . $set_tax->get_error_message() . ']' ); + GIW_Utils::log( 'Failed to set taxonomy [' . $tax_name . '] on post [' . $item_slug . '] - ' . $set_tax->get_error_message() ); } } } @@ -259,6 +260,8 @@ public function create_posts( $repo_structure, $parent ){ foreach( $repo_structure as $item_slug => $item_props ){ + try { + GIW_Utils::log( 'At repository item - ' . $item_slug); $first_character = substr( $item_slug, 0, 1 ); @@ -298,7 +301,7 @@ public function create_posts( $repo_structure, $parent ){ $this->create_post( $directory_post, $item_slug, $index_props, $parent ); }else{ - + // If index posts exists for the directory if( array_key_exists( 'index', $item_props[ 'items' ] ) ){ $index_props = $item_props[ 'items' ][ 'index' ]; @@ -313,6 +316,12 @@ public function create_posts( $repo_structure, $parent ){ } + } catch( \Exception $e ){ + GIW_Utils::log( 'Error processing item [' . $item_slug . ']: ' . $e->getMessage() ); + $this->stats[ 'posts' ][ 'failed' ]++; + continue; + } + } } @@ -350,6 +359,12 @@ public function upload_images_recursive( $images, &$uploaded_images ){ continue; } + $allowed_image_extensions = array( 'jpg', 'jpeg', 'jpe', 'png', 'gif', 'webp' ); + if( !in_array( strtolower( $image_props[ 'file_type' ] ), $allowed_image_extensions ) ){ + GIW_Utils::log( 'Skipping non-image file [' . $image_slug . '] with extension [' . $image_props[ 'file_type' ] . ']' ); + continue; + } + $image_path = $image_props[ 'rel_url' ]; GIW_Utils::log( 'Starting image ' . $image_path ); @@ -364,7 +379,8 @@ public function upload_images_recursive( $images, &$uploaded_images ){ $uploaded_image_id = $this->upload_image( $image_props, 0, null, 'id' ); if( is_wp_error( $uploaded_image_id ) ){ - GIW_Utils::log( 'Failed to upload image. Error [' . $uploaded_image_id->get_error_message() . ']' ); + GIW_Utils::log( 'Failed to upload image [' . $image_path . ']. Error [' . $uploaded_image_id->get_error_message() . ']' ); + $this->stats[ 'images' ][ 'failed' ]++; continue; } @@ -503,6 +519,14 @@ public function publish(){ $this->create_posts( $repo_structure, 0 ); GIW_Utils::log( '++++++++++ Done ++++++++++' ); + GIW_Utils::log( sprintf( 'SYNC SUMMARY: Posts: %d new, %d updated, %d failed | Images: %d uploaded, %d failed', + count( $this->stats[ 'posts' ][ 'new' ] ), + count( $this->stats[ 'posts' ][ 'updated' ] ), + $this->stats[ 'posts' ][ 'failed' ], + count( $this->stats[ 'images' ][ 'uploaded' ] ), + $this->stats[ 'images' ][ 'failed' ] + )); + $message = 'Successfully published posts'; $result = 1; diff --git a/includes/repository.php b/includes/repository.php index 6e23b64..d041f7e 100644 --- a/includes/repository.php +++ b/includes/repository.php @@ -34,6 +34,7 @@ public function get( $url ){ 'headers' => array( 'Authorization' => 'Basic ' . base64_encode($username . ':' . $access_token), ), + 'timeout' => 30, ); $response = wp_remote_get( $url, $args ); diff --git a/readme.txt b/readme.txt index b55a3fe..297bbf7 100644 --- a/readme.txt +++ b/readme.txt @@ -8,7 +8,7 @@ License: GPLv2 or later Requires PHP: 5.3 Requires at least: 4.4 Tested up to: 6.6.1 -Stable tag: 2.0 +Stable tag: 2.1 Publish markdown files present in a GitHub repository as posts to WordPress automatically @@ -142,6 +142,16 @@ Yes, if you want to pull posts from a folder in a repository then you can specif ## Changelog +### 2.1 +* Fix: Malformed YAML no longer crashes entire sync — files are skipped with detailed error logging. +* Fix: Non-image files (e.g. .gitkeep, .DS_Store) in _images/ no longer block image uploads. +* Fix: Missing failed image counter before continue in upload loop. +* New: Error recovery — try/catch in post creation loop prevents one bad file from blocking the rest. +* New: Improved log messages with file slugs, paths, and specific WP_Error details. +* New: Sync summary stats at end of log (posts new/updated/failed, images uploaded/failed). +* New: 30s timeout on GitHub API requests to prevent indefinite hangs. +* New: Standalone test suite (62 tests, no WordPress required). + ### 2.0 * Fix: Disable inline URLs from being converted to link tags. (Thanks to @SienciLabs for the report) diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..41e9d7a --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,99 @@ +code = $code; + $this->message = $message; + } + + public function get_error_message(){ + return $this->message; + } + + public function get_error_code(){ + return $this->code; + } +} + +function is_wp_error( $thing ){ + return $thing instanceof WP_Error; +} + +// ── GIW_Utils stub that captures log messages ── + +class GIW_Utils { + public static $logs = array(); + + public static function log( $message ){ + if( is_array( $message ) || is_object( $message ) ){ + self::$logs[] = print_r( $message, true ); + } else { + self::$logs[] = (string) $message; + } + } + + public static function clear_logs(){ + self::$logs = array(); + } + + public static function get_logs(){ + return self::$logs; + } + + public static function remove_extension_relative_url( $url ){ + return preg_replace( '/\.md$/', '', $url ); + } + + public static function process_content_template( $template, $html ){ + return $html; + } + + public static function process_date( $date ){ + return $date; + } + + public static function get_uploaded_images(){ + return array(); + } +} + +// ── Load composer autoloader (real Symfony YAML + Parsedown) ── + +$autoloader = GIW_PATH . 'vendor/autoload.php'; +if( !file_exists( $autoloader ) ){ + echo "ERROR: vendor/autoload.php not found. Run 'composer install' first.\n"; + exit(1); +} +require_once $autoloader; + +// ── Load plugin file under test ── + +require_once GIW_PATH . 'includes/parsedown.php'; diff --git a/tests/fixtures/empty-frontmatter.md b/tests/fixtures/empty-frontmatter.md new file mode 100644 index 0000000..67d9519 --- /dev/null +++ b/tests/fixtures/empty-frontmatter.md @@ -0,0 +1,6 @@ +--- +--- + +## Empty Front Matter + +The YAML block is empty. diff --git a/tests/fixtures/horizontal-rule-in-body.md b/tests/fixtures/horizontal-rule-in-body.md new file mode 100644 index 0000000..3ff9f2b --- /dev/null +++ b/tests/fixtures/horizontal-rule-in-body.md @@ -0,0 +1,14 @@ +--- +title: Horizontal Rule Test +post_status: publish +--- + +## Section One + +Some content here. + +--- + +## Section Two + +More content after the horizontal rule. diff --git a/tests/fixtures/malformed-bad-indent.md b/tests/fixtures/malformed-bad-indent.md new file mode 100644 index 0000000..7f2121b --- /dev/null +++ b/tests/fixtures/malformed-bad-indent.md @@ -0,0 +1,10 @@ +--- +title: Bad Indent +taxonomy: + category: + - broken-indent + - also-broken +post_status: publish +--- + +## Content diff --git a/tests/fixtures/malformed-colon-in-value.md b/tests/fixtures/malformed-colon-in-value.md new file mode 100644 index 0000000..5955b18 --- /dev/null +++ b/tests/fixtures/malformed-colon-in-value.md @@ -0,0 +1,6 @@ +--- +title: How to: Fix Things: A Guide +post_status: publish +--- + +## Content diff --git a/tests/fixtures/malformed-nested-quotes.md b/tests/fixtures/malformed-nested-quotes.md new file mode 100644 index 0000000..c1f0058 --- /dev/null +++ b/tests/fixtures/malformed-nested-quotes.md @@ -0,0 +1,6 @@ +--- +title: Post with "nested quotes" that 'break' YAML: badly +post_status: publish +--- + +## Content diff --git a/tests/fixtures/malformed-scalar-only.md b/tests/fixtures/malformed-scalar-only.md new file mode 100644 index 0000000..ad5082d --- /dev/null +++ b/tests/fixtures/malformed-scalar-only.md @@ -0,0 +1,5 @@ +--- +just a plain string not a mapping +--- + +## Content diff --git a/tests/fixtures/malformed-tabs.md b/tests/fixtures/malformed-tabs.md new file mode 100644 index 0000000..395ee64 --- /dev/null +++ b/tests/fixtures/malformed-tabs.md @@ -0,0 +1,8 @@ +--- +title: Tabs Post +taxonomy: + category: + - broken-tabs +--- + +## Content diff --git a/tests/fixtures/malformed-unclosed-string.md b/tests/fixtures/malformed-unclosed-string.md new file mode 100644 index 0000000..451cf06 --- /dev/null +++ b/tests/fixtures/malformed-unclosed-string.md @@ -0,0 +1,6 @@ +--- +title: "Unclosed string +post_status: publish +--- + +## Content diff --git a/tests/fixtures/no-frontmatter.md b/tests/fixtures/no-frontmatter.md new file mode 100644 index 0000000..c7c000d --- /dev/null +++ b/tests/fixtures/no-frontmatter.md @@ -0,0 +1,3 @@ +## No Front Matter + +This file has no YAML front matter at all. diff --git a/tests/fixtures/skip-file.md b/tests/fixtures/skip-file.md new file mode 100644 index 0000000..6b84b74 --- /dev/null +++ b/tests/fixtures/skip-file.md @@ -0,0 +1,7 @@ +--- +title: Skipped Post +post_status: publish +skip_file: "yes" +--- + +## This should be skipped diff --git a/tests/fixtures/valid-basic.md b/tests/fixtures/valid-basic.md new file mode 100644 index 0000000..ab05796 --- /dev/null +++ b/tests/fixtures/valid-basic.md @@ -0,0 +1,8 @@ +--- +title: Basic Post +post_status: publish +--- + +## Hello World + +This is a basic post. diff --git a/tests/fixtures/valid-full.md b/tests/fixtures/valid-full.md new file mode 100644 index 0000000..8b63824 --- /dev/null +++ b/tests/fixtures/valid-full.md @@ -0,0 +1,24 @@ +--- +title: Full Post +menu_order: 5 +post_status: draft +post_excerpt: A full test post +post_date: 2025-01-15 10:00:00 +comment_status: open +stick_post: "no" +featured_image: _images/hero.jpg +taxonomy: + category: + - tech + - news + post_tag: + - test +custom_fields: + key1: value1 + key2: value2 +skip_file: "no" +--- + +## Full Post Content + +Paragraph with **bold** and *italic*. diff --git a/tests/run-tests.php b/tests/run-tests.php new file mode 100644 index 0000000..3482bdd --- /dev/null +++ b/tests/run-tests.php @@ -0,0 +1,305 @@ +parse_content( fixture( 'valid-basic.md' ) ); +assert_equals( 'Basic Post', $result['front_matter']['title'], 'Valid basic: title parsed' ); +assert_equals( 'publish', $result['front_matter']['post_status'], 'Valid basic: post_status parsed' ); +assert_true( strpos( $result['markdown'], '## Hello World' ) !== false, 'Valid basic: markdown body present' ); + +// Test 2: Valid full front matter +GIW_Utils::clear_logs(); +$result = $parsedown->parse_content( fixture( 'valid-full.md' ) ); +assert_equals( 'Full Post', $result['front_matter']['title'], 'Valid full: title parsed' ); +assert_equals( 'draft', $result['front_matter']['post_status'], 'Valid full: post_status parsed' ); +assert_equals( 5, $result['front_matter']['menu_order'], 'Valid full: menu_order parsed' ); +assert_equals( 'A full test post', $result['front_matter']['post_excerpt'], 'Valid full: post_excerpt parsed' ); +assert_true( is_array( $result['front_matter']['taxonomy'] ), 'Valid full: taxonomy is array' ); +assert_true( in_array( 'tech', $result['front_matter']['taxonomy']['category'] ), 'Valid full: taxonomy category contains tech' ); + +// Test 3: No front matter +GIW_Utils::clear_logs(); +$result = $parsedown->parse_content( fixture( 'no-frontmatter.md' ) ); +assert_equals( '', $result['front_matter']['title'], 'No front matter: defaults used (empty title)' ); +assert_true( strpos( $result['markdown'], '## No Front Matter' ) !== false, 'No front matter: full content as markdown' ); + +// Test 4: Empty front matter — YAML parses empty string to null (not array) +GIW_Utils::clear_logs(); +$result = $parsedown->parse_content( fixture( 'empty-frontmatter.md' ) ); +assert_equals( 'yes', $result['front_matter']['skip_file'], 'Empty front matter: skip_file set (non-array guard)' ); + +// Test 5: Malformed nested quotes +GIW_Utils::clear_logs(); +$result = $parsedown->parse_content( fixture( 'malformed-nested-quotes.md' ) ); +// This may parse OK or throw — either way should not crash +assert_true( is_array( $result['front_matter'] ), 'Nested quotes: returns array (no crash)' ); + +// Test 6: Malformed bad indent +GIW_Utils::clear_logs(); +$result = $parsedown->parse_content( fixture( 'malformed-bad-indent.md' ) ); +assert_equals( 'yes', $result['front_matter']['skip_file'], 'Bad indent: skip_file set' ); +$logs = GIW_Utils::get_logs(); +$has_error_log = false; +foreach( $logs as $log ){ + if( strpos( $log, 'YAML parse error' ) !== false || strpos( $log, 'non-array' ) !== false ){ + $has_error_log = true; + } +} +assert_true( $has_error_log, 'Bad indent: error logged' ); + +// Test 7: Malformed tabs +GIW_Utils::clear_logs(); +$result = $parsedown->parse_content( fixture( 'malformed-tabs.md' ) ); +assert_equals( 'yes', $result['front_matter']['skip_file'], 'Tabs in YAML: skip_file set' ); + +// Test 8: Scalar-only YAML +GIW_Utils::clear_logs(); +$result = $parsedown->parse_content( fixture( 'malformed-scalar-only.md' ) ); +assert_equals( 'yes', $result['front_matter']['skip_file'], 'Scalar YAML: skip_file set (non-array guard)' ); +$logs = GIW_Utils::get_logs(); +$has_non_array_log = false; +foreach( $logs as $log ){ + if( strpos( $log, 'non-array' ) !== false ){ + $has_non_array_log = true; + } +} +assert_true( $has_non_array_log, 'Scalar YAML: non-array log message' ); + +// Test 9: Unclosed string +GIW_Utils::clear_logs(); +$result = $parsedown->parse_content( fixture( 'malformed-unclosed-string.md' ) ); +assert_true( is_array( $result['front_matter'] ), 'Unclosed string: returns array (no crash)' ); + +// Test 10: Colon in value (unquoted) +GIW_Utils::clear_logs(); +$result = $parsedown->parse_content( fixture( 'malformed-colon-in-value.md' ) ); +assert_true( is_array( $result['front_matter'] ), 'Colon in value: returns array (no crash)' ); + +// Test 11: Horizontal rule in body (not confused with front matter) +GIW_Utils::clear_logs(); +$result = $parsedown->parse_content( fixture( 'horizontal-rule-in-body.md' ) ); +assert_equals( 'Horizontal Rule Test', $result['front_matter']['title'], 'HR in body: title parsed correctly' ); +assert_true( strpos( $result['markdown'], '## Section Two' ) !== false, 'HR in body: content after HR preserved' ); + +// Test 12: Error logging verification — bad indent should log raw YAML +GIW_Utils::clear_logs(); +$parsedown->parse_content( fixture( 'malformed-bad-indent.md' ) ); +$logs = GIW_Utils::get_logs(); +$has_raw_yaml = false; +foreach( $logs as $log ){ + if( strpos( $log, 'Raw:' ) !== false || strpos( $log, 'non-array' ) !== false ){ + $has_raw_yaml = true; + } +} +assert_true( $has_raw_yaml, 'Error logging: error details logged for malformed YAML' ); + +// Test 13: skip_file in fixture +GIW_Utils::clear_logs(); +$result = $parsedown->parse_content( fixture( 'skip-file.md' ) ); +assert_equals( 'yes', $result['front_matter']['skip_file'], 'skip_file: correctly parsed from front matter' ); + +// ══════════════════════════════════════════════ +// Suite 2: Image Extension Filtering +// ══════════════════════════════════════════════ + +echo "\n=== Suite 2: Image Extension Filtering ===\n"; + +// The filter logic from publisher.php +function is_allowed_image_extension( $file_type ){ + $allowed = array( 'jpg', 'jpeg', 'jpe', 'png', 'gif', 'webp' ); + return in_array( strtolower( $file_type ), $allowed ); +} + +// Valid extensions +assert_true( is_allowed_image_extension( 'jpg' ), 'Image ext: jpg allowed' ); +assert_true( is_allowed_image_extension( 'jpeg' ), 'Image ext: jpeg allowed' ); +assert_true( is_allowed_image_extension( 'jpe' ), 'Image ext: jpe allowed' ); +assert_true( is_allowed_image_extension( 'png' ), 'Image ext: png allowed' ); +assert_true( is_allowed_image_extension( 'gif' ), 'Image ext: gif allowed' ); +assert_true( is_allowed_image_extension( 'webp' ), 'Image ext: webp allowed' ); +assert_true( is_allowed_image_extension( 'JPG' ), 'Image ext: JPG (uppercase) allowed' ); +assert_true( is_allowed_image_extension( 'Png' ), 'Image ext: Png (mixed case) allowed' ); + +// Invalid extensions +assert_false( is_allowed_image_extension( 'gitkeep' ), 'Image ext: gitkeep rejected' ); +assert_false( is_allowed_image_extension( '' ), 'Image ext: empty string rejected' ); +assert_false( is_allowed_image_extension( 'DS_Store' ), 'Image ext: DS_Store rejected' ); +assert_false( is_allowed_image_extension( 'md' ), 'Image ext: md rejected' ); +assert_false( is_allowed_image_extension( 'txt' ), 'Image ext: txt rejected' ); +assert_false( is_allowed_image_extension( 'svg' ), 'Image ext: svg rejected' ); +assert_false( is_allowed_image_extension( 'bmp' ), 'Image ext: bmp rejected' ); +assert_false( is_allowed_image_extension( 'tiff' ), 'Image ext: tiff rejected' ); +assert_false( is_allowed_image_extension( 'ico' ), 'Image ext: ico rejected' ); +assert_false( is_allowed_image_extension( 'pdf' ), 'Image ext: pdf rejected' ); +assert_false( is_allowed_image_extension( 'php' ), 'Image ext: php rejected' ); +assert_false( is_allowed_image_extension( 'html' ), 'Image ext: html rejected' ); + +// Edge cases with file_type as repository.php provides it +// repository.php line 80-88: file_type is always set, empty string if no extension +assert_false( is_allowed_image_extension( '' ), 'Image ext: no extension (empty) rejected' ); + +// ══════════════════════════════════════════════ +// Suite 3: Item Slug Filtering +// ══════════════════════════════════════════════ + +echo "\n=== Suite 3: Item Slug Filtering ===\n"; + +function should_skip_item( $slug ){ + $first_character = substr( $slug, 0, 1 ); + return in_array( $first_character, array( '_', '.' ) ); +} + +assert_true( should_skip_item( '_images' ), 'Slug filter: _images skipped' ); +assert_true( should_skip_item( '_templates' ), 'Slug filter: _templates skipped' ); +assert_true( should_skip_item( '.gitignore' ), 'Slug filter: .gitignore skipped' ); +assert_true( should_skip_item( '.hidden' ), 'Slug filter: .hidden skipped' ); +assert_false( should_skip_item( 'my-post' ), 'Slug filter: my-post passes' ); +assert_false( should_skip_item( 'index' ), 'Slug filter: index passes' ); +assert_false( should_skip_item( 'About-Us' ), 'Slug filter: About-Us passes' ); + +// ══════════════════════════════════════════════ +// Suite 4: Error Recovery Simulation +// ══════════════════════════════════════════════ + +echo "\n=== Suite 4: Error Recovery Simulation ===\n"; + +// Simulate processing a sequence: good file, bad file (throws), good file +// All should produce results without crash propagation +$sequence = array( + 'good-1' => fixture( 'valid-basic.md' ), + 'bad' => fixture( 'malformed-bad-indent.md' ), + 'good-2' => fixture( 'valid-full.md' ), +); + +$results = array(); +$errors = 0; + +foreach( $sequence as $slug => $content ){ + try { + GIW_Utils::clear_logs(); + $result = $parsedown->parse_content( $content ); + $results[ $slug ] = $result; + } catch( \Exception $e ){ + $errors++; + $results[ $slug ] = 'error'; + } +} + +assert_equals( 0, $errors, 'Error recovery: no uncaught exceptions' ); +assert_equals( 3, count( $results ), 'Error recovery: all 3 items processed' ); +assert_equals( 'Basic Post', $results['good-1']['front_matter']['title'], 'Error recovery: first good file OK' ); +assert_equals( 'yes', $results['bad']['front_matter']['skip_file'], 'Error recovery: bad file got skip_file' ); +assert_equals( 'Full Post', $results['good-2']['front_matter']['title'], 'Error recovery: second good file OK after bad' ); + +// ══════════════════════════════════════════════ +// Suite 5: Error Message Quality +// ══════════════════════════════════════════════ + +echo "\n=== Suite 5: Error Message Quality ===\n"; + +GIW_Utils::clear_logs(); +$parsedown->parse_content( fixture( 'malformed-bad-indent.md' ) ); +$logs = GIW_Utils::get_logs(); +$log_text = implode( "\n", $logs ); +assert_true( + strpos( $log_text, 'YAML parse error' ) !== false || strpos( $log_text, 'non-array' ) !== false, + 'Error quality: descriptive error type in log' +); + +GIW_Utils::clear_logs(); +$parsedown->parse_content( fixture( 'malformed-scalar-only.md' ) ); +$logs = GIW_Utils::get_logs(); +$log_text = implode( "\n", $logs ); +assert_true( + strpos( $log_text, 'non-array' ) !== false, + 'Error quality: scalar YAML triggers non-array message' +); + +// ══════════════════════════════════════════════ +// Suite 6: Markdown Rendering Smoke +// ══════════════════════════════════════════════ + +echo "\n=== Suite 6: Markdown Rendering Smoke ===\n"; + +$md_result = $parsedown->parse_content( fixture( 'valid-full.md' ) ); +$html = $parsedown->text( $md_result['markdown'] ); + +assert_true( strpos( $html, '

' ) !== false, 'Markdown: H2 rendered' ); +assert_true( strpos( $html, 'bold' ) !== false, 'Markdown: bold rendered' ); +assert_true( strpos( $html, 'italic' ) !== false, 'Markdown: italic rendered' ); + +// ══════════════════════════════════════════════ +// Results +// ══════════════════════════════════════════════ + +echo "\n" . str_repeat( '=', 50 ) . "\n"; +echo "Results: $passed / $total passed\n"; + +if( count( $failed_tests ) > 0 ){ + echo "\nFailed tests:\n"; + foreach( $failed_tests as $name ){ + echo " - $name\n"; + } + echo "\n"; + exit(1); +} + +echo "All tests passed!\n"; +exit(0);