xarxaprod-wp-theme/vendor/wp-cli/i18n-command/src/JsFunctionsScanner.php

388 lines
12 KiB
PHP

<?php
namespace WP_CLI\I18n;
use Gettext\Utils\JsFunctionsScanner as GettextJsFunctionsScanner;
use Gettext\Utils\ParsedComment;
use Peast\Peast;
use Peast\Syntax\Node;
use Peast\Traverser;
final class JsFunctionsScanner extends GettextJsFunctionsScanner {
/**
* If not false, comments will be extracted.
*
* @var string|false|array
*/
private $extract_comments = false;
/**
* Holds a list of source code comments already added to a string.
*
* Prevents associating the same comment to multiple strings.
*
* @var Node\Comment[] $comments_cache
*/
private $comments_cache = [];
/**
* Enable extracting comments that start with a tag (if $tag is empty all the comments will be extracted).
*
* @param mixed $tag
*/
public function enableCommentsExtraction( $tag = '' ) {
$this->extract_comments = $tag;
}
/**
* Disable comments extraction.
*/
public function disableCommentsExtraction() {
$this->extract_comments = false;
}
/**
* {@inheritdoc}
*/
public function saveGettextFunctions( $translations, array $options ) {
// Ignore multiple translations for now.
// @todo Add proper support for multiple translations.
if ( is_array( $translations ) ) {
$translations = $translations[0];
}
$peast_options = [
'sourceType' => Peast::SOURCE_TYPE_MODULE,
'comments' => false !== $this->extract_comments,
'jsx' => true,
];
$ast = Peast::latest( $this->code, $peast_options )->parse();
$traverser = new Traverser();
$all_comments = [];
/**
* Traverse through JS code to find and extract gettext functions.
*
* Make sure translator comments in front of variable declarations
* and inside nested call expressions are available when parsing the function call.
*/
$traverser->addFunction(
function ( $node ) use ( &$translations, $options, &$all_comments ) {
$functions = $options['functions'];
$file = $options['file'];
$add_reference = ! empty( $options['addReferences'] );
/** @var Node\Node $node */
foreach ( $node->getLeadingComments() as $comment ) {
$all_comments[] = $comment;
}
/** @var Node\CallExpression $node */
if ( 'CallExpression' !== $node->getType() ) {
return;
}
$callee = $this->resolveExpressionCallee( $node );
if ( ! $callee || ! isset( $functions[ $callee['name'] ] ) ) {
return;
}
/** @var Node\CallExpression $node */
foreach ( $node->getArguments() as $argument ) {
// Support nested function calls.
$argument->setLeadingComments( $argument->getLeadingComments() + $node->getLeadingComments() );
}
foreach ( $callee['comments'] as $comment ) {
$all_comments[] = $comment;
}
$domain = null;
$original = null;
$context = null;
$plural = null;
$args = [];
/** @var Node\Node $argument */
foreach ( $node->getArguments() as $argument ) {
foreach ( $argument->getLeadingComments() as $comment ) {
$all_comments[] = $comment;
}
if (
'Identifier' === $argument->getType() ||
'Expression' === substr( $argument->getType(), -strlen( 'Expression' ) )
) {
$args[] = ''; // The value doesn't matter as it's unused.
continue;
}
if ( 'Literal' === $argument->getType() ) {
/** @var Node\Literal $argument */
$args[] = $argument->getValue();
continue;
}
if ( 'TemplateLiteral' === $argument->getType() && 0 === count( $argument->getExpressions() ) ) {
/** @var Node\TemplateLiteral $argument */
/** @var Node\TemplateElement[] $parts */
// Since there are no expressions within the TemplateLiteral, there is only one TemplateElement.
$parts = $argument->getParts();
$args[] = $parts[0]->getValue();
continue;
}
// If we reach this, an unsupported argument type has been encountered.
// Do not try to parse this function call at all.
return;
}
switch ( $functions[ $callee['name'] ] ) {
case 'text_domain':
case 'gettext':
list( $original, $domain ) = array_pad( $args, 2, null );
break;
case 'text_context_domain':
list( $original, $context, $domain ) = array_pad( $args, 3, null );
break;
case 'single_plural_number_domain':
list( $original, $plural, $number, $domain ) = array_pad( $args, 4, null );
break;
case 'single_plural_number_context_domain':
list( $original, $plural, $number, $context, $domain ) = array_pad( $args, 5, null );
break;
}
if ( '' === (string) $original ) {
return;
}
if ( $domain !== $translations->getDomain() && null !== $translations->getDomain() ) {
return;
}
if ( isset( $options['line'] ) ) {
$line = $options['line'];
} else {
$line = $node->getLocation()->getStart()->getLine();
}
$translation = $translations->insert( $context, $original, $plural );
if ( $add_reference ) {
$translation->addReference( $file, $line );
}
/** @var Node\Comment $comment */
foreach ( $all_comments as $comment ) {
// Comments should be before the translation.
if ( ! $this->commentPrecedesNode( $comment, $node ) ) {
continue;
}
if ( in_array( $comment, $this->comments_cache, true ) ) {
continue;
}
$parsed_comment = ParsedComment::create( $comment->getRawText(), $comment->getLocation()->getStart()->getLine() );
$prefixes = array_filter( (array) $this->extract_comments );
if ( $parsed_comment->checkPrefixes( $prefixes ) ) {
$translation->addExtractedComment( $parsed_comment->getComment() );
$this->comments_cache[] = $comment;
}
}
if ( isset( $parsed_comment ) ) {
$all_comments = [];
}
}
);
/**
* Traverse through JS code contained within eval() to find and extract gettext functions.
*/
$scanner = $this;
$traverser->addFunction(
function ( $node ) use ( &$translations, $options, $scanner ) {
/** @var Node\CallExpression $node */
if ( 'CallExpression' !== $node->getType() ) {
return;
}
$callee = $this->resolveExpressionCallee( $node );
if ( ! $callee || 'eval' !== $callee['name'] ) {
return;
}
$eval_contents = '';
/** @var Node\Node $argument */
foreach ( $node->getArguments() as $argument ) {
if ( 'Literal' === $argument->getType() ) {
/** @var Node\Literal $argument */
$eval_contents = $argument->getValue();
break;
}
}
if ( ! $eval_contents ) {
return;
}
// Override the line location to be that of the eval().
$options['line'] = $node->getLocation()->getStart()->getLine();
$class = get_class( $scanner );
$evals = new $class( $eval_contents );
$evals->enableCommentsExtraction( $options['extractComments'] );
$evals->saveGettextFunctions( $translations, $options );
}
);
$traverser->traverse( $ast );
}
/**
* Resolve the callee of a call expression using known formats.
*
* @param Node\CallExpression $node The call expression whose callee to resolve.
*
* @return array|bool Array containing the name and comments of the identifier if resolved. False if not.
*/
private function resolveExpressionCallee( Node\CallExpression $node ) {
$callee = $node->getCallee();
// If the callee is a simple identifier it can simply be returned.
// For example: __( "translation" ).
if ( 'Identifier' === $callee->getType() ) {
return [
'name' => $callee->getName(),
'comments' => $callee->getLeadingComments(),
];
}
// If the callee is a member expression resolve it to the property.
// For example: wp.i18n.__( "translation" ) or u.__( "translation" ).
if (
'MemberExpression' === $callee->getType() &&
'Identifier' === $callee->getProperty()->getType()
) {
// Make sure to unpack wp.i18n which is a nested MemberExpression.
$comments = 'MemberExpression' === $callee->getObject()->getType()
? $callee->getObject()->getObject()->getLeadingComments()
: $callee->getObject()->getLeadingComments();
return [
'name' => $callee->getProperty()->getName(),
'comments' => $comments,
];
}
// If the callee is a call expression as created by Webpack resolve it.
// For example: Object(u.__)( "translation" ).
if (
'CallExpression' === $callee->getType() &&
'Identifier' === $callee->getCallee()->getType() &&
'Object' === $callee->getCallee()->getName() &&
[] !== $callee->getArguments() &&
'MemberExpression' === $callee->getArguments()[0]->getType()
) {
$property = $callee->getArguments()[0]->getProperty();
// Matches minified webpack statements: Object(u.__)( "translation" ).
if ( 'Identifier' === $property->getType() ) {
return [
'name' => $property->getName(),
'comments' => $callee->getCallee()->getLeadingComments(),
];
}
// Matches unminified webpack statements:
// Object(_wordpress_i18n__WEBPACK_IMPORTED_MODULE_7__["__"])( "translation" );
if ( 'Literal' === $property->getType() ) {
$name = $property->getValue();
// Matches mangled webpack statement:
// Object(_wordpress_i18n__WEBPACK_IMPORTED_MODULE_7__[/* __ */ "a"])( "translation" );
$leading_property_comments = $property->getLeadingComments();
if ( count( $leading_property_comments ) === 1 && $leading_property_comments[0]->getKind() === 'multiline' ) {
$name = trim( $leading_property_comments[0]->getText() );
}
return [
'name' => $name,
'comments' => $callee->getCallee()->getLeadingComments(),
];
}
}
// If the callee is an indirect function call as created by babel, resolve it.
// For example: `(0, u.__)( "translation" )`.
if (
'ParenthesizedExpression' === $callee->getType()
&& 'SequenceExpression' === $callee->getExpression()->getType()
&& 2 === count( $callee->getExpression()->getExpressions() )
&& 'Literal' === $callee->getExpression()->getExpressions()[0]->getType()
&& [] !== $node->getArguments()
) {
// Matches any general indirect function call: `(0, __)( "translation" )`.
if ( 'Identifier' === $callee->getExpression()->getExpressions()[1]->getType() ) {
return [
'name' => $callee->getExpression()->getExpressions()[1]->getName(),
'comments' => $callee->getLeadingComments(),
];
}
// Matches indirect function calls used by babel for module imports: `(0, _i18n.__)( "translation" )`.
if ( 'MemberExpression' === $callee->getExpression()->getExpressions()[1]->getType() ) {
$property = $callee->getExpression()->getExpressions()[1]->getProperty();
if ( 'Identifier' === $property->getType() ) {
return [
'name' => $property->getName(),
'comments' => $callee->getLeadingComments(),
];
}
}
}
// Unknown format.
return false;
}
/**
* Returns wether or not a comment precedes a node.
* The comment must be before the node and on the same line or the one before.
*
* @param Node\Comment $comment The comment.
* @param Node\Node $node The node.
*
* @return bool Whether or not the comment precedes the node.
*/
private function commentPrecedesNode( Node\Comment $comment, Node\Node $node ) {
// Comments should be on the same or an earlier line than the translation.
if ( $node->getLocation()->getStart()->getLine() - $comment->getLocation()->getEnd()->getLine() > 1 ) {
return false;
}
// Comments on the same line should be before the translation.
if (
$node->getLocation()->getStart()->getLine() === $comment->getLocation()->getEnd()->getLine() &&
$node->getLocation()->getStart()->getColumn() < $comment->getLocation()->getStart()->getColumn()
) {
return false;
}
return true;
}
}