": [ {item}, ... ]} using native JSON encoding. */ class Json_Renderer implements Renderer { /** @var string */ private $root; /** @var bool */ private $pretty; /** * @param string $root Collection key. * @param bool $pretty Pretty print. * @throws \InvalidArgumentException For an invalid collection name. */ public function __construct( string $root, bool $pretty = false ) { if ( ! Path_Validator::is_valid_name( $root, true ) ) { throw new \InvalidArgumentException( 'Invalid JSON collection name.' ); } $this->root = $root; $this->pretty = $pretty; } /** * {@inheritdoc} */ public function content_type(): string { return 'application/json; charset=UTF-8'; } /** * Renders items as JSON. * * @param array> $items Mapped items. * @return string * @throws \JsonException When encoding fails. * @throws \InvalidArgumentException For invalid keys. */ public function render( array $items ): string { $prepared = array(); foreach ( $items as $item ) { $prepared[] = $this->prepare( $item ); } // Readable output ("Q&A", "é", "a/b") but "<" and ">" stay escaped so the body can never be read as markup. $flags = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_THROW_ON_ERROR; if ( $this->pretty ) { $flags |= JSON_PRETTY_PRINT; } return json_encode( (object) array( $this->root => $prepared ), $flags ); // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode -- wp_json_encode would add a non-throwing fallback; we need strict errors and no WordPress dependency. } /** * Cleans strings and turns groups into objects so empty groups encode as {}. * * @param mixed $node Node. * @return mixed * @throws \InvalidArgumentException For invalid keys. */ private function prepare( $node ) { if ( $node instanceof Term_List ) { return array_map( array( Text_Cleaner::class, 'clean' ), $node->names ); } if ( is_array( $node ) ) { // XML attributes do not exist in JSON: drop them, keep the text. if ( array_key_exists( Mapping_Engine::VALUE, $node ) ) { return $this->prepare( $node[ Mapping_Engine::VALUE ] ); } unset( $node[ Mapping_Engine::ATTRIBUTES ] ); if ( Text_Cleaner::is_list( $node ) && array() !== $node ) { return array_map( array( $this, 'prepare' ), $node ); } $object = new \stdClass(); foreach ( $node as $name => $value ) { $name = (string) $name; if ( ! Path_Validator::is_valid_name( $name, true ) ) { throw new \InvalidArgumentException( 'Invalid JSON key.' ); } $object->$name = $this->prepare( $value ); } return $object; } if ( is_int( $node ) ) { return $node; } return null === $node ? null : Text_Cleaner::clean( (string) $node ); } }