diff --git a/features/makepot.feature b/features/makepot.feature index c292be88..6221be59 100644 --- a/features/makepot.feature +++ b/features/makepot.feature @@ -92,7 +92,7 @@ Feature: Generate a POT file of a WordPress project """ And the foo-plugin.pot file should contain: """ - #: foo-plugin.php + #: foo-plugin.php:5 """ And the foo-plugin.pot file should contain: """ @@ -3890,12 +3890,12 @@ Feature: Generate a POT file of a WordPress project And the foo-theme/foo-theme.pot file should exist And the foo-theme/foo-theme.pot file should contain: """ - #: patterns/my-pattern.php + #: patterns/my-pattern.php:3 msgctxt "Pattern title" msgid "My pattern title." msgstr "" - #: patterns/my-pattern.php + #: patterns/my-pattern.php:4 msgctxt "Pattern description" msgid "My pattern description." msgstr "" diff --git a/src/FileDataExtractor.php b/src/FileDataExtractor.php index 0d97c864..f724a6bf 100644 --- a/src/FileDataExtractor.php +++ b/src/FileDataExtractor.php @@ -18,7 +18,7 @@ class FileDataExtractor { * @param string $file Path to the file. * @param array $headers List of headers, in the format array('HeaderKey' => 'Header Name'). * - * @return array Array of file headers in `HeaderKey => Header Value` format. + * @return array Array of file headers in `HeaderKey => ['value' => Header Value, 'line' => Line Number]` format. */ public static function get_file_data( $file, $headers ) { // We don't need to write to the file, so just open for reading. @@ -42,14 +42,24 @@ public static function get_file_data( $file, $headers ) { * @param string $text String to look for metadata in. * @param array $headers List of headers. * - * @return array Array of file headers in `HeaderKey => Header Value` format. + * @return array Array of file headers in `HeaderKey => ['value' => Header Value, 'line' => Line Number]` format. */ public static function get_file_data_from_string( $text, $headers ) { foreach ( $headers as $field => $regex ) { - if ( preg_match( '/^[ \t\/*#@]*' . preg_quote( $regex, '/' ) . ':(.*)$/mi', $text, $match ) && $match[1] ) { - $headers[ $field ] = static::_cleanup_header_comment( $match[1] ); + if ( preg_match( '/^[ \t\/*#@]*' . preg_quote( $regex, '/' ) . ':(.*)$/mi', $text, $match, PREG_OFFSET_CAPTURE ) && $match[1][0] ) { + $value = static::_cleanup_header_comment( $match[1][0] ); + + // Calculate line number from the offset + $line_num = substr_count( $text, "\n", 0, $match[0][1] ) + 1; + $headers[ $field ] = [ + 'value' => $value, + 'line' => $line_num, + ]; } else { - $headers[ $field ] = ''; + $headers[ $field ] = [ + 'value' => '', + 'line' => 0, + ]; } } diff --git a/src/IterableCodeExtractor.php b/src/IterableCodeExtractor.php index 51b3e53b..112264ed 100644 --- a/src/IterableCodeExtractor.php +++ b/src/IterableCodeExtractor.php @@ -69,10 +69,15 @@ public static function fromFile( $file_or_files, Translations $translations, arr if ( ! empty( $options['wpExtractTemplates'] ) ) { $headers = FileDataExtractor::get_file_data_from_string( $text, [ 'Template Name' => 'Template Name' ] ); - if ( ! empty( $headers['Template Name'] ) ) { - $translation = new Translation( '', $headers['Template Name'] ); + if ( ! empty( $headers['Template Name']['value'] ) ) { + $translation = new Translation( '', $headers['Template Name']['value'] ); $translation->addExtractedComment( 'Template Name of the theme' ); + // Add file reference with line number if available + if ( ! empty( $headers['Template Name']['line'] ) ) { + $translation->addReference( $options['file'], $headers['Template Name']['line'] ); + } + $translations[] = $translation; } } @@ -87,16 +92,28 @@ public static function fromFile( $file_or_files, Translations $translations, arr ] ); - if ( ! empty( $headers['Title'] ) ) { - $translation = new Translation( 'Pattern title', $headers['Title'] ); - $translation->addReference( $options['file'] ); + if ( ! empty( $headers['Title']['value'] ) ) { + $translation = new Translation( 'Pattern title', $headers['Title']['value'] ); + + // Add file reference with line number if available + if ( ! empty( $headers['Title']['line'] ) ) { + $translation->addReference( $options['file'], $headers['Title']['line'] ); + } else { + $translation->addReference( $options['file'] ); + } $translations[] = $translation; } - if ( ! empty( $headers['Description'] ) ) { - $translation = new Translation( 'Pattern description', $headers['Description'] ); - $translation->addReference( $options['file'] ); + if ( ! empty( $headers['Description']['value'] ) ) { + $translation = new Translation( 'Pattern description', $headers['Description']['value'] ); + + // Add file reference with line number if available + if ( ! empty( $headers['Description']['line'] ) ) { + $translation->addReference( $options['file'], $headers['Description']['line'] ); + } else { + $translation->addReference( $options['file'] ); + } $translations[] = $translation; } diff --git a/src/MakePotCommand.php b/src/MakePotCommand.php index 8688096e..3d14245d 100644 --- a/src/MakePotCommand.php +++ b/src/MakePotCommand.php @@ -350,8 +350,8 @@ public function handle_arguments( $args, $assoc_args ) { if ( ! $ignore_domain ) { $this->domain = $this->slug; - if ( ! empty( $this->main_file_data['Text Domain'] ) ) { - $this->domain = $this->main_file_data['Text Domain']; + if ( ! empty( $this->main_file_data['Text Domain']['value'] ) ) { + $this->domain = $this->main_file_data['Text Domain']['value']; } $this->domain = Utils\get_flag_value( $assoc_args, 'domain', $this->domain ); @@ -362,12 +362,12 @@ public function handle_arguments( $args, $assoc_args ) { // Determine destination. $this->destination = "{$this->source}/{$this->slug}.pot"; - if ( ! empty( $this->main_file_data['Domain Path'] ) ) { + if ( ! empty( $this->main_file_data['Domain Path']['value'] ) ) { // Domain Path inside source folder. $this->destination = sprintf( '%s/%s/%s.pot', $this->source, - $this->unslashit( $this->main_file_data['Domain Path'] ), + $this->unslashit( $this->main_file_data['Domain Path']['value'] ), $this->slug ); } @@ -480,7 +480,7 @@ protected function get_main_file_data() { ); // Stop when it contains a valid Theme Name header. - if ( ! empty( $theme_data['Theme Name'] ) ) { + if ( ! empty( $theme_data['Theme Name']['value'] ) ) { WP_CLI::log( 'Theme stylesheet detected.' ); WP_CLI::debug( sprintf( 'Theme stylesheet: %s', $file->getRealPath() ), 'make-pot' ); @@ -502,7 +502,7 @@ protected function get_main_file_data() { ); // Stop when it contains a valid Theme Name header. - if ( ! empty( $theme_data['Theme Name'] ) ) { + if ( ! empty( $theme_data['Theme Name']['value'] ) ) { WP_CLI::log( 'Theme stylesheet detected.' ); WP_CLI::debug( sprintf( 'Theme stylesheet: %s', $file->getRealPath() . '/style.css' ), 'make-pot' ); @@ -524,7 +524,7 @@ protected function get_main_file_data() { ); // Stop when we find a file with a valid Plugin Name header. - if ( ! empty( $plugin_data['Plugin Name'] ) ) { + if ( ! empty( $plugin_data['Plugin Name']['value'] ) ) { WP_CLI::log( 'Plugin file detected.' ); WP_CLI::debug( sprintf( 'Plugin file: %s', $file->getRealPath() ), 'make-pot' ); @@ -616,11 +616,11 @@ protected function extract_strings() { // Set entries from main file data. foreach ( $this->main_file_data as $header => $data ) { - if ( empty( $data ) ) { + if ( empty( $data['value'] ) ) { continue; } - $translation = new Translation( '', $data ); + $translation = new Translation( '', $data['value'] ); if ( $is_theme ) { $translation->addExtractedComment( sprintf( '%s of the theme', $header ) ); @@ -629,9 +629,13 @@ protected function extract_strings() { } if ( $this->main_file_path && $this->location ) { - $translation->addReference( - ltrim( str_replace( Utils\normalize_path( "$this->source/" ), '', Utils\normalize_path( $this->main_file_path ) ), '/' ) - ); + $file_reference = ltrim( str_replace( Utils\normalize_path( "$this->source/" ), '', Utils\normalize_path( $this->main_file_path ) ), '/' ); + // Add line number if available + if ( ! empty( $data['line'] ) ) { + $translation->addReference( $file_reference, $data['line'] ); + } else { + $translation->addReference( $file_reference ); + } } $translations[] = $translation; @@ -920,38 +924,38 @@ protected function get_file_comment() { } if ( isset( $this->main_file_data['Theme Name'] ) ) { - if ( ! empty( $this->main_file_data['License'] ) ) { + if ( ! empty( $this->main_file_data['License']['value'] ) ) { return sprintf( "Copyright (C) %1\$s %2\$s\nThis file is distributed under the %3\$s.", date( 'Y' ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date - $this->main_file_data['Author'], - $this->main_file_data['License'] + $this->main_file_data['Author']['value'], + $this->main_file_data['License']['value'] ); } return sprintf( "Copyright (C) %1\$s %2\$s\nThis file is distributed under the same license as the %3\$s theme.", date( 'Y' ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date - $this->main_file_data['Author'], - $this->main_file_data['Theme Name'] + $this->main_file_data['Author']['value'], + $this->main_file_data['Theme Name']['value'] ); } if ( isset( $this->main_file_data['Plugin Name'] ) ) { - if ( ! empty( $this->main_file_data['License'] ) ) { + if ( ! empty( $this->main_file_data['License']['value'] ) ) { return sprintf( "Copyright (C) %1\$s %2\$s\nThis file is distributed under the %3\$s.", date( 'Y' ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date - $this->main_file_data['Author'], - $this->main_file_data['License'] + $this->main_file_data['Author']['value'], + $this->main_file_data['License']['value'] ); } return sprintf( "Copyright (C) %1\$s %2\$s\nThis file is distributed under the same license as the %3\$s plugin.", date( 'Y' ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date - $this->main_file_data['Author'], - $this->main_file_data['Plugin Name'] + $this->main_file_data['Author']['value'], + $this->main_file_data['Plugin Name']['value'] ); } @@ -969,14 +973,14 @@ protected function set_default_headers( $translations ) { $bugs_address = null; if ( ! $version && isset( $this->main_file_data['Version'] ) ) { - $version = $this->main_file_data['Version']; + $version = $this->main_file_data['Version']['value']; } if ( isset( $this->main_file_data['Theme Name'] ) ) { - $name = $this->main_file_data['Theme Name']; + $name = $this->main_file_data['Theme Name']['value']; $bugs_address = sprintf( 'https://wordpress.org/support/theme/%s', $this->slug ); } elseif ( isset( $this->main_file_data['Plugin Name'] ) ) { - $name = $this->main_file_data['Plugin Name']; + $name = $this->main_file_data['Plugin Name']['value']; $bugs_address = sprintf( 'https://wordpress.org/support/plugin/%s', $this->slug ); } diff --git a/tests/FileDataExtractorTest.php b/tests/FileDataExtractorTest.php new file mode 100644 index 00000000..f2663eb4 --- /dev/null +++ b/tests/FileDataExtractorTest.php @@ -0,0 +1,126 @@ + 'Plugin Name', + 'Description' => 'Description', + 'Version' => 'Version', + ] + ); + + $this->assertIsArray( $headers['Plugin Name'] ); + $this->assertEquals( 'My Plugin', $headers['Plugin Name']['value'] ); + $this->assertEquals( 3, $headers['Plugin Name']['line'] ); + + $this->assertIsArray( $headers['Description'] ); + $this->assertEquals( 'A test plugin', $headers['Description']['value'] ); + $this->assertEquals( 4, $headers['Description']['line'] ); + + $this->assertIsArray( $headers['Version'] ); + $this->assertEquals( '1.0.0', $headers['Version']['value'] ); + $this->assertEquals( 5, $headers['Version']['line'] ); + } + + public function test_line_numbers_with_different_line_endings() { + // Test with different line positions + $text = " 'Plugin Name', + 'Description' => 'Description', + ] + ); + + $this->assertEquals( 'Test Plugin', $headers['Plugin Name']['value'] ); + $this->assertEquals( 5, $headers['Plugin Name']['line'] ); + + $this->assertEquals( 'Description here', $headers['Description']['value'] ); + $this->assertEquals( 6, $headers['Description']['line'] ); + } + + public function test_empty_headers_with_line_numbers() { + $text = ' 'Plugin Name', + ] + ); + + $this->assertIsArray( $headers['Plugin Name'] ); + $this->assertEquals( '', $headers['Plugin Name']['value'] ); + $this->assertEquals( 0, $headers['Plugin Name']['line'] ); + } + + public function test_theme_headers_with_line_numbers() { + $text = <<<'TEXT' +/* +Theme Name: My Theme +Description: A beautiful theme +Author: John Doe +Version: 2.0.0 +*/ +TEXT; + + $headers = FileDataExtractor::get_file_data_from_string( + $text, + [ + 'Theme Name' => 'Theme Name', + 'Description' => 'Description', + 'Author' => 'Author', + 'Version' => 'Version', + ] + ); + + $this->assertEquals( 'My Theme', $headers['Theme Name']['value'] ); + $this->assertEquals( 2, $headers['Theme Name']['line'] ); + + $this->assertEquals( 'A beautiful theme', $headers['Description']['value'] ); + $this->assertEquals( 3, $headers['Description']['line'] ); + + $this->assertEquals( 'John Doe', $headers['Author']['value'] ); + $this->assertEquals( 4, $headers['Author']['line'] ); + + $this->assertEquals( '2.0.0', $headers['Version']['value'] ); + $this->assertEquals( 5, $headers['Version']['line'] ); + } + + public function test_header_with_trailing_comment() { + $text = <<<'TEXT' + 'Plugin Name', + ] + ); + + $this->assertEquals( 'My Plugin', $headers['Plugin Name']['value'] ); + $this->assertEquals( 3, $headers['Plugin Name']['line'] ); + } +}