基础代码

This commit is contained in:
2021-02-26 22:23:13 +08:00
parent 7884df52f0
commit a719feebba
2585 changed files with 328263 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
# Configuration file for nektos/act.
# See https://github.com/nektos/act#configuration
-P ubuntu-latest=shivammathur/node:latest
+18
View File
@@ -0,0 +1,18 @@
.DS_Store
.git
.gitignore
.gitlab-ci.yml
.editorconfig
.travis.yml
behat.yml
circle.yml
phpcs.xml.dist
phpunit.xml.dist
bin/
features/
utils/
*.zip
*.tar.gz
*.swp
*.txt
*.log
+25
View File
@@ -0,0 +1,25 @@
# This file is for unifying the coding style for different editors and IDEs
# editorconfig.org
# WordPress Coding Standards
# https://make.wordpress.org/core/handbook/coding-standards/
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = tab
[{.jshintrc,*.json,*.yml,*.feature}]
indent_style = space
indent_size = 2
[{*.txt,wp-config-sample.php}]
end_of_line = crlf
[composer.json]
indent_style = space
indent_size = 4
+1
View File
@@ -0,0 +1 @@
* @wp-cli/committers
+11
View File
@@ -0,0 +1,11 @@
<!--
Thanks for creating a new issue!
Found a bug or want to suggest an enhancement? Before completing your issue, please review our best practices: https://make.wordpress.org/cli/handbook/bug-reports/
Need help with something? GitHub issues aren't for general support questions, but there are other venues you can try: https://wp-cli.org/#support
You can safely delete this comment.
-->
@@ -0,0 +1,16 @@
<!--
Thanks for submitting a pull request!
Please review our contributing guidelines if you haven't recently: https://make.wordpress.org/cli/handbook/contributing/#creating-a-pull-request
Here's an overview to our process:
1. One of the project committers will soon provide a code review: https://make.wordpress.org/cli/handbook/code-review/
2. You are expected to address the code review comments in a timely manner (if we don't hear from you in two weeks, we'll consider your pull request abandoned).
3. Please make sure to include functional tests for your changes.
4. The reviewing committer will merge your pull request as soon as it passes code review (and provided it fits within the scope of the project).
You can safely delete this comment.
-->
+9
View File
@@ -0,0 +1,9 @@
version: 2
updates:
- package-ecosystem: composer
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10
labels:
- scope:distribution
+19
View File
@@ -0,0 +1,19 @@
# Configuration for move-issues - https://github.com/dessant/move-issues
# Repository to extend settings from
_extends: wp-cli/wp-cli
# Delete the command comment when it contains no other content
# deleteCommand: true
# Close the source issue after moving
# closeSourceIssue: true
# Lock the source issue after moving
# lockSourceIssue: false
# Mention issue and comment authors
# mentionAuthors: true
# Preserve mentions in the issue content
# keepContentMentions: true
+28
View File
@@ -0,0 +1,28 @@
# Used by Probot Settings: https://probot.github.io/apps/settings/
repository:
description: Provides internationalization tools for WordPress projects.
labels:
- name: bug
color: fc2929
- name: scope:documentation
color: 0e8a16
- name: scope:testing
color: 5319e7
- name: good-first-issue
color: eb6420
- name: help-wanted
color: 159818
- name: maybelater
color: c2e0c6
- name: state:unconfirmed
color: bfe5bf
- name: state:unsupported
color: bfe5bf
- name: wontfix
color: c2e0c6
- name: command:i18n
color: c5def5
- name: command:i18n-make-pot
color: c5def5
- name: command:i18n-make-json
color: c5def5
@@ -0,0 +1,102 @@
name: Code Quality Checks
on: pull_request
jobs:
lint: #-----------------------------------------------------------------------
name: Lint PHP files
runs-on: ubuntu-latest
steps:
- name: Check out source code
uses: actions/checkout@v2
- name: Check existence of composer.json file
id: check_composer_file
uses: andstor/file-existence-action@v1
with:
files: "composer.json"
- name: Set up PHP envirnoment
uses: shivammathur/setup-php@v2
with:
php-version: '7.4'
tools: cs2pr
- name: Get Composer cache Directory
if: steps.check_composer_file.outputs.files_exists == 'true'
id: composer-cache
run: |
echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Use Composer cache
if: steps.check_composer_file.outputs.files_exists == 'true'
uses: actions/cache@v1
with:
path: ${{ steps['composer-cache'].outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-
- name: Install dependencies
if: steps.check_composer_file.outputs.files_exists == 'true'
run: COMPOSER_ROOT_VERSION=dev-master composer install --prefer-dist --no-progress --no-suggest
- name: Check existence of vendor/bin/parallel-lint file
id: check_linter_file
uses: andstor/file-existence-action@v1
with:
files: "vendor/bin/parallel-lint"
- name: Run Linter
if: steps.check_linter_file.outputs.files_exists == 'true'
run: vendor/bin/parallel-lint -j 10 . --exclude vendor --checkstyle | cs2pr
phpcs: #----------------------------------------------------------------------
name: PHPCS
runs-on: ubuntu-latest
steps:
- name: Check out source code
uses: actions/checkout@v2
- name: Check existence of composer.json & phpcs.xml.dist files
id: check_files
uses: andstor/file-existence-action@v1
with:
files: "composer.json, phpcs.xml.dist"
- name: Set up PHP envirnoment
uses: shivammathur/setup-php@v2
with:
php-version: '7.4'
tools: cs2pr
- name: Get Composer cache Directory
if: steps.check_files.outputs.files_exists == 'true'
id: composer-cache
run: |
echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Use Composer cache
if: steps.check_files.outputs.files_exists == 'true'
uses: actions/cache@v1
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-
- name: Install dependencies
if: steps.check_files.outputs.files_exists == 'true'
run: COMPOSER_ROOT_VERSION=dev-master composer install --prefer-dist --no-progress --no-suggest
- name: Check existence of vendor/bin/phpcs file
id: check_phpcs_binary_file
uses: andstor/file-existence-action@v1
with:
files: "vendor/bin/phpcs"
- name: Run PHPCS
if: steps.check_phpcs_binary_file.outputs.files_exists == 'true'
run: vendor/bin/phpcs -q --report=checkstyle | cs2pr
+146
View File
@@ -0,0 +1,146 @@
name: Testing
on: pull_request
jobs:
unit: #-----------------------------------------------------------------------
name: Unit test / PHP ${{ matrix.php }}
strategy:
fail-fast: false
matrix:
php: ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0']
runs-on: ubuntu-latest
steps:
- name: Check out source code
uses: actions/checkout@v2
- name: Check existence of composer.json file
id: check_files
uses: andstor/file-existence-action@v1
with:
files: "composer.json, phpunit.xml.dist"
- name: Set up PHP environment
if: steps.check_files.outputs.files_exists == 'true'
uses: shivammathur/setup-php@v2
with:
php-version: '${{ matrix.php }}'
coverage: none
tools: composer,cs2pr
- name: Get Composer cache Directory
if: steps.check_files.outputs.files_exists == 'true'
id: composer-cache
run: |
echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Use Composer cache
if: steps.check_files.outputs.files_exists == 'true'
uses: actions/cache@master
with:
path: ${{ steps['composer-cache'].outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-
- name: Install dependencies
if: steps.check_files.outputs.files_exists == 'true'
run: COMPOSER_ROOT_VERSION=dev-master composer install --prefer-dist --no-progress --no-suggest
- name: Setup problem matcher to provide annotations for PHPUnit
if: steps.check_files.outputs.files_exists == 'true'
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: Run PHPUnit
if: steps.check_files.outputs.files_exists == 'true'
run: composer phpunit
functional: #----------------------------------------------------------------------
name: Functional - WP ${{ matrix.wp }} on PHP ${{ matrix.php }}
strategy:
fail-fast: false
matrix:
php: ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4']
wp: ['latest']
test: ["composer behat || composer behat-rerun"]
include:
- php: '5.6'
wp: 'trunk'
test: "composer behat || composer behat-rerun"
- php: '7.4'
wp: 'trunk'
test: "composer behat || composer behat-rerun"
- php: '5.6'
wp: '3.7'
test: "composer behat || composer behat-rerun || true"
runs-on: ubuntu-latest
services:
mysql:
image: mysql:5.7
env:
MYSQL_DATABASE: wp_cli_test
MYSQL_USER: root
MYSQL_ROOT_PASSWORD: root
ports:
- 3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- name: Check out source code
uses: actions/checkout@v2
- name: Check existence of composer.json & behat.yml files
id: check_files
uses: andstor/file-existence-action@v1
with:
files: "composer.json, behat.yml"
- name: Set up PHP envirnoment
uses: shivammathur/setup-php@v2
with:
php-version: '${{ matrix.php }}'
extensions: mysql, zip
coverage: none
tools: composer
- name: Get Composer cache Directory
if: steps.check_files.outputs.files_exists == 'true'
id: composer-cache
run: |
echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Use Composer cache
if: steps.check_files.outputs.files_exists == 'true'
uses: actions/cache@master
with:
path: ${{ steps['composer-cache'].outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-
- name: Install dependencies
if: steps.check_files.outputs.files_exists == 'true'
run: COMPOSER_ROOT_VERSION=dev-master composer install --prefer-dist --no-progress --no-suggest
- name: Start MySQL server
if: steps.check_files.outputs.files_exists == 'true'
run: sudo service mysql start
- name: Prepare test database
if: steps.check_files.outputs.files_exists == 'true'
run: |
export MYQSL_HOST=127.0.0.1
export MYSQL_TCP_PORT=${{ job.services.mysql.ports['3306'] }}
mysql -e 'CREATE DATABASE IF NOT EXISTS wp_cli_test;' -uroot -proot
mysql -e 'GRANT ALL PRIVILEGES ON wp_cli_test.* TO "wp_cli_test"@"127.0.0.1" IDENTIFIED BY "password1"' -uroot -proot
mysql -e 'GRANT ALL PRIVILEGES ON wp_cli_test_scaffold.* TO "wp_cli_test"@"127.0.0.1" IDENTIFIED BY "password1"' -uroot -proot
- name: Run Behat
if: steps.check_files.outputs.files_exists == 'true'
env:
WP_VERSION: '${{ matrix.wp }}'
run: ${{ matrix.test }}
+13
View File
@@ -0,0 +1,13 @@
.DS_Store
wp-cli.local.yml
/node_modules
/vendor
*.zip
*.tar.gz
*.swp
*.txt
*.log
composer.lock
phpunit.xml
phpcs.xml
.phpcs.xml
+8
View File
@@ -0,0 +1,8 @@
Contributing
============
We appreciate you taking the initiative to contribute to this project.
Contributing isnt limited to just code. We encourage you to contribute in the way that best fits your abilities, by writing tutorials, giving a demo at your local meetup, helping other users with their support questions, or revising our documentation.
For a more thorough introduction, [check out WP-CLI's guide to contributing](https://make.wordpress.org/cli/handbook/contributing/). This package follows those policy and guidelines.
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (C) 2011-2018 WP-CLI Development Group (https://github.com/wp-cli/i18n-command/contributors)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+196
View File
@@ -0,0 +1,196 @@
wp-cli/i18n-command
===================
Provides internationalization tools for WordPress projects.
[![Build Status](https://travis-ci.org/wp-cli/i18n-command.svg?branch=master)](https://travis-ci.org/wp-cli/i18n-command)
Quick links: [Using](#using) | [Installing](#installing) | [Contributing](#contributing) | [Support](#support)
## Using
This package implements the following commands:
### wp i18n
Provides internationalization tools for WordPress projects.
~~~
wp i18n
~~~
**EXAMPLES**
# Create a POT file for the WordPress plugin/theme in the current directory
$ wp i18n make-pot . languages/my-plugin.pot
### wp i18n make-pot
Create a POT file for a WordPress project.
~~~
wp i18n make-pot <source> [<destination>] [--slug=<slug>] [--domain=<domain>] [--ignore-domain] [--merge[=<paths>]] [--subtract=<paths>] [--include=<paths>] [--exclude=<paths>] [--headers=<headers>] [--skip-js] [--skip-audit] [--file-comment=<file-comment>] [--package-name=<name>]
~~~
Scans PHP and JavaScript files for translatable strings, as well as theme stylesheets and plugin files
if the source directory is detected as either a plugin or theme.
**OPTIONS**
<source>
Directory to scan for string extraction.
[<destination>]
Name of the resulting POT file.
[--slug=<slug>]
Plugin or theme slug. Defaults to the source directory's basename.
[--domain=<domain>]
Text domain to look for in the source code, unless the `--ignore-domain` option is used.
By default, the "Text Domain" header of the plugin or theme is used.
If none is provided, it falls back to the project slug.
[--ignore-domain]
Ignore the text domain completely and extract strings with any text domain.
[--merge[=<paths>]]
Comma-separated list of POT files whose contents should be merged with the extracted strings.
If left empty, defaults to the destination POT file. POT file headers will be ignored.
[--subtract=<paths>]
Comma-separated list of POT files whose contents should act as some sort of blacklist for string extraction.
Any string which is found on that blacklist will not be extracted.
This can be useful when you want to create multiple POT files from the same source directory with slightly
different content and no duplicate strings between them.
[--include=<paths>]
Comma-separated list of files and paths that should be used for string extraction.
If provided, only these files and folders will be taken into account for string extraction.
For example, `--include="src,my-file.php` will ignore anything besides `my-file.php` and files in the `src`
directory. Simple glob patterns can be used, i.e. `--include=foo-*.php` includes any PHP file with the `foo-`
prefix. Leading and trailing slashes are ignored, i.e. `/my/directory/` is the same as `my/directory`.
[--exclude=<paths>]
Comma-separated list of files and paths that should be skipped for string extraction.
For example, `--exclude=".github,myfile.php` would ignore any strings found within `myfile.php` or the `.github`
folder. Simple glob patterns can be used, i.e. `--exclude=foo-*.php` excludes any PHP file with the `foo-`
prefix. Leading and trailing slashes are ignored, i.e. `/my/directory/` is the same as `my/directory`. The
following files and folders are always excluded: node_modules, .git, .svn, .CVS, .hg, vendor, *.min.js.
[--headers=<headers>]
Array in JSON format of custom headers which will be added to the POT file. Defaults to empty array.
[--skip-js]
Skips JavaScript string extraction. Useful when this is done in another build step, e.g. through Babel.
[--skip-audit]
Skips string audit where it tries to find possible mistakes in translatable strings. Useful when running in an
automated environment.
[--file-comment=<file-comment>]
String that should be added as a comment to the top of the resulting POT file.
By default, a copyright comment is added for WordPress plugins and themes in the following manner:
```
Copyright (C) 2018 Example Plugin Author
This file is distributed under the same license as the Example Plugin package.
```
If a plugin or theme specifies a license in their main plugin file or stylesheet, the comment looks like
this:
```
Copyright (C) 2018 Example Plugin Author
This file is distributed under the GPLv2.
```
[--package-name=<name>]
Name to use for package name in the resulting POT file's `Project-Id-Version` header.
Overrides plugin or theme name, if applicable.
**EXAMPLES**
# Create a POT file for the WordPress plugin/theme in the current directory
$ wp i18n make-pot . languages/my-plugin.pot
# Create a POT file for the continents and cities list in WordPress core.
$ wp i18n make-pot . continents-and-cities.pot --include="wp-admin/includes/continents-cities.php"
--ignore-domain
### wp i18n make-json
Extract JavaScript strings from PO files and add them to individual JSON files.
~~~
wp i18n make-json <source> [<destination>] [--purge] [--pretty-print]
~~~
For JavaScript internationalization purposes, WordPress requires translations to be split up into
one Jed-formatted JSON file per JavaScript source file.
See https://make.wordpress.org/core/2018/11/09/new-javascript-i18n-support-in-wordpress/ to learn more
about WordPress JavaScript internationalization.
**OPTIONS**
<source>
Path to an existing PO file or a directory containing multiple PO files.
[<destination>]
Path to the destination directory for the resulting JSON files. Defaults to the source directory.
[--purge]
Whether to purge the strings that were extracted from the original source file. Defaults to true, use `--no-purge` to skip the removal.
[--pretty-print]
Pretty-print resulting JSON files.
**EXAMPLES**
# Create JSON files for all PO files in the languages directory
$ wp i18n make-json languages
# Create JSON files for my-plugin-de_DE.po and leave the PO file untouched.
$ wp i18n make-json my-plugin-de_DE.po /tmp --no-purge
## Installing
This package is included with WP-CLI itself, no additional installation necessary.
To install the latest version of this package over what's included in WP-CLI, run:
wp package install git@github.com:wp-cli/i18n-command.git
## Contributing
We appreciate you taking the initiative to contribute to this project.
Contributing isnt limited to just code. We encourage you to contribute in the way that best fits your abilities, by writing tutorials, giving a demo at your local meetup, helping other users with their support questions, or revising our documentation.
For a more thorough introduction, [check out WP-CLI's guide to contributing](https://make.wordpress.org/cli/handbook/contributing/). This package follows those policy and guidelines.
### Reporting a bug
Think youve found a bug? Wed love for you to help us get it fixed.
Before you create a new issue, you should [search existing issues](https://github.com/wp-cli/i18n-command/issues?q=label%3Abug%20) to see if theres an existing resolution to it, or if its already been fixed in a newer version.
Once youve done a bit of searching and discovered there isnt an open or fixed issue for your bug, please [create a new issue](https://github.com/wp-cli/i18n-command/issues/new). Include as much detail as you can, and clear steps to reproduce if possible. For more guidance, [review our bug report documentation](https://make.wordpress.org/cli/handbook/bug-reports/).
### Creating a pull request
Want to contribute a new feature? Please first [open a new issue](https://github.com/wp-cli/i18n-command/issues/new) to discuss whether the feature is a good fit for the project.
Once you've decided to commit the time to seeing your pull request through, [please follow our guidelines for creating a pull request](https://make.wordpress.org/cli/handbook/pull-requests/) to make sure it's a pleasant experience. See "[Setting up](https://make.wordpress.org/cli/handbook/pull-requests/#setting-up)" for details specific to working on this package locally.
## Support
Github issues aren't for general support questions, but there are other venues you can try: https://wp-cli.org/#support
*This README.md is generated dynamically from the project's codebase using `wp scaffold package-readme` ([doc](https://github.com/wp-cli/scaffold-package-command#wp-scaffold-package-readme)). To suggest changes, please submit a pull request against the corresponding part of the codebase.*
+64
View File
@@ -0,0 +1,64 @@
{
"name": "wp-cli/i18n-command",
"type": "wp-cli-package",
"description": "Provides internationalization tools for WordPress projects.",
"homepage": "https://github.com/wp-cli/i18n-command",
"license": "MIT",
"authors": [
{
"name": "Pascal Birchler",
"homepage": "https://pascalbirchler.com/"
}
],
"require": {
"gettext/gettext": "^4.8",
"mck89/peast": "^1.8",
"wp-cli/wp-cli": "^2"
},
"require-dev": {
"wp-cli/scaffold-command": "^1.2 || ^2",
"wp-cli/wp-cli-tests": "^2.1.3"
},
"config": {
"process-timeout": 7200,
"sort-packages": true
},
"extra": {
"branch-alias": {
"dev-master": "2.x-dev"
},
"bundled": true,
"commands": [
"i18n",
"i18n make-pot",
"i18n make-json"
]
},
"autoload": {
"psr-4": {
"WP_CLI\\I18n\\": "src/"
},
"files": [
"i18n-command.php"
]
},
"minimum-stability": "dev",
"prefer-stable": true,
"scripts": {
"behat": "run-behat-tests",
"behat-rerun": "rerun-behat-tests",
"lint": "run-linter-tests",
"phpcs": "run-phpcs-tests",
"phpunit": "run-php-unit-tests",
"prepare-tests": "install-package-tests",
"test": [
"@lint",
"@phpcs",
"@phpunit",
"@behat"
]
},
"support": {
"issues": "https://github.com/wp-cli/i18n-command/issues"
}
}
+566
View File
@@ -0,0 +1,566 @@
Feature: Split PO files into JSON files.
Background:
Given a WP install
Scenario: Bail for invalid source file or directory
When I try `wp i18n make-json foo`
Then STDERR should contain:
"""
Error: Source file or directory does not exist!
"""
And the return code should be 1
Scenario: Uses source folder as destination by default
Given an empty foo-plugin directory
And a foo-plugin/foo-plugin-de_DE.po file:
"""
# Copyright (C) 2018 Foo Plugin
# This file is distributed under the same license as the Foo Plugin package.
msgid ""
msgstr ""
"Project-Id-Version: Foo Plugin\n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
"X-Domain: foo-plugin\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: foo-plugin.js:15
msgid "Foo Plugin"
msgstr "Foo Plugin"
"""
When I run `wp i18n make-json foo-plugin`
Then STDOUT should contain:
"""
Success: Created 1 file.
"""
And the return code should be 0
And the foo-plugin/foo-plugin-de_DE-56746e49c6485323d16a717754b7447e.json file should exist
Scenario: Allows setting custom destination directory
Given an empty foo-plugin directory
And a foo-plugin/foo-plugin-de_DE.po file:
"""
# Copyright (C) 2018 Foo Plugin
# This file is distributed under the same license as the Foo Plugin package.
msgid ""
msgstr ""
"Project-Id-Version: Foo Plugin\n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
"X-Domain: foo-plugin\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: foo-plugin.js:15
msgid "Foo Plugin"
msgstr "Foo Plugin"
"""
When I run `wp i18n make-json foo-plugin result`
Then STDOUT should contain:
"""
Success: Created 1 file.
"""
And the return code should be 0
And the result/foo-plugin-de_DE-56746e49c6485323d16a717754b7447e.json file should exist
Scenario: Sets some meta data
Given an empty foo-plugin directory
And a foo-plugin/foo-plugin-de_DE.po file:
"""
# Copyright (C) 2018 Foo Plugin
# This file is distributed under the same license as the Foo Plugin package.
msgid ""
msgstr ""
"Project-Id-Version: Foo Plugin\n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
"X-Domain: foo-plugin\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: foo-plugin.js:15
msgid "Foo Plugin"
msgstr "Foo Plugin"
"""
When I run `wp i18n make-json foo-plugin`
Then STDOUT should contain:
"""
Success: Created 1 file.
"""
And the return code should be 0
And the foo-plugin/foo-plugin-de_DE-56746e49c6485323d16a717754b7447e.json file should contain:
"""
"translation-revision-date":
"""
And the foo-plugin/foo-plugin-de_DE-56746e49c6485323d16a717754b7447e.json file should contain:
"""
"generator":"WP-CLI
"""
And the foo-plugin/foo-plugin-de_DE-56746e49c6485323d16a717754b7447e.json file should contain:
"""
"source":"foo-plugin.js"
"""
Scenario: Always uses messages as text domain
Given an empty foo-plugin directory
And a foo-plugin/foo-plugin-de_DE.po file:
"""
# Copyright (C) 2018 Foo Plugin
# This file is distributed under the same license as the Foo Plugin package.
msgid ""
msgstr ""
"Project-Id-Version: Foo Plugin\n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
"X-Domain: foo-plugin\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: foo-plugin.js:15
msgid "Foo Plugin"
msgstr "Foo Plugin"
"""
When I run `wp i18n make-json foo-plugin`
Then STDOUT should contain:
"""
Success: Created 1 file.
"""
And the return code should be 0
And the foo-plugin/foo-plugin-de_DE-56746e49c6485323d16a717754b7447e.json file should contain:
"""
"domain":"messages"
"""
And the foo-plugin/foo-plugin-de_DE-56746e49c6485323d16a717754b7447e.json file should contain:
"""
"messages":{
"""
Scenario: Sets correct plural form
Given an empty foo-plugin directory
And a foo-plugin/foo-plugin-de_DE.po file:
"""
# Copyright (C) 2018 Foo Plugin
# This file is distributed under the same license as the Foo Plugin package.
msgid ""
msgstr ""
"Project-Id-Version: Foo Plugin\n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
"X-Domain: foo-plugin\n"
"Plural-Forms: nplurals=3; plural=(n != 2);\n"
#: foo-plugin.js:15
msgid "Foo Plugin"
msgstr "Foo Plugin"
"""
When I run `wp i18n make-json foo-plugin`
Then STDOUT should contain:
"""
Success: Created 1 file.
"""
And the return code should be 0
And the foo-plugin/foo-plugin-de_DE-56746e49c6485323d16a717754b7447e.json file should contain:
"""
"plural-forms":"nplurals=3; plural=(n != 2);"
"""
Scenario: Sets default plural form if missing
Given an empty foo-plugin directory
And a foo-plugin/foo-plugin-de_DE.po file:
"""
# Copyright (C) 2018 Foo Plugin
# This file is distributed under the same license as the Foo Plugin package.
msgid ""
msgstr ""
"Project-Id-Version: Foo Plugin\n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
"X-Domain: foo-plugin\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: foo-plugin.js:15
msgid "Foo Plugin"
msgstr "Foo Plugin"
"""
When I run `wp i18n make-json foo-plugin`
Then STDOUT should contain:
"""
Success: Created 1 file.
"""
And the return code should be 0
And the foo-plugin/foo-plugin-de_DE-56746e49c6485323d16a717754b7447e.json file should contain:
"""
"plural-forms":"nplurals=2; plural=(n != 1);"
"""
Scenario: Splits PO file into multiple JSON files
Given an empty foo-plugin directory
And a foo-plugin/foo-plugin-de_DE.po file:
"""
# Copyright (C) 2018 Foo Plugin
# This file is distributed under the same license as the Foo Plugin package.
msgid ""
msgstr ""
"Project-Id-Version: Foo Plugin\n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
"X-Domain: foo-plugin\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: a.js:10
msgid "A"
msgstr "A"
#: b.js:10
msgid "B"
msgstr "B"
#: a.js:10
#: b.js:10
msgid "C"
msgstr "C"
#: foo-plugin.php:10
msgid "D"
msgstr "D"
"""
When I run `wp i18n make-json foo-plugin`
Then STDOUT should contain:
"""
Success: Created 2 files.
"""
And the return code should be 0
And the foo-plugin/foo-plugin-de_DE-95f0a310f289230d56c3a4949c17963e.json file should exist
And the foo-plugin/foo-plugin-de_DE-95f0a310f289230d56c3a4949c17963e.json file should contain:
"""
"A"
"""
And the foo-plugin/foo-plugin-de_DE-95f0a310f289230d56c3a4949c17963e.json file should contain:
"""
"C"
"""
And the foo-plugin/foo-plugin-de_DE-95f0a310f289230d56c3a4949c17963e.json file should not contain:
"""
"B"
"""
And the foo-plugin/foo-plugin-de_DE-95f0a310f289230d56c3a4949c17963e.json file should not contain:
"""
"D"
"""
And the foo-plugin/foo-plugin-de_DE-656ad21ad877025a82411b49aa0f8b88.json file should exist
And the foo-plugin/foo-plugin-de_DE-656ad21ad877025a82411b49aa0f8b88.json file should contain:
"""
"B"
"""
And the foo-plugin/foo-plugin-de_DE-656ad21ad877025a82411b49aa0f8b88.json file should contain:
"""
"C"
"""
And the foo-plugin/foo-plugin-de_DE-656ad21ad877025a82411b49aa0f8b88.json file should not contain:
"""
"A"
"""
And the foo-plugin/foo-plugin-de_DE-656ad21ad877025a82411b49aa0f8b88.json file should not contain:
"""
"D"
"""
And the foo-plugin/foo-plugin-de_DE.po file should contain:
"""
"D"
"""
And the foo-plugin/foo-plugin-de_DE.po file should not contain:
"""
"A"
"""
And the foo-plugin/foo-plugin-de_DE.po file should not contain:
"""
"B"
"""
And the foo-plugin/foo-plugin-de_DE.po file should not contain:
"""
"C"
"""
Scenario: Does not remove strings from original PO file
Given an empty foo-plugin directory
And a foo-plugin/foo-plugin-de_DE.po file:
"""
# Copyright (C) 2018 Foo Plugin
# This file is distributed under the same license as the Foo Plugin package.
msgid ""
msgstr ""
"Project-Id-Version: Foo Plugin\n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
"X-Domain: foo-plugin\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: a.js:10
msgid "A"
msgstr "A"
#: b.js:10
msgid "B"
msgstr "B"
#: a.js:10
#: b.js:10
msgid "C"
msgstr "C"
#: foo-plugin.php:10
msgid "D"
msgstr "D"
"""
When I run `wp i18n make-json foo-plugin --no-purge`
Then STDOUT should contain:
"""
Success: Created 2 files.
"""
And the return code should be 0
And the foo-plugin/foo-plugin-de_DE-95f0a310f289230d56c3a4949c17963e.json file should exist
And the foo-plugin/foo-plugin-de_DE-95f0a310f289230d56c3a4949c17963e.json file should contain:
"""
"A"
"""
And the foo-plugin/foo-plugin-de_DE-95f0a310f289230d56c3a4949c17963e.json file should contain:
"""
"C"
"""
And the foo-plugin/foo-plugin-de_DE-95f0a310f289230d56c3a4949c17963e.json file should not contain:
"""
"B"
"""
And the foo-plugin/foo-plugin-de_DE-95f0a310f289230d56c3a4949c17963e.json file should not contain:
"""
"D"
"""
And the foo-plugin/foo-plugin-de_DE-656ad21ad877025a82411b49aa0f8b88.json file should exist
And the foo-plugin/foo-plugin-de_DE-656ad21ad877025a82411b49aa0f8b88.json file should contain:
"""
"B"
"""
And the foo-plugin/foo-plugin-de_DE-656ad21ad877025a82411b49aa0f8b88.json file should contain:
"""
"C"
"""
And the foo-plugin/foo-plugin-de_DE-656ad21ad877025a82411b49aa0f8b88.json file should not contain:
"""
"A"
"""
And the foo-plugin/foo-plugin-de_DE-656ad21ad877025a82411b49aa0f8b88.json file should not contain:
"""
"D"
"""
And the foo-plugin/foo-plugin-de_DE.po file should contain:
"""
"D"
"""
And the foo-plugin/foo-plugin-de_DE.po file should contain:
"""
"A"
"""
And the foo-plugin/foo-plugin-de_DE.po file should contain:
"""
"B"
"""
And the foo-plugin/foo-plugin-de_DE.po file should contain:
"""
"C"
"""
Scenario: Correctly saves strings with context
Given an empty foo-plugin directory
And a foo-plugin/foo-plugin-de_DE.po file:
"""
# Copyright (C) 2018 Foo Plugin
# This file is distributed under the same license as the Foo Plugin package.
msgid ""
msgstr ""
"Project-Id-Version: Foo Plugin\n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
"X-Domain: foo-plugin\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: foo-plugin.js:15
msgctxt "Plugin Name"
msgid "Foo Plugin (EN)"
msgstr "Foo Plugin (DE)"
"""
When I run `wp i18n make-json foo-plugin`
Then STDOUT should contain:
"""
Success: Created 1 file.
"""
And the return code should be 0
And the foo-plugin/foo-plugin-de_DE-56746e49c6485323d16a717754b7447e.json file should contain:
"""
"domain":"messages"
"""
And the foo-plugin/foo-plugin-de_DE-56746e49c6485323d16a717754b7447e.json file should contain:
"""
"Plugin Name\u0004Foo Plugin (EN)":["Foo Plugin (DE)"]
"""
Scenario: Should create pretty-printed JSON files
Given an empty foo-plugin directory
And a foo-plugin/foo-plugin-de_DE.po file:
"""
# Copyright (C) 2018 Foo Plugin
# This file is distributed under the same license as the Foo Plugin package.
msgid ""
msgstr ""
"Project-Id-Version: Foo Plugin\n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
"X-Domain: foo-plugin\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: foo-plugin.js:15
msgid "Foo Plugin"
msgstr "Foo Plugin"
"""
When I run `wp i18n make-json foo-plugin --pretty-print`
Then STDOUT should contain:
"""
Success: Created 1 file.
"""
And the return code should be 0
And the foo-plugin/foo-plugin-de_DE-56746e49c6485323d16a717754b7447e.json file should contain:
"""
"domain": "messages",
"locale_data": {
"messages": {
"": {
"domain": "messages",
"lang": "de_DE",
"plural-forms": "nplurals=2; plural=(n != 1);"
},
"Foo Plugin": [
"Foo Plugin"
]
}
}
"""
Scenario: Should not error for invalid languages
Given an empty foo-plugin directory
And a foo-plugin/foo-plugin-invalid.po file:
"""
# Copyright (C) 2018 Foo Plugin
# This file is distributed under the same license as the Foo Plugin package.
msgid ""
msgstr ""
"Project-Id-Version: Foo Plugin\n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: invalid\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
"X-Domain: foo-plugin\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: foo-plugin.js:15
msgid "Foo Plugin"
msgstr "Foo Plugin"
"""
When I run `wp i18n make-json foo-plugin`
Then STDOUT should contain:
"""
Success: Created 1 file.
"""
And the return code should be 0
And the foo-plugin/foo-plugin-invalid-56746e49c6485323d16a717754b7447e.json file should contain:
"""
"lang":"invalid"
"""
+158
View File
@@ -0,0 +1,158 @@
Feature: Generate MO files from PO files
Background:
Given a WP install
Scenario: Bail for invalid source directories
When I try `wp i18n make-mo foo`
Then STDERR should contain:
"""
Error: Source file or directory does not exist!
"""
And the return code should be 1
Scenario: Uses source folder as destination by default
Given an empty foo-plugin directory
And a foo-plugin/foo-plugin-de_DE.po file:
"""
# Copyright (C) 2018 Foo Plugin
# This file is distributed under the same license as the Foo Plugin package.
msgid ""
msgstr ""
"Project-Id-Version: Foo Plugin\n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
"X-Domain: foo-plugin\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: foo-plugin.js:15
msgid "Foo Plugin"
msgstr "Foo Plugin"
"""
When I run `wp i18n make-mo foo-plugin`
Then STDOUT should contain:
"""
Success: Created 1 file.
"""
And the return code should be 0
And the foo-plugin/foo-plugin-de_DE.mo file should exist
Scenario: Allows setting custom destination directory
Given an empty foo-plugin directory
And a foo-plugin/foo-plugin-de_DE.po file:
"""
# Copyright (C) 2018 Foo Plugin
# This file is distributed under the same license as the Foo Plugin package.
msgid ""
msgstr ""
"Project-Id-Version: Foo Plugin\n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
"X-Domain: foo-plugin\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: foo-plugin.js:15
msgid "Foo Plugin"
msgstr "Foo Plugin"
"""
When I run `wp i18n make-mo foo-plugin result`
Then STDOUT should contain:
"""
Success: Created 1 file.
"""
And the return code should be 0
And the result/foo-plugin-de_DE.mo file should exist
Scenario: Does include headers
Given an empty foo-plugin directory
And a foo-plugin/foo-plugin-de_DE.po file:
"""
# Copyright (C) 2018 Foo Plugin
# This file is distributed under the same license as the Foo Plugin package.
msgid ""
msgstr ""
"Project-Id-Version: Foo Plugin\n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
"X-Domain: foo-plugin\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: foo-plugin.js:15
msgid "Foo Plugin"
msgstr "Foo Plugin"
"""
When I run `wp i18n make-mo foo-plugin`
Then STDOUT should contain:
"""
Success: Created 1 file.
"""
And the return code should be 0
And the foo-plugin/foo-plugin-de_DE.mo file should contain:
"""
Language: de_DE
"""
And the foo-plugin/foo-plugin-de_DE.mo file should contain:
"""
X-Domain: foo-plugin
"""
Scenario: Does include translations
Given an empty foo-plugin directory
And a foo-plugin/foo-plugin-de_DE.po file:
"""
# Copyright (C) 2018 Foo Plugin
# This file is distributed under the same license as the Foo Plugin package.
msgid ""
msgstr ""
"Project-Id-Version: Foo Plugin\n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/foo-plugin\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2018-05-02T22:06:24+00:00\n"
"PO-Revision-Date: 2018-05-02T22:06:24+00:00\n"
"X-Domain: foo-plugin\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: foo-plugin.js:15
msgid "Foo Plugin"
msgstr "Bar Plugin"
"""
When I run `wp i18n make-mo foo-plugin`
Then STDOUT should contain:
"""
Success: Created 1 file.
"""
And the return code should be 0
And the foo-plugin/foo-plugin-de_DE.mo file should contain:
"""
Bar Plugin
"""
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
<?php
if ( ! class_exists( 'WP_CLI' ) ) {
return;
}
$wpcli_i18n_autoloader = __DIR__ . '/vendor/autoload.php';
if ( file_exists( $wpcli_i18n_autoloader ) ) {
require_once $wpcli_i18n_autoloader;
}
if ( class_exists( 'WP_CLI\Dispatcher\CommandNamespace' ) ) {
WP_CLI::add_command( 'i18n', '\WP_CLI\I18n\CommandNamespace' );
}
WP_CLI::add_command( 'i18n make-pot', '\WP_CLI\I18n\MakePotCommand' );
WP_CLI::add_command( 'i18n make-json', '\WP_CLI\I18n\MakeJsonCommand' );
WP_CLI::add_command( 'i18n make-mo', '\WP_CLI\I18n\MakeMoCommand' );
+86
View File
@@ -0,0 +1,86 @@
<?xml version="1.0"?>
<ruleset name="WP-CLI-i18n">
<description>Custom ruleset for WP-CLI I18n-command</description>
<!--
#############################################################################
COMMAND LINE ARGUMENTS
For help understanding this file: https://github.com/squizlabs/PHP_CodeSniffer/wiki/Annotated-ruleset.xml
For help using PHPCS: https://github.com/squizlabs/PHP_CodeSniffer/wiki/Usage
#############################################################################
-->
<!-- What to scan. -->
<file>.</file>
<!-- Ignoring select files/folders.
https://github.com/squizlabs/PHP_CodeSniffer/wiki/Advanced-Usage#ignoring-files-and-folders -->
<exclude-pattern>*/tests/data/*</exclude-pattern>
<!-- Show progress. -->
<arg value="p"/>
<!-- Strip the filepaths down to the relevant bit. -->
<arg name="basepath" value="./"/>
<!-- Check up to 8 files simultaneously. -->
<arg name="parallel" value="8"/>
<!--
#############################################################################
USE THE WP_CLI_CS RULESET
#############################################################################
-->
<rule ref="WP_CLI_CS"/>
<!--
#############################################################################
PROJECT SPECIFIC CONFIGURATION FOR SNIFFS
#############################################################################
-->
<!-- For help understanding the `testVersion` configuration setting:
https://github.com/PHPCompatibility/PHPCompatibility#sniffing-your-code-for-compatibility-with-specific-php-versions -->
<config name="testVersion" value="5.6-"/>
<!-- Verify that everything in the global namespace is either namespaced or prefixed.
See: https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#naming-conventions-prefix-everything-in-the-global-namespace -->
<rule ref="WordPress.NamingConventions.PrefixAllGlobals">
<properties>
<property name="prefixes" type="array">
<element value="WP_CLI\I18n"/><!-- Namespaces. -->
<element value="wpcli_i18n"/><!-- Global variables and such. -->
</property>
</properties>
</rule>
<!-- Whitelist method names for classes that extend a PSR-2 package. -->
<rule ref="WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid">
<exclude-pattern>*/src/IterableCodeExtractor\.php$</exclude-pattern>
</rule>
<!-- Whitelist property name for a a JSON-decoded object. -->
<rule ref="WordPress.NamingConventions.ValidVariableName">
<properties>
<property name="customPropertiesWhitelist" type="array">
<element value="sourcesContent"/>
<element value="functionsScannerClass"/>
</property>
</properties>
</rule>
<!-- Allow for JavaScript format examples to be provided in comments.
See: https://github.com/squizlabs/PHP_CodeSniffer/wiki/Customisable-Sniff-Properties#squizphpcommentedoutcode -->
<rule ref="Squiz.PHP.CommentedOutCode">
<properties>
<property name="maxPercentage" value="45"/>
</properties>
</rule>
<!-- Whitelist method names for classes that extend a PSR-2 package. -->
<rule ref="WordPress.DateTime.RestrictedFunctions.date_date">
<exclude-pattern>*/src/MakePotCommand\.php$</exclude-pattern>
</rule>
</ruleset>
+72
View File
@@ -0,0 +1,72 @@
<?php
namespace WP_CLI\I18n;
use Gettext\Extractors\Extractor;
use Gettext\Extractors\ExtractorInterface;
use Gettext\Translations;
use WP_CLI;
final class BlockExtractor extends Extractor implements ExtractorInterface {
use IterableCodeExtractor;
/**
* @inheritdoc
*/
public static function fromString( $string, Translations $translations, array $options = [] ) {
$file = $options['file'];
WP_CLI::debug( "Parsing file {$file}", 'make-pot' );
$file_data = json_decode( $string, true );
if ( null === $file_data ) {
WP_CLI::debug(
sprintf(
'Could not parse file %1$s: error code %2$s',
$file,
json_last_error()
),
'make-pot'
);
return;
}
$domain = isset( $file_data['textdomain'] ) ? $file_data['textdomain'] : null;
// Allow missing domain, but skip if they don't match.
if ( null !== $domain && $domain !== $translations->getDomain() ) {
return;
}
foreach ( $file_data as $key => $original ) {
switch ( $key ) {
case 'title':
case 'description':
$translation = $translations->insert( sprintf( 'block %s', $key ), $original );
$translation->addReference( $file );
break;
case 'keywords':
if ( ! is_array( $original ) ) {
continue 2;
}
foreach ( $original as $msg ) {
$translation = $translations->insert( 'block keyword', $msg );
$translation->addReference( $file );
}
break;
case 'styles':
if ( ! is_array( $original ) ) {
continue 2;
}
foreach ( $original as $msg ) {
$translation = $translations->insert( 'block style label', $msg['label'] );
$translation->addReference( $file );
}
}
}
}
}
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace WP_CLI\I18n;
use WP_CLI\Dispatcher\CommandNamespace as BaseCommandNamespace;
/**
* Provides internationalization tools for WordPress projects.
*
* ## EXAMPLES
*
* # Create a POT file for the WordPress plugin/theme in the current directory
* $ wp i18n make-pot . languages/my-plugin.pot
*
* @when before_wp_load
*/
class CommandNamespace extends BaseCommandNamespace {
}
+264
View File
@@ -0,0 +1,264 @@
<?php
namespace WP_CLI\I18n;
use Gettext\Translation;
use Gettext\Translations;
use RecursiveCallbackFilterIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use SplFileInfo;
use WP_CLI;
use WP_CLI\Utils;
trait IterableCodeExtractor {
private static $dir = '';
/**
* Extract the translations from a file.
*
* @param array|string $file_or_files A path of a file or files
* @param Translations $translations The translations instance to append the new translations.
* @param array $options {
* Optional. An array of options passed down to static::fromString()
*
* @type bool $wpExtractTemplates Extract 'Template Name' headers in theme files. Default 'false'.
* @type array $restrictFileNames Skip all files which are not included in this array.
* }
* @return null
*/
public static function fromFile( $file_or_files, Translations $translations, array $options = [] ) {
foreach ( static::getFiles( $file_or_files ) as $file ) {
if ( ! empty( $options['restrictFileNames'] ) ) {
$basename = Utils\basename( $file );
if ( ! in_array( $basename, $options['restrictFileNames'], true ) ) {
continue;
}
}
// Make sure a relative file path is added as a comment.
$options['file'] = ltrim( str_replace( static::$dir, '', Utils\normalize_path( $file ) ), '/' );
$string = file_get_contents( $file );
if ( ! $string ) {
WP_CLI::debug(
sprintf(
'Could not load file %1s',
$file
),
'make-pot'
);
continue;
}
if ( ! empty( $options['wpExtractTemplates'] ) ) {
$headers = MakePotCommand::get_file_data_from_string( $string, [ 'Template Name' => 'Template Name' ] );
if ( ! empty( $headers['Template Name'] ) ) {
$translation = new Translation( '', $headers['Template Name'] );
$translation->addExtractedComment( 'Template Name of the theme' );
$translations[] = $translation;
}
}
static::fromString( $string, $translations, $options );
}
}
/**
* Extract the translations from a file.
*
* @param string $dir Root path to start the recursive traversal in.
* @param Translations $translations The translations instance to append the new translations.
* @param array $options {
* Optional. An array of options passed down to static::fromString()
*
* @type bool $wpExtractTemplates Extract 'Template Name' headers in theme files. Default 'false'.
* @type array $exclude A list of path to exclude. Default [].
* @type array $extensions A list of extensions to process. Default [].
* }
* @return null
*/
public static function fromDirectory( $dir, Translations $translations, array $options = [] ) {
$dir = Utils\normalize_path( $dir );
static::$dir = $dir;
$include = isset( $options['include'] ) ? $options['include'] : [];
$exclude = isset( $options['exclude'] ) ? $options['exclude'] : [];
$files = static::getFilesFromDirectory( $dir, $include, $exclude, $options['extensions'] );
if ( ! empty( $files ) ) {
static::fromFile( $files, $translations, $options );
}
static::$dir = '';
}
/**
* Determines whether a file is valid based on given matchers.
*
* @param SplFileInfo $file File or directory.
* @param array $matchers List of files and directories to match.
* @return int How strongly the file is matched.
*/
protected static function calculateMatchScore( SplFileInfo $file, array $matchers = [] ) {
if ( empty( $matchers ) ) {
return 0;
}
if ( in_array( $file->getBasename(), $matchers, true ) ) {
return 10;
}
// Check for more complex paths, e.g. /some/sub/folder.
$root_relative_path = str_replace( static::$dir, '', $file->getPathname() );
foreach ( $matchers as $path_or_file ) {
$pattern = preg_quote( str_replace( '*', '__wildcard__', $path_or_file ), '/' );
$pattern = '(^|/)' . str_replace( '__wildcard__', '(.+)', $pattern );
// Base score is the amount of nested directories, discounting wildcards.
$base_score = count(
array_filter(
explode( '/', $path_or_file ),
static function ( $component ) {
return '*' !== $component;
}
)
);
if ( 0 === $base_score ) {
// If the matcher is simply * it gets a score above the implicit score but below 1.
$base_score = 0.2;
}
// If the matcher contains no wildcards and matches the end of the path.
if (
false === strpos( $path_or_file, '*' ) &&
false !== mb_ereg( $pattern . '$', $root_relative_path )
) {
return $base_score * 10;
}
// If the matcher matches the end of the path or a full directory contained.
if ( false !== mb_ereg( $pattern . '(/|$)', $root_relative_path ) ) {
return $base_score;
}
}
return 0;
}
/**
* Determines whether or not a directory has children that may be matched.
*
* @param SplFileInfo $dir Directory.
* @param array $matchers List of files and directories to match.
* @return bool Whether or not there are any matchers for children of this directory.
*/
protected static function containsMatchingChildren( SplFileInfo $dir, array $matchers = [] ) {
if ( empty( $matchers ) ) {
return false;
}
/** @var string $root_relative_path */
$root_relative_path = str_replace( static::$dir, '', $dir->getPathname() );
foreach ( $matchers as $path_or_file ) {
// If the matcher contains no wildcards and the path matches the start of the matcher.
if (
'' !== $root_relative_path &&
false === strpos( $path_or_file, '*' ) &&
0 === strpos( $path_or_file . '/', $root_relative_path )
) {
return true;
}
$base = current( explode( '*', $path_or_file ) );
// If start of the path matches the start of the matcher until the first wildcard.
// Or the start of the matcher until the first wildcard matches the start of the path.
if (
( '' !== $root_relative_path && 0 === strpos( $base, $root_relative_path ) ) ||
( '' !== $base && 0 === strpos( $root_relative_path, $base ) )
) {
return true;
}
}
return false;
}
/**
* Recursively gets all PHP files within a directory.
*
* @param string $dir A path of a directory.
* @param array $include List of files and directories to include.
* @param array $exclude List of files and directories to skip.
* @param array $extensions List of filename extensions to process.
*
* @return array File list.
*/
public static function getFilesFromDirectory( $dir, array $include = [], array $exclude = [], $extensions = [] ) {
$filtered_files = [];
$files = new RecursiveIteratorIterator(
new RecursiveCallbackFilterIterator(
new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS | RecursiveDirectoryIterator::UNIX_PATHS ),
static function ( $file, $key, $iterator ) use ( $include, $exclude, $extensions ) {
/** @var RecursiveCallbackFilterIterator $iterator */
/** @var SplFileInfo $file */
// Normalize include and exclude paths.
$include = array_map( 'static::trim_leading_slash', $include );
$exclude = array_map( 'static::trim_leading_slash', $exclude );
// If no $include is passed everything gets the weakest possible matching score.
$inclusion_score = empty( $include ) ? 0.1 : static::calculateMatchScore( $file, $include );
$exclusion_score = static::calculateMatchScore( $file, $exclude );
// Always include directories that aren't excluded.
if ( 0 === $exclusion_score && $iterator->hasChildren() ) {
return true;
}
if ( ( 0 === $inclusion_score || $exclusion_score > $inclusion_score ) && $iterator->hasChildren() ) {
// Always include directories that may have matching children even if they are excluded.
return static::containsMatchingChildren( $file, $include );
}
return ( ( $inclusion_score >= $exclusion_score ) && $file->isFile() && in_array( $file->getExtension(), $extensions, true ) );
}
),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ( $files as $file ) {
/** @var SplFileInfo $file */
if ( ! $file->isFile() || ! in_array( $file->getExtension(), $extensions, true ) ) {
continue;
}
$filtered_files[] = Utils\normalize_path( $file->getPathname() );
}
sort( $filtered_files, SORT_NATURAL | SORT_FLAG_CASE );
return $filtered_files;
}
/**
* Trim leading slash from a path.
*
* @param string $path Path to trim.
* @return string Trimmed path.
*/
private static function trim_leading_slash( $path ) {
return ltrim( $path, '/' );
}
}
+82
View File
@@ -0,0 +1,82 @@
<?php
namespace WP_CLI\I18n;
use Gettext\Generators\Jed;
use Gettext\Translation;
use Gettext\Translations;
/**
* Jed file generator.
*
* Adds some more meta data to JED translation files than the default generator.
*/
class JedGenerator extends Jed {
/**
* {@parentDoc}.
*/
public static function toString( Translations $translations, array $options = [] ) {
$options += static::$options;
$domain = $translations->getDomain() ?: 'messages';
$messages = static::buildMessages( $translations );
$configuration = [
'' => [
'domain' => $domain,
'lang' => $translations->getLanguage() ?: 'en',
'plural-forms' => $translations->getHeader( 'Plural-Forms' ) ?: 'nplurals=2; plural=(n != 1);',
],
];
$data = [
'translation-revision-date' => $translations->getHeader( 'PO-Revision-Date' ),
'generator' => 'WP-CLI/' . WP_CLI_VERSION,
'source' => $options['source'],
'domain' => $domain,
'locale_data' => [
$domain => $configuration + $messages,
],
];
return json_encode( $data, $options['json'] );
}
/**
* Generates an array with all translations.
*
* @param Translations $translations
*
* @return array
*/
public static function buildMessages( Translations $translations ) {
$plural_forms = $translations->getPluralForms();
$number_of_plurals = is_array( $plural_forms ) ? ( $plural_forms[0] - 1 ) : null;
$messages = [];
$context_glue = chr( 4 );
foreach ( $translations as $translation ) {
/** @var Translation $translation */
if ( $translation->isDisabled() ) {
continue;
}
$key = $translation->getOriginal();
if ( $translation->hasContext() ) {
$key = $translation->getContext() . $context_glue . $key;
}
if ( $translation->hasPluralTranslations( true ) ) {
$message = $translation->getPluralTranslations( $number_of_plurals );
array_unshift( $message, $translation->getTranslation() );
} else {
$message = [ $translation->getTranslation() ];
}
$messages[ $key ] = $message;
}
return $messages;
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
namespace WP_CLI\I18n;
use Exception;
use Gettext\Extractors\JsCode;
use Gettext\Translations;
use Peast\Syntax\Exception as PeastException;
use WP_CLI;
final class JsCodeExtractor extends JsCode {
use IterableCodeExtractor;
public static $options = [
'extractComments' => [ 'translators', 'Translators' ],
'constants' => [],
'functions' => [
'__' => 'text_domain',
'_x' => 'text_context_domain',
'_n' => 'single_plural_number_domain',
'_nx' => 'single_plural_number_context_domain',
],
];
protected static $functionsScannerClass = 'WP_CLI\I18n\JsFunctionsScanner';
/**
* @inheritdoc
*/
public static function fromString( $string, Translations $translations, array $options = [] ) {
WP_CLI::debug( "Parsing file {$options['file']}", 'make-pot' );
try {
static::fromStringMultiple( $string, [ $translations ], $options );
} catch ( PeastException $exception ) {
WP_CLI::debug(
sprintf(
'Could not parse file %1$s: %2$s (line %3$d, column %4$d)',
$options['file'],
$exception->getMessage(),
$exception->getPosition()->getLine(),
$exception->getPosition()->getColumn()
),
'make-pot'
);
} catch ( Exception $exception ) {
WP_CLI::debug(
sprintf(
'Could not parse file %1$s: %2$s',
$options['file'],
$exception->getMessage()
),
'make-pot'
);
}
}
/**
* @inheritDoc
*/
public static function fromStringMultiple( $string, array $translations, array $options = [] ) {
$options += static::$options;
/** @var JsFunctionsScanner $functions */
$functions = new static::$functionsScannerClass( $string );
$functions->enableCommentsExtraction( $options['extractComments'] );
$functions->saveGettextFunctions( $translations, $options );
}
}
+279
View File
@@ -0,0 +1,279 @@
<?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;
/**
* 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];
}
$code = $this->code;
// See https://github.com/mck89/peast/issues/7
// Temporary workaround to fix dynamic imports. The τ is a greek letter.
// This will trick the parser into thinking that it is a normal method call.
$code = preg_replace( '/import(\\s*\\()/', 'imporτ$1', $code );
$peast_options = [
'sourceType' => Peast::SOURCE_TYPE_MODULE,
'comments' => false !== $this->extract_comments,
'jsx' => true,
];
$ast = Peast::latest( $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'];
/** @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() ) {
$args[] = ''; // The value doesn't matter as it's unused.
}
if ( 'Literal' === $argument->getType() ) {
/** @var Node\Literal $argument */
$args[] = $argument->getValue();
}
}
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;
}
$translation = $translations->insert( $context, $original, $plural );
$translation->addReference( $file, $node->getLocation()->getStart()->getLine() );
/** @var Node\Comment $comment */
foreach ( $all_comments as $comment ) {
// Comments should be before the translation.
if ( ! $this->commentPrecedesNode( $comment, $node ) ) {
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() );
}
}
if ( isset( $parsed_comment ) ) {
$all_comments = [];
}
}
);
$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(),
];
}
}
// 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;
}
}
+254
View File
@@ -0,0 +1,254 @@
<?php
namespace WP_CLI\I18n;
use Gettext\Extractors\Po as PoExtractor;
use Gettext\Generators\Po as PoGenerator;
use Gettext\Translation;
use Gettext\Translations;
use WP_CLI;
use WP_CLI_Command;
use WP_CLI\Utils;
use DirectoryIterator;
use IteratorIterator;
use SplFileInfo;
class MakeJsonCommand extends WP_CLI_Command {
/**
* Options passed to json_encode().
*
* @var int JSON options.
*/
protected $json_options = 0;
/**
* Extract JavaScript strings from PO files and add them to individual JSON files.
*
* For JavaScript internationalization purposes, WordPress requires translations to be split up into
* one Jed-formatted JSON file per JavaScript source file.
*
* See https://make.wordpress.org/core/2018/11/09/new-javascript-i18n-support-in-wordpress/ to learn more
* about WordPress JavaScript internationalization.
*
* ## OPTIONS
*
* <source>
* : Path to an existing PO file or a directory containing multiple PO files.
*
* [<destination>]
* : Path to the destination directory for the resulting JSON files. Defaults to the source directory.
*
* [--purge]
* : Whether to purge the strings that were extracted from the original source file. Defaults to true, use `--no-purge` to skip the removal.
*
* [--pretty-print]
* : Pretty-print resulting JSON files.
*
* ## EXAMPLES
*
* # Create JSON files for all PO files in the languages directory
* $ wp i18n make-json languages
*
* # Create JSON files for my-plugin-de_DE.po and leave the PO file untouched.
* $ wp i18n make-json my-plugin-de_DE.po /tmp --no-purge
*
* @when before_wp_load
*
* @throws WP_CLI\ExitException
*/
public function __invoke( $args, $assoc_args ) {
$purge = Utils\get_flag_value( $assoc_args, 'purge', true );
if ( Utils\get_flag_value( $assoc_args, 'pretty-print', false ) ) {
$this->json_options |= JSON_PRETTY_PRINT;
}
$source = realpath( $args[0] );
if ( ! $source || ( ! is_file( $source ) && ! is_dir( $source ) ) ) {
WP_CLI::error( 'Source file or directory does not exist!' );
}
$destination = is_file( $source ) ? dirname( $source ) : $source;
if ( isset( $args[1] ) ) {
$destination = $args[1];
}
// Two is_dir() checks in case of a race condition.
if ( ! is_dir( $destination )
&& ! mkdir( $destination, 0777, true )
&& ! is_dir( $destination )
) {
WP_CLI::error( 'Could not create destination directory!' );
}
$result_count = 0;
if ( is_file( $source ) ) {
$files = [ new SplFileInfo( $source ) ];
} else {
$files = new IteratorIterator( new DirectoryIterator( $source ) );
}
/** @var DirectoryIterator $file */
foreach ( $files as $file ) {
if ( $file->isFile() && $file->isReadable() && 'po' === $file->getExtension() ) {
$result = $this->make_json( $file->getRealPath(), $destination );
$result_count += count( $result );
if ( $purge ) {
$removed = $this->remove_js_strings_from_po_file( $file->getRealPath() );
if ( ! $removed ) {
WP_CLI::warning( sprintf( 'Could not update file %s', basename( $source ) ) );
}
}
}
}
WP_CLI::success( sprintf( 'Created %d %s.', $result_count, Utils\pluralize( 'file', $result_count ) ) );
}
/**
* Splits a single PO file into multiple JSON files.
*
* @param string $source_file Path to the source file.
* @param string $destination Path to the destination directory.
* @return array List of created JSON files.
*/
protected function make_json( $source_file, $destination ) {
/** @var Translations[] $mapping */
$mapping = [];
$translations = new Translations();
$result = [];
PoExtractor::fromFile( $source_file, $translations );
$base_file_name = basename( $source_file, '.po' );
foreach ( $translations as $index => $translation ) {
/** @var Translation $translation */
// Find all unique sources this translation originates from.
$sources = array_map(
function ( $reference ) {
$file = $reference[0];
if ( substr( $file, - 7 ) === '.min.js' ) {
return substr( $file, 0, - 7 ) . '.js';
}
if ( substr( $file, - 3 ) === '.js' ) {
return $file;
}
return null;
},
$translation->getReferences()
);
$sources = array_unique( array_filter( $sources ) );
foreach ( $sources as $source ) {
if ( ! isset( $mapping[ $source ] ) ) {
$mapping[ $source ] = new Translations();
// phpcs:ignore Squiz.PHP.CommentedOutCode.Found -- Provide code that is meant to be used once the bug is fixed.
// See https://core.trac.wordpress.org/ticket/45441
// $mapping[ $source ]->setDomain( $translations->getDomain() );
$mapping[ $source ]->setHeader( 'Language', $translations->getLanguage() );
$mapping[ $source ]->setHeader( 'PO-Revision-Date', $translations->getHeader( 'PO-Revision-Date' ) );
$plural_forms = $translations->getPluralForms();
if ( $plural_forms ) {
list( $count, $rule ) = $plural_forms;
$mapping[ $source ]->setPluralForms( $count, $rule );
}
}
$mapping[ $source ][] = $translation;
}
}
$result += $this->build_json_files( $mapping, $base_file_name, $destination );
return $result;
}
/**
* Builds a mapping of JS file names to translation entries.
*
* Exports translations for each JS file to a separate translation file.
*
* @param array $mapping A mapping of files to translation entries.
* @param string $base_file_name Base file name for JSON files.
* @param string $destination Path to the destination directory.
*
* @return array List of created JSON files.
*/
protected function build_json_files( $mapping, $base_file_name, $destination ) {
$result = [];
foreach ( $mapping as $file => $translations ) {
/** @var Translations $translations */
$hash = md5( $file );
$destination_file = "${destination}/{$base_file_name}-{$hash}.json";
$success = JedGenerator::toFile(
$translations,
$destination_file,
[
'json' => $this->json_options,
'source' => $file,
]
);
if ( ! $success ) {
WP_CLI::warning( sprintf( 'Could not create file %s', basename( $destination_file, '.json' ) ) );
continue;
}
$result[] = $destination_file;
}
return $result;
}
/**
* Removes strings from PO file that only occur in JavaScript file.
*
* @param string $source_file Path to the PO file.
* @return bool True on success, false otherwise.
*/
protected function remove_js_strings_from_po_file( $source_file ) {
/** @var Translations[] $mapping */
$translations = new Translations();
PoExtractor::fromFile( $source_file, $translations );
foreach ( $translations->getArrayCopy() as $translation ) {
/** @var Translation $translation */
if ( ! $translation->hasReferences() ) {
continue;
}
foreach ( $translation->getReferences() as $reference ) {
$file = $reference[0];
if ( substr( $file, - 3 ) !== '.js' ) {
continue 2;
}
}
unset( $translations[ $translation->getId() ] );
}
return PoGenerator::toFile( $translations, $source_file );
}
}
+80
View File
@@ -0,0 +1,80 @@
<?php
namespace WP_CLI\I18n;
use DirectoryIterator;
use Gettext\Translations;
use IteratorIterator;
use SplFileInfo;
use WP_CLI;
use WP_CLI\Utils;
use WP_CLI_Command;
class MakeMoCommand extends WP_CLI_Command {
/**
* Create MO files from PO files.
*
* ## OPTIONS
*
* <source>
* : Path to an existing PO file or a directory containing multiple PO files.
*
* [<destination>]
* : Path to the destination directory for the resulting MO files. Defaults to the source directory.
*
* @when before_wp_load
*
* @throws WP_CLI\ExitException
*/
public function __invoke( $args, $assoc_args ) {
$source = realpath( $args[0] );
if ( ! $source || ( ! is_file( $source ) && ! is_dir( $source ) ) ) {
WP_CLI::error( 'Source file or directory does not exist!' );
}
$destination = is_file( $source ) ? dirname( $source ) : $source;
if ( isset( $args[1] ) ) {
$destination = $args[1];
}
// Two is_dir() checks in case of a race condition.
if ( ! is_dir( $destination )
&& ! mkdir( $destination, 0777, true )
&& ! is_dir( $destination )
) {
WP_CLI::error( 'Could not create destination directory!' );
}
if ( is_file( $source ) ) {
$files = [ new SplFileInfo( $source ) ];
} else {
$files = new IteratorIterator( new DirectoryIterator( $source ) );
}
$result_count = 0;
/** @var DirectoryIterator $file */
foreach ( $files as $file ) {
if ( 'po' !== $file->getExtension() ) {
continue;
}
if ( ! $file->isFile() || ! $file->isReadable() ) {
WP_CLI::warning( sprintf( 'Could not read file %s', $file->getFilename() ) );
continue;
}
$file_basename = basename( $file->getFilename(), '.po' );
$destination_file = "{$destination}/{$file_basename}.mo";
$translations = Translations::fromPoFile( $file->getPathname() );
if ( ! $translations->toMoFile( $destination_file ) ) {
WP_CLI::warning( sprintf( 'Could not create file %s', $destination_file ) );
continue;
}
$result_count++;
}
WP_CLI::success( sprintf( 'Created %d %s.', $result_count, Utils\pluralize( 'file', $result_count ) ) );
}
}
+937
View File
@@ -0,0 +1,937 @@
<?php
namespace WP_CLI\I18n;
use Gettext\Extractors\Po;
use Gettext\Merge;
use Gettext\Translation;
use Gettext\Translations;
use WP_CLI;
use WP_CLI_Command;
use WP_CLI\Utils;
use DirectoryIterator;
use IteratorIterator;
class MakePotCommand extends WP_CLI_Command {
/**
* @var string
*/
protected $source;
/**
* @var string
*/
protected $destination;
/**
* @var array
*/
protected $merge = [];
/**
* @var Translations
*/
protected $exceptions;
/**
* @var array
*/
protected $include = [];
/**
* @var array
*/
protected $exclude = [ 'node_modules', '.git', '.svn', '.CVS', '.hg', 'vendor', 'Gruntfile.js', 'webpack.config.js', '*.min.js' ];
/**
* @var string
*/
protected $slug;
/**
* @var array
*/
protected $main_file_data = [];
/**
* @var bool
*/
protected $skip_js = false;
/**
* @var bool
*/
protected $skip_php = false;
/**
* @var bool
*/
protected $skip_block_json = false;
/**
* @var bool
*/
protected $skip_audit = false;
/**
* @var array
*/
protected $headers = [];
/**
* @var string
*/
protected $domain;
/**
* @var string
*/
protected $package_name;
/**
* @var string
*/
protected $file_comment;
/**
* @var string
*/
protected $project_type = 'generic';
/**
* These Regexes copied from http://php.net/manual/en/function.sprintf.php#93552
* and adjusted for better precision and updated specs.
*/
const SPRINTF_PLACEHOLDER_REGEX = '/(?:
(?<!%) # Don\'t match a literal % (%%).
(
% # Start of placeholder.
(?:[0-9]+\$)? # Optional ordering of the placeholders.
[+-]? # Optional sign specifier.
(?:
(?:0|\'.)? # Optional padding specifier - excluding the space.
-? # Optional alignment specifier.
[0-9]* # Optional width specifier.
(?:\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character.
| # Only recognize the space as padding in combination with a width specifier.
(?:[ ])? # Optional space padding specifier.
-? # Optional alignment specifier.
[0-9]+ # Width specifier.
(?:\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character.
)
[bcdeEfFgGosuxX] # Type specifier.
)
)/x';
/**
* "Unordered" means there's no position specifier: '%s', not '%2$s'.
*/
const UNORDERED_SPRINTF_PLACEHOLDER_REGEX = '/(?:
(?<!%) # Don\'t match a literal % (%%).
% # Start of placeholder.
[+-]? # Optional sign specifier.
(?:
(?:0|\'.)? # Optional padding specifier - excluding the space.
-? # Optional alignment specifier.
[0-9]* # Optional width specifier.
(?:\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character.
| # Only recognize the space as padding in combination with a width specifier.
(?:[ ])? # Optional space padding specifier.
-? # Optional alignment specifier.
[0-9]+ # Width specifier.
(?:\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character.
)
[bcdeEfFgGosuxX] # Type specifier.
)/x';
/**
* Create a POT file for a WordPress project.
*
* Scans PHP and JavaScript files for translatable strings, as well as theme stylesheets and plugin files
* if the source directory is detected as either a plugin or theme.
*
* ## OPTIONS
*
* <source>
* : Directory to scan for string extraction.
*
* [<destination>]
* : Name of the resulting POT file.
*
* [--slug=<slug>]
* : Plugin or theme slug. Defaults to the source directory's basename.
*
* [--domain=<domain>]
* : Text domain to look for in the source code, unless the `--ignore-domain` option is used.
* By default, the "Text Domain" header of the plugin or theme is used.
* If none is provided, it falls back to the project slug.
*
* [--ignore-domain]
* : Ignore the text domain completely and extract strings with any text domain.
*
* [--merge[=<paths>]]
* : Comma-separated list of POT files whose contents should be merged with the extracted strings.
* If left empty, defaults to the destination POT file. POT file headers will be ignored.
*
* [--subtract=<paths>]
* : Comma-separated list of POT files whose contents should act as some sort of blacklist for string extraction.
* Any string which is found on that blacklist will not be extracted.
* This can be useful when you want to create multiple POT files from the same source directory with slightly
* different content and no duplicate strings between them.
*
* [--include=<paths>]
* : Comma-separated list of files and paths that should be used for string extraction.
* If provided, only these files and folders will be taken into account for string extraction.
* For example, `--include="src,my-file.php` will ignore anything besides `my-file.php` and files in the `src`
* directory. Simple glob patterns can be used, i.e. `--include=foo-*.php` includes any PHP file with the `foo-`
* prefix. Leading and trailing slashes are ignored, i.e. `/my/directory/` is the same as `my/directory`.
*
* [--exclude=<paths>]
* : Comma-separated list of files and paths that should be skipped for string extraction.
* For example, `--exclude=".github,myfile.php` would ignore any strings found within `myfile.php` or the `.github`
* folder. Simple glob patterns can be used, i.e. `--exclude=foo-*.php` excludes any PHP file with the `foo-`
* prefix. Leading and trailing slashes are ignored, i.e. `/my/directory/` is the same as `my/directory`. The
* following files and folders are always excluded: node_modules, .git, .svn, .CVS, .hg, vendor, *.min.js.
*
* [--headers=<headers>]
* : Array in JSON format of custom headers which will be added to the POT file. Defaults to empty array.
*
* [--skip-js]
* : Skips JavaScript string extraction. Useful when this is done in another build step, e.g. through Babel.
*
* [--skip-php]
* : Skips PHP string extraction.
*
* [--skip-block-json]
* : Skips string extraction from block.json files.
*
* [--skip-audit]
* : Skips string audit where it tries to find possible mistakes in translatable strings. Useful when running in an
* automated environment.
*
* [--file-comment=<file-comment>]
* : String that should be added as a comment to the top of the resulting POT file.
* By default, a copyright comment is added for WordPress plugins and themes in the following manner:
*
* ```
* Copyright (C) 2018 Example Plugin Author
* This file is distributed under the same license as the Example Plugin package.
* ```
*
* If a plugin or theme specifies a license in their main plugin file or stylesheet, the comment looks like
* this:
*
* ```
* Copyright (C) 2018 Example Plugin Author
* This file is distributed under the GPLv2.
* ```
*
* [--package-name=<name>]
* : Name to use for package name in the resulting POT file's `Project-Id-Version` header.
* Overrides plugin or theme name, if applicable.
*
* ## EXAMPLES
*
* # Create a POT file for the WordPress plugin/theme in the current directory
* $ wp i18n make-pot . languages/my-plugin.pot
*
* # Create a POT file for the continents and cities list in WordPress core.
* $ wp i18n make-pot . continents-and-cities.pot --include="wp-admin/includes/continents-cities.php"
* --ignore-domain
*
* @when before_wp_load
*
* @throws WP_CLI\ExitException
*/
public function __invoke( $args, $assoc_args ) {
$this->handle_arguments( $args, $assoc_args );
$translations = $this->extract_strings();
if ( ! $translations ) {
WP_CLI::warning( 'No strings found' );
}
$translations_count = count( $translations );
if ( 1 === $translations_count ) {
WP_CLI::debug( sprintf( 'Extracted %d string', $translations_count ), 'make-pot' );
} else {
WP_CLI::debug( sprintf( 'Extracted %d strings', $translations_count ), 'make-pot' );
}
if ( ! PotGenerator::toFile( $translations, $this->destination ) ) {
WP_CLI::error( 'Could not generate a POT file!' );
}
WP_CLI::success( 'POT file successfully generated!' );
}
/**
* Process arguments from command-line in a reusable way.
*
* @throws WP_CLI\ExitException
*
* @param array $args Command arguments.
* @param array $assoc_args Associative arguments.
*/
public function handle_arguments( $args, $assoc_args ) {
$array_arguments = array( 'headers' );
$assoc_args = Utils\parse_shell_arrays( $assoc_args, $array_arguments );
$this->source = realpath( $args[0] );
$this->slug = Utils\get_flag_value( $assoc_args, 'slug', Utils\basename( $this->source ) );
$this->skip_js = Utils\get_flag_value( $assoc_args, 'skip-js', $this->skip_js );
$this->skip_php = Utils\get_flag_value( $assoc_args, 'skip-php', $this->skip_php );
$this->skip_block_json = Utils\get_flag_value( $assoc_args, 'skip-block-json', $this->skip_block_json );
$this->skip_audit = Utils\get_flag_value( $assoc_args, 'skip-audit', $this->skip_audit );
$this->headers = Utils\get_flag_value( $assoc_args, 'headers', $this->headers );
$this->file_comment = Utils\get_flag_value( $assoc_args, 'file-comment' );
$this->package_name = Utils\get_flag_value( $assoc_args, 'package-name' );
$ignore_domain = Utils\get_flag_value( $assoc_args, 'ignore-domain', false );
if ( ! $this->source || ! is_dir( $this->source ) ) {
WP_CLI::error( 'Not a valid source directory!' );
}
$this->main_file_data = $this->get_main_file_data();
if ( $ignore_domain ) {
WP_CLI::debug( 'Extracting all strings regardless of text domain', 'make-pot' );
}
if ( ! $ignore_domain ) {
$this->domain = $this->slug;
if ( ! empty( $this->main_file_data['Text Domain'] ) ) {
$this->domain = $this->main_file_data['Text Domain'];
}
$this->domain = Utils\get_flag_value( $assoc_args, 'domain', $this->domain );
WP_CLI::debug( sprintf( 'Extracting all strings with text domain "%s"', $this->domain ), 'make-pot' );
}
// Determine destination.
$this->destination = "{$this->source}/{$this->slug}.pot";
if ( ! empty( $this->main_file_data['Domain Path'] ) ) {
// Domain Path inside source folder.
$this->destination = sprintf(
'%s/%s/%s.pot',
$this->source,
$this->unslashit( $this->main_file_data['Domain Path'] ),
$this->slug
);
}
if ( isset( $args[1] ) ) {
$this->destination = $args[1];
}
WP_CLI::debug( sprintf( 'Destination: %s', $this->destination ), 'make-pot' );
// Two is_dir() checks in case of a race condition.
if ( ! is_dir( dirname( $this->destination ) )
&& ! mkdir( dirname( $this->destination ), 0777, true )
&& ! is_dir( dirname( $this->destination ) )
) {
WP_CLI::error( 'Could not create destination directory!' );
}
if ( isset( $assoc_args['merge'] ) ) {
if ( true === $assoc_args['merge'] ) {
$this->merge = [ $this->destination ];
} elseif ( ! empty( $assoc_args['merge'] ) ) {
$this->merge = explode( ',', $assoc_args['merge'] );
}
$this->merge = array_filter(
$this->merge,
function ( $file ) {
if ( ! file_exists( $file ) ) {
WP_CLI::warning( sprintf( 'Invalid file provided to --merge: %s', $file ) );
return false;
}
return true;
}
);
if ( ! empty( $this->merge ) ) {
WP_CLI::debug(
sprintf(
'Merging with existing POT %s: %s',
WP_CLI\Utils\pluralize( 'file', count( $this->merge ) ),
implode( ',', $this->merge )
),
'make-pot'
);
}
}
$this->exceptions = new Translations();
if ( isset( $assoc_args['subtract'] ) ) {
$exceptions = explode( ',', $assoc_args['subtract'] );
$exceptions = array_filter(
$exceptions,
function ( $exception ) {
if ( ! file_exists( $exception ) ) {
WP_CLI::warning( sprintf( 'Invalid file provided to --subtract: %s', $exception ) );
return false;
}
$exception_translations = new Translations();
Po::fromFile( $exception, $exception_translations );
$this->exceptions->mergeWith( $exception_translations );
return true;
}
);
if ( ! empty( $exceptions ) ) {
WP_CLI::debug( sprintf( 'Ignoring any string already existing in: %s', implode( ',', $exceptions ) ), 'make-pot' );
}
}
if ( isset( $assoc_args['include'] ) ) {
$this->include = array_filter( explode( ',', $assoc_args['include'] ) );
$this->include = array_map( [ $this, 'unslashit' ], $this->include );
$this->include = array_unique( $this->include );
WP_CLI::debug( sprintf( 'Only including the following files: %s', implode( ',', $this->include ) ), 'make-pot' );
}
if ( isset( $assoc_args['exclude'] ) ) {
$this->exclude = array_filter( array_merge( $this->exclude, explode( ',', $assoc_args['exclude'] ) ) );
$this->exclude = array_map( [ $this, 'unslashit' ], $this->exclude );
$this->exclude = array_unique( $this->exclude );
}
WP_CLI::debug( sprintf( 'Excluding the following files: %s', implode( ',', $this->exclude ) ), 'make-pot' );
}
/**
* Removes leading and trailing slashes of a string.
*
* @param string $string What to add and remove slashes from.
* @return string String without leading and trailing slashes.
*/
protected function unslashit( $string ) {
return ltrim( rtrim( trim( $string ), '/\\' ), '/\\' );
}
/**
* Retrieves the main file data of the plugin or theme.
*
* @return array
*/
protected function get_main_file_data() {
$files = new IteratorIterator( new DirectoryIterator( $this->source ) );
/** @var DirectoryIterator $file */
foreach ( $files as $file ) {
// wp-content/themes/my-theme/style.css
if ( $file->isFile() && 'style' === $file->getBasename( '.css' ) && $file->isReadable() ) {
$theme_data = static::get_file_data( $file->getRealPath(), array_combine( $this->get_file_headers( 'theme' ), $this->get_file_headers( 'theme' ) ) );
// Stop when it contains a valid Theme Name header.
if ( ! empty( $theme_data['Theme Name'] ) ) {
WP_CLI::log( 'Theme stylesheet detected.' );
WP_CLI::debug( sprintf( 'Theme stylesheet: %s', $file->getRealPath() ), 'make-pot' );
$this->project_type = 'theme';
return $theme_data;
}
}
// wp-content/themes/my-themes/theme-a/style.css
if ( $file->isDir() && ! $file->isDot() && is_readable( $file->getRealPath() . '/style.css' ) ) {
$theme_data = static::get_file_data( $file->getRealPath() . '/style.css', array_combine( $this->get_file_headers( 'theme' ), $this->get_file_headers( 'theme' ) ) );
// Stop when it contains a valid Theme Name header.
if ( ! empty( $theme_data['Theme Name'] ) ) {
WP_CLI::log( 'Theme stylesheet detected.' );
WP_CLI::debug( sprintf( 'Theme stylesheet: %s', $file->getRealPath() . '/style.css' ), 'make-pot' );
$this->project_type = 'theme';
return $theme_data;
}
}
// wp-content/plugins/my-plugin/my-plugin.php
if ( $file->isFile() && $file->isReadable() && 'php' === $file->getExtension() ) {
$plugin_data = static::get_file_data( $file->getRealPath(), array_combine( $this->get_file_headers( 'plugin' ), $this->get_file_headers( 'plugin' ) ) );
// Stop when we find a file with a valid Plugin Name header.
if ( ! empty( $plugin_data['Plugin Name'] ) ) {
WP_CLI::log( 'Plugin file detected.' );
WP_CLI::debug( sprintf( 'Plugin file: %s', $file->getRealPath() ), 'make-pot' );
$this->project_type = 'plugin';
return $plugin_data;
}
}
}
WP_CLI::debug( 'No valid theme stylesheet or plugin file found, treating as a regular project.', 'make-pot' );
return [];
}
/**
* Returns the file headers for themes and plugins.
*
* @param string $type Source type, either theme or plugin.
*
* @return array List of file headers.
*/
protected function get_file_headers( $type ) {
switch ( $type ) {
case 'plugin':
return [
'Plugin Name',
'Plugin URI',
'Description',
'Author',
'Author URI',
'Version',
'License',
'Domain Path',
'Text Domain',
];
case 'theme':
return [
'Theme Name',
'Theme URI',
'Description',
'Author',
'Author URI',
'Version',
'License',
'Domain Path',
'Text Domain',
];
default:
return [];
}
}
/**
* Creates a POT file and stores it on disk.
*
* @throws WP_CLI\ExitException
*
* @return Translations A Translation set.
*/
protected function extract_strings() {
$translations = new Translations();
// Add existing strings first but don't keep headers.
if ( ! empty( $this->merge ) ) {
$existing_translations = new Translations();
Po::fromFile( $this->merge, $existing_translations );
$translations->mergeWith( $existing_translations, Merge::ADD | Merge::REMOVE );
}
PotGenerator::setCommentBeforeHeaders( $this->get_file_comment() );
$this->set_default_headers( $translations );
// POT files have no Language header.
$translations->deleteHeader( Translations::HEADER_LANGUAGE );
// Only relevant for PO files, not POT files.
$translations->setHeader( 'PO-Revision-Date', 'YEAR-MO-DA HO:MI+ZONE' );
if ( $this->domain ) {
$translations->setDomain( $this->domain );
}
unset( $this->main_file_data['Version'], $this->main_file_data['License'], $this->main_file_data['Domain Path'], $this->main_file_data['Text Domain'] );
// Set entries from main file data.
foreach ( $this->main_file_data as $header => $data ) {
if ( empty( $data ) ) {
continue;
}
$translation = new Translation( '', $data );
if ( isset( $this->main_file_data['Theme Name'] ) ) {
$translation->addExtractedComment( sprintf( '%s of the theme', $header ) );
} else {
$translation->addExtractedComment( sprintf( '%s of the plugin', $header ) );
}
$translations[] = $translation;
}
try {
if ( ! $this->skip_php ) {
$options = [
// Extract 'Template Name' headers in theme files.
'wpExtractTemplates' => isset( $this->main_file_data['Theme Name'] ),
'include' => $this->include,
'exclude' => $this->exclude,
'extensions' => [ 'php' ],
];
PhpCodeExtractor::fromDirectory( $this->source, $translations, $options );
}
if ( ! $this->skip_js ) {
JsCodeExtractor::fromDirectory(
$this->source,
$translations,
[
'include' => $this->include,
'exclude' => $this->exclude,
'extensions' => [ 'js', 'jsx' ],
]
);
MapCodeExtractor::fromDirectory(
$this->source,
$translations,
[
'include' => $this->include,
'exclude' => $this->exclude,
'extensions' => [ 'map' ],
]
);
}
if ( ! $this->skip_block_json ) {
BlockExtractor::fromDirectory(
$this->source,
$translations,
[
// Only look for block.json files, nothing else.
'restrictFileNames' => [ 'block.json' ],
'include' => $this->include,
'exclude' => $this->exclude,
'extensions' => [ 'json' ],
]
);
}
} catch ( \Exception $e ) {
WP_CLI::error( $e->getMessage() );
}
foreach ( $this->exceptions as $translation ) {
if ( $translations->find( $translation ) ) {
unset( $translations[ $translation->getId() ] );
}
}
if ( ! $this->skip_audit ) {
$this->audit_strings( $translations );
}
return $translations;
}
/**
* Audits strings.
*
* Goes through all extracted strings to find possible mistakes.
*
* @param Translations $translations Translations object.
*/
protected function audit_strings( $translations ) {
foreach ( $translations as $translation ) {
/** @var Translation $translation */
$references = $translation->getReferences();
// File headers don't have any file references.
$location = $translation->hasReferences() ? '(' . implode( ':', array_shift( $references ) ) . ')' : '';
// Check 1: Flag strings with placeholders that should have translator comments.
if (
! $translation->hasExtractedComments() &&
preg_match( self::SPRINTF_PLACEHOLDER_REGEX, $translation->getOriginal(), $placeholders ) >= 1
) {
$message = sprintf(
'The string "%1$s" contains placeholders but has no "translators:" comment to clarify their meaning. %2$s',
$translation->getOriginal(),
$location
);
WP_CLI::warning( $message );
}
// Check 2: Flag strings with different translator comments.
if ( $translation->hasExtractedComments() ) {
$comments = $translation->getExtractedComments();
// Remove plugin header information from comments.
$comments = array_filter(
$comments,
function ( $comment ) {
/** @var string $comment */
/** @var string $file_header */
foreach ( $this->get_file_headers( $this->project_type ) as $file_header ) {
if ( 0 === strpos( $comment, $file_header ) ) {
return null;
}
}
return $comment;
}
);
$comments_count = count( $comments );
if ( $comments_count > 1 ) {
$message = sprintf(
'The string "%1$s" has %2$d different translator comments. %3$s',
$translation->getOriginal(),
$comments_count,
$location
);
WP_CLI::warning( $message );
}
}
$non_placeholder_content = trim( preg_replace( '`^([\'"])(.*)\1$`Ds', '$2', $translation->getOriginal() ) );
$non_placeholder_content = preg_replace( self::SPRINTF_PLACEHOLDER_REGEX, '', $non_placeholder_content );
// Check 3: Flag empty strings without any translatable content.
if ( '' === $non_placeholder_content ) {
$message = sprintf(
'Found string without translatable content. %s',
$location
);
WP_CLI::warning( $message );
}
// Check 4: Flag strings with multiple unordered placeholders (%s %s %s vs. %1$s %2$s %3$s).
$unordered_matches_count = preg_match_all( self::UNORDERED_SPRINTF_PLACEHOLDER_REGEX, $translation->getOriginal(), $unordered_matches );
$unordered_matches = $unordered_matches[0];
if ( $unordered_matches_count >= 2 ) {
$message = sprintf(
'Multiple placeholders should be ordered. %s',
$location
);
WP_CLI::warning( $message );
}
if ( $translation->hasPlural() ) {
preg_match_all( self::SPRINTF_PLACEHOLDER_REGEX, $translation->getOriginal(), $single_placeholders );
$single_placeholders = $single_placeholders[0];
preg_match_all( self::SPRINTF_PLACEHOLDER_REGEX, $translation->getPlural(), $plural_placeholders );
$plural_placeholders = $plural_placeholders[0];
// see https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/#plurals
if ( count( $single_placeholders ) < count( $plural_placeholders ) ) {
// Check 5: Flag things like _n( 'One comment', '%s Comments' )
$message = sprintf(
'Missing singular placeholder, needed for some languages. See https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/#plurals %s',
$location
);
WP_CLI::warning( $message );
} else {
// Reordering is fine, but mismatched placeholders is probably wrong.
sort( $single_placeholders );
sort( $plural_placeholders );
// Check 6: Flag things like _n( '%s Comment (%d)', '%s Comments (%s)' )
if ( $single_placeholders !== $plural_placeholders ) {
$message = sprintf(
'Mismatched placeholders for singular and plural string. %s',
$location
);
WP_CLI::warning( $message );
}
}
}
}
}
/**
* Returns the copyright comment for the given package.
*
* @return string File comment.
*/
protected function get_file_comment() {
if ( '' === $this->file_comment ) {
return '';
}
if ( isset( $this->file_comment ) ) {
return implode( "\n", explode( '\n', $this->file_comment ) );
}
if ( isset( $this->main_file_data['Theme Name'] ) ) {
if ( isset( $this->main_file_data['License'] ) ) {
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']
);
}
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']
);
}
if ( isset( $this->main_file_data['Plugin Name'] ) ) {
if ( isset( $this->main_file_data['License'] ) && ! empty( $this->main_file_data['License'] ) ) {
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']
);
}
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']
);
}
return '';
}
/**
* Sets default POT file headers for the project.
*
* @param Translations $translations Translations object.
*/
protected function set_default_headers( $translations ) {
$name = null;
$version = $this->get_wp_version();
$bugs_address = null;
if ( ! $version && isset( $this->main_file_data['Version'] ) ) {
$version = $this->main_file_data['Version'];
}
if ( isset( $this->main_file_data['Theme Name'] ) ) {
$name = $this->main_file_data['Theme Name'];
$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'];
$bugs_address = sprintf( 'https://wordpress.org/support/plugin/%s', $this->slug );
}
if ( null !== $this->package_name ) {
$name = $this->package_name;
}
if ( null !== $name ) {
$translations->setHeader( 'Project-Id-Version', $name . ( $version ? ' ' . $version : '' ) );
}
if ( null !== $bugs_address ) {
$translations->setHeader( 'Report-Msgid-Bugs-To', $bugs_address );
}
$translations->setHeader( 'Last-Translator', 'FULL NAME <EMAIL@ADDRESS>' );
$translations->setHeader( 'Language-Team', 'LANGUAGE <LL@li.org>' );
$translations->setHeader( 'X-Generator', 'WP-CLI ' . WP_CLI_VERSION );
foreach ( $this->headers as $key => $value ) {
$translations->setHeader( $key, $value );
}
}
/**
* Extracts the WordPress version number from wp-includes/version.php.
*
* @return string|false Version number on success, false otherwise.
*/
protected function get_wp_version() {
$version_php = $this->source . '/wp-includes/version.php';
if ( ! file_exists( $version_php ) || ! is_readable( $version_php ) ) {
return false;
}
return preg_match( '/\$wp_version\s*=\s*\'(.*?)\';/', file_get_contents( $version_php ), $matches ) ? $matches[1] : false;
}
/**
* Retrieves metadata from a file.
*
* Searches for metadata in the first 8kiB of a file, such as a plugin or theme.
* Each piece of metadata must be on its own line. Fields can not span multiple
* lines, the value will get cut at the end of the first line.
*
* If the file data is not within that first 8kiB, then the author should correct
* their plugin file and move the data headers to the top.
*
* @see get_file_data()
*
* @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.
*/
protected static function get_file_data( $file, $headers ) {
// We don't need to write to the file, so just open for reading.
$fp = fopen( $file, 'rb' );
// Pull only the first 8kiB of the file in.
$file_data = fread( $fp, 8192 );
// PHP will close file handle, but we are good citizens.
fclose( $fp );
// Make sure we catch CR-only line endings.
$file_data = str_replace( "\r", "\n", $file_data );
return static::get_file_data_from_string( $file_data, $headers );
}
/**
* Retrieves metadata from a string.
*
* @param string $string String to look for metadata in.
* @param array $headers List of headers.
*
* @return array Array of file headers in `HeaderKey => Header Value` format.
*/
public static function get_file_data_from_string( $string, $headers ) {
foreach ( $headers as $field => $regex ) {
if ( preg_match( '/^[ \t\/*#@]*' . preg_quote( $regex, '/' ) . ':(.*)$/mi', $string, $match ) && $match[1] ) {
$headers[ $field ] = static::_cleanup_header_comment( $match[1] );
} else {
$headers[ $field ] = '';
}
}
return $headers;
}
/**
* Strip close comment and close php tags from file headers used by WP.
*
* @see _cleanup_header_comment()
*
* @param string $str Header comment to clean up.
*
* @return string
*/
protected static function _cleanup_header_comment( $str ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore -- Not changing because third-party commands might use/extend.
return trim( preg_replace( '/\s*(?:\*\/|\?>).*/', '', $str ) );
}
}
+63
View File
@@ -0,0 +1,63 @@
<?php
namespace WP_CLI\I18n;
use Gettext\Extractors\JsCode;
use Gettext\Translations;
use Peast\Syntax\Exception as PeastException;
use WP_CLI;
final class MapCodeExtractor extends JsCode {
use IterableCodeExtractor;
public static $options = [
'extractComments' => [ 'translators', 'Translators' ],
'constants' => [],
'functions' => [
'__' => 'text_domain',
'_x' => 'text_context_domain',
'_n' => 'single_plural_number_domain',
'_nx' => 'single_plural_number_context_domain',
],
];
/**
* {@inheritdoc}
*/
public static function fromString( $string, Translations $translations, array $options = [] ) {
if ( ! array_key_exists( 'file', $options ) || substr( $options['file'], -7 ) !== '.js.map' ) {
return;
}
$options['file'] = substr( $options['file'], 0, -7 ) . '.js';
try {
$options += static::$options;
$map_object = json_decode( $string );
if ( ! isset( $map_object->sourcesContent ) || ! is_array( $map_object->sourcesContent ) ) {
return;
}
$string = implode( "\n", $map_object->sourcesContent );
WP_CLI::debug( "Parsing file {$options['file']}", 'make-pot' );
$functions = new JsFunctionsScanner( $string );
$functions->enableCommentsExtraction( $options['extractComments'] );
$functions->saveGettextFunctions( $translations, $options );
} catch ( PeastException $e ) {
WP_CLI::debug(
sprintf(
'Could not parse file %1$s.map: %2$s (line %3$d, column %4$d in the concatenated sourcesContent)',
$options['file'],
$e->getMessage(),
$e->getPosition()->getLine(),
$e->getPosition()->getColumn()
),
'make-pot'
);
}
}
}
+67
View File
@@ -0,0 +1,67 @@
<?php
namespace WP_CLI\I18n;
use Exception;
use Gettext\Extractors\PhpCode;
use Gettext\Translations;
use WP_CLI;
final class PhpCodeExtractor extends PhpCode {
use IterableCodeExtractor;
public static $options = [
'extractComments' => [ 'translators', 'Translators' ],
'constants' => [],
'functions' => [
'__' => 'text_domain',
'esc_attr__' => 'text_domain',
'esc_html__' => 'text_domain',
'esc_xml__' => 'text_domain',
'_e' => 'text_domain',
'esc_attr_e' => 'text_domain',
'esc_html_e' => 'text_domain',
'esc_xml_e' => 'text_domain',
'_x' => 'text_context_domain',
'_ex' => 'text_context_domain',
'esc_attr_x' => 'text_context_domain',
'esc_html_x' => 'text_context_domain',
'esc_xml_x' => 'text_context_domain',
'_n' => 'single_plural_number_domain',
'_nx' => 'single_plural_number_context_domain',
'_n_noop' => 'single_plural_domain',
'_nx_noop' => 'single_plural_context_domain',
// Compat.
'_' => 'gettext', // Same as 'text_domain'.
// Deprecated.
'_c' => 'text_domain',
'_nc' => 'single_plural_number_domain',
'__ngettext' => 'single_plural_number_domain',
'__ngettext_noop' => 'single_plural_domain',
],
];
protected static $functionsScannerClass = 'WP_CLI\I18n\PhpFunctionsScanner';
/**
* {@inheritdoc}
*/
public static function fromString( $string, Translations $translations, array $options = [] ) {
WP_CLI::debug( "Parsing file {$options['file']}", 'make-pot' );
try {
static::fromStringMultiple( $string, [ $translations ], $options );
} catch ( Exception $exception ) {
WP_CLI::debug(
sprintf(
'Could not parse file %1$s: %2$s',
$options['file'],
$exception->getMessage()
),
'make-pot'
);
}
}
}
+83
View File
@@ -0,0 +1,83 @@
<?php
namespace WP_CLI\I18n;
use Gettext\Utils\PhpFunctionsScanner as GettextPhpFunctionsScanner;
class PhpFunctionsScanner extends GettextPhpFunctionsScanner {
/**
* {@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];
}
$functions = $options['functions'];
$file = $options['file'];
foreach ( $this->getFunctions( $options['constants'] ) as $function ) {
list( $name, $line, $args ) = $function;
if ( ! isset( $functions[ $name ] ) ) {
continue;
}
$original = null;
$domain = null;
$context = null;
$plural = null;
switch ( $functions[ $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;
case 'single_plural_domain':
list( $original, $plural, $domain ) = array_pad( $args, 3, null );
break;
case 'single_plural_context_domain':
list( $original, $plural, $context, $domain ) = array_pad( $args, 4, null );
break;
default:
// Should never happen.
\WP_CLI::error( sprintf( "Internal error: unknown function map '%s' for '%s'.", $functions[ $name ], $name ) );
}
if ( '' === (string) $original ) {
continue;
}
if ( $domain !== $translations->getDomain() && null !== $translations->getDomain() ) {
continue;
}
$translation = $translations->insert( $context, $original, $plural );
$translation = $translation->addReference( $file, $line );
if ( isset( $function[3] ) ) {
foreach ( $function[3] as $extracted_comment ) {
$translation = $translation->addExtractedComment( $extracted_comment );
}
}
}
}
}
+139
View File
@@ -0,0 +1,139 @@
<?php
namespace WP_CLI\I18n;
use Gettext\Generators\Po as PoGenerator;
use Gettext\Translations;
/**
* POT file generator.
*
* The only difference to the existing PO file generator is that this
* adds some comments at the very beginning of the file.
*/
class PotGenerator extends PoGenerator {
protected static $comments_before_headers = [];
/**
* Text to include as a comment before the start of the PO contents
*
* Doesn't need to include # in the beginning of lines, these are added automatically.
*
* @param string $comment File comment.
*/
public static function setCommentBeforeHeaders( $comment ) {
$comments = explode( "\n", $comment );
foreach ( $comments as $line ) {
if ( '' !== trim( $line ) ) {
static::$comments_before_headers[] = '# ' . $line;
}
}
}
/**
* {@parentDoc}.
*/
public static function toString( Translations $translations, array $options = [] ) {
$lines = static::$comments_before_headers;
$lines[] = 'msgid ""';
$lines[] = 'msgstr ""';
$plural_form = $translations->getPluralForms();
$plural_size = is_array( $plural_form ) ? ( $plural_form[0] - 1 ) : 1;
foreach ( $translations->getHeaders() as $name => $value ) {
$lines[] = sprintf( '"%s: %s\\n"', $name, $value );
}
$lines[] = '';
foreach ( $translations as $translation ) {
/** @var \Gettext\Translation $translation */
if ( $translation->hasComments() ) {
foreach ( $translation->getComments() as $comment ) {
$lines[] = '# ' . $comment;
}
}
if ( $translation->hasExtractedComments() ) {
foreach ( $translation->getExtractedComments() as $comment ) {
$lines[] = '#. ' . $comment;
}
}
foreach ( $translation->getReferences() as $reference ) {
$lines[] = '#: ' . $reference[0] . ( null !== $reference[1] ? ':' . $reference[1] : '' );
}
if ( $translation->hasFlags() ) {
$lines[] = '#, ' . implode( ',', $translation->getFlags() );
}
$prefix = $translation->isDisabled() ? '#~ ' : '';
if ( $translation->hasContext() ) {
$lines[] = $prefix . 'msgctxt ' . self::convertString( $translation->getContext() );
}
self::addLines( $lines, $prefix . 'msgid', $translation->getOriginal() );
if ( $translation->hasPlural() ) {
self::addLines( $lines, $prefix . 'msgid_plural', $translation->getPlural() );
for ( $i = 0; $i <= $plural_size; $i ++ ) {
self::addLines( $lines, $prefix . 'msgstr[' . $i . ']', '' );
}
} else {
self::addLines( $lines, $prefix . 'msgstr', $translation->getTranslation() );
}
$lines[] = '';
}
return implode( "\n", $lines );
}
/**
* Escapes and adds double quotes to a string.
*
* @param string $string Multiline string.
*
* @return string[]
*/
protected static function multilineQuote( $string ) {
$lines = explode( "\n", $string );
$last = count( $lines ) - 1;
foreach ( $lines as $k => $line ) {
if ( $k === $last ) {
$lines[ $k ] = self::convertString( $line );
} else {
$lines[ $k ] = self::convertString( $line . "\n" );
}
}
return $lines;
}
/**
* Add one or more lines depending whether the string is multiline or not.
*
* @param array &$lines Array lines should be added to.
* @param string $name Name of the line, e.g. msgstr or msgid_plural.
* @param string $value The line to add.
*/
protected static function addLines( array &$lines, $name, $value ) {
$newlines = self::multilineQuote( $value );
if ( count( $newlines ) === 1 ) {
$lines[] = $name . ' ' . $newlines[0];
} else {
$lines[] = $name . ' ""';
foreach ( $newlines as $line ) {
$lines[] = $line;
}
}
}
}
@@ -0,0 +1,151 @@
<?php
namespace WP_CLI\I18n\Tests;
use PHPUnit_Framework_TestCase;
use WP_CLI\I18n\IterableCodeExtractor;
use WP_CLI\Utils;
class IterableCodeExtractorTest extends PHPUnit_Framework_TestCase {
/** @var string A path files are located */
private static $base;
public function setUp() {
/**
* PHP5.4 cannot set property with __DIR__ constant.
*/
self::$base = Utils\normalize_path( __DIR__ ) . '/data/';
$property = new \ReflectionProperty( 'WP_CLI\I18n\IterableCodeExtractor', 'dir' );
$property->setAccessible( true );
$property->setValue( null, self::$base );
$property->setAccessible( false );
}
public function test_can_include_files() {
$includes = [ 'foo-plugin', 'bar', 'baz/inc*.js' ];
$result = IterableCodeExtractor::getFilesFromDirectory( self::$base, $includes, [], [ 'php', 'js' ] );
$expected = static::$base . 'foo-plugin/foo-plugin.php';
$this->assertContains( $expected, $result );
$expected = static::$base . 'baz/includes/should_be_included.js';
$this->assertContains( $expected, $result );
$expected = 'hoge/should_NOT_be_included.js';
$this->assertNotContains( $expected, $result );
}
public function test_can_include_empty_array() {
$result = IterableCodeExtractor::getFilesFromDirectory( self::$base, [], [], [ 'php', 'js' ] );
$expected_1 = static::$base . 'foo-plugin/foo-plugin.php';
$expected_2 = static::$base . 'baz/includes/should_be_included.js';
$this->assertContains( $expected_1, $result );
$this->assertContains( $expected_2, $result );
}
public function test_can_include_wildcard() {
$result = IterableCodeExtractor::getFilesFromDirectory( self::$base, [ '*' ], [], [ 'php', 'js' ] );
$expected_1 = static::$base . 'foo-plugin/foo-plugin.php';
$expected_2 = static::$base . 'baz/includes/should_be_included.js';
$this->assertContains( $expected_1, $result );
$this->assertContains( $expected_2, $result );
}
public function test_can_include_subdirectories() {
$result = IterableCodeExtractor::getFilesFromDirectory( self::$base, [ 'foo/bar/*' ], [], [ 'php', 'js' ] );
$expected_1 = static::$base . 'foo/bar/foo/bar/foo/bar/deep_directory_also_included.php';
$expected_2 = static::$base . 'foo/bar/foofoo/included.js';
$this->assertContains( $expected_1, $result );
$this->assertContains( $expected_2, $result );
}
public function test_can_include_only_php() {
$result = IterableCodeExtractor::getFilesFromDirectory( self::$base, [ 'foo/bar/*' ], [], [ 'php' ] );
$expected_1 = static::$base . 'foo/bar/foo/bar/foo/bar/deep_directory_also_included.php';
$expected_2 = static::$base . 'foo/bar/foofoo/ignored.js';
$this->assertContains( $expected_1, $result );
$this->assertNotContains( $expected_2, $result );
}
public function test_can_exclude_override_wildcard() {
$result = IterableCodeExtractor::getFilesFromDirectory( self::$base, [ 'foo/bar/*' ], [ 'foo/bar/excluded/*' ], [ 'php' ] );
$expected_1 = static::$base . 'foo/bar/foo/bar/foo/bar/deep_directory_also_included.php';
$expected_2 = static::$base . 'foo/bar/excluded/excluded.js';
$this->assertContains( $expected_1, $result );
$this->assertNotContains( $expected_2, $result );
}
public function test_can_exclude_override_matching_directory() {
$result = IterableCodeExtractor::getFilesFromDirectory( self::$base, [ 'foo/bar/*' ], [ 'foo/bar/excluded/*' ], [ 'php' ] );
$expected_1 = static::$base . 'foo/bar/foo/bar/foo/bar/deep_directory_also_included.php';
$expected_2 = static::$base . 'foo/bar/excluded/excluded.js';
$this->assertContains( $expected_1, $result );
$this->assertNotContains( $expected_2, $result );
}
public function test_can_not_exclude_partially_directory() {
$result = IterableCodeExtractor::getFilesFromDirectory( self::$base, [ 'foo/bar/*' ], [ 'exc' ], [ 'js' ] );
$expected_1 = static::$base . 'foo/bar/foo/bar/foo/bar/deep_directory_also_included.php';
$expected_2 = static::$base . 'foo/bar/excluded/ignored.js';
$this->assertNotContains( $expected_1, $result );
$this->assertContains( $expected_2, $result );
}
public function test_can_exclude_by_wildcard() {
$result = IterableCodeExtractor::getFilesFromDirectory( self::$base, [], [ '*' ], [ 'php', 'js' ] );
$this->assertEmpty( $result );
}
public function test_can_exclude_files() {
$excludes = [ 'hoge' ];
$result = IterableCodeExtractor::getFilesFromDirectory( self::$base, [], $excludes, [ 'php', 'js' ] );
$expected = static::$base . 'hoge/should_NOT_be_included.js';
$this->assertNotContains( $expected, $result );
}
public function test_can_override_exclude_by_include() {
// Overrides include option
$includes = [ 'excluded/ignored.js' ];
$excludes = [ 'excluded/*.js' ];
$result = IterableCodeExtractor::getFilesFromDirectory( self::$base, $includes, $excludes, [ 'php', 'js' ] );
$expected = static::$base . 'foo/bar/excluded/ignored.js';
$this->assertContains( $expected, $result );
}
public function test_can_return_all_directory_files_sorted() {
$result = IterableCodeExtractor::getFilesFromDirectory( self::$base, [ '*' ], [], [ 'php', 'js' ] );
$expected = array(
static::$base . 'baz/includes/should_be_included.js',
static::$base . 'foo-plugin/foo-plugin.php',
static::$base . 'foo/bar/excluded/ignored.js',
static::$base . 'foo/bar/foo/bar/foo/bar/deep_directory_also_included.php',
static::$base . 'foo/bar/foofoo/included.js',
static::$base . 'hoge/should_NOT_be_included.js',
static::$base . 'vendor/vendor-file.php',
);
$this->assertEquals( $expected, $result );
}
public function test_can_include_file_in_excluded_folder() {
$includes = [ 'vendor/vendor-file.php' ];
$excludes = [ 'vendor' ];
$result = IterableCodeExtractor::getFilesFromDirectory( self::$base, $includes, $excludes, [ 'php', 'js' ] );
$expected = static::$base . 'vendor/vendor-file.php';
$this->assertContains( $expected, $result );
}
public function test_can_include_file_in_excluded_folder_with_leading_slash() {
$includes = [ '/vendor/vendor-file.php' ];
$excludes = [ 'vendor' ];
$result = IterableCodeExtractor::getFilesFromDirectory( self::$base, $includes, $excludes, [ 'php', 'js' ] );
$expected = static::$base . 'vendor/vendor-file.php';
$this->assertContains( $expected, $result );
}
public function test_can_include_file_in_excluded_folder_by_wildcard() {
$includes = [ 'vendor/**' ];
$excludes = [ 'vendor' ];
$result = IterableCodeExtractor::getFilesFromDirectory( self::$base, $includes, $excludes, [ 'php', 'js' ] );
$expected = static::$base . 'vendor/vendor-file.php';
$this->assertContains( $expected, $result );
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace WP_CLI\I18n\Tests;
use Gettext\Translation;
use WP_CLI\I18n\PotGenerator;
use Gettext\Translations;
use PHPUnit_Framework_TestCase;
class PotGeneratorTest extends PHPUnit_Framework_TestCase {
public function test_adds_correct_amount_of_plural_strings() {
$translations = new Translations();
$translation = new Translation( '', '%d cat', '%d cats' );
$translations[] = $translation;
$result = PotGenerator::toString( $translations );
$this->assertContains( 'msgid "%d cat"', $result );
$this->assertContains( 'msgid_plural "%d cats"', $result );
$this->assertContains( 'msgstr[0] ""', $result );
$this->assertContains( 'msgstr[1] ""', $result );
}
}
+2
View File
@@ -0,0 +1,2 @@
require:
- i18n-command.php
@@ -0,0 +1,2 @@
vendor
.DS_Store
+21
View File
@@ -0,0 +1,21 @@
The MIT License
Copyright (c) 2011 Vladimir Andersen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+40
View File
@@ -0,0 +1,40 @@
# `mustangostang/spyc` WP-CLI fork
This is a fork of the [`mustangostang/spyc`](https://github.com/mustangostang/spyc) package, under a different name to avoid potential security issues.
See [`wp-cli/wp-cli#4017`](https://github.com/wp-cli/wp-cli/issues/4017) for more information.
**_Original documentation follows below:_**
---
**Spyc** is a YAML loader/dumper written in pure PHP. Given a YAML document, Spyc will return an array that
you can use however you see fit. Given an array, Spyc will return a string which contains a YAML document
built from your data.
**YAML** is an amazingly human friendly and strikingly versatile data serialization language which can be used
for log files, config files, custom protocols, the works. For more information, see http://www.yaml.org.
Spyc supports YAML 1.0 specification.
## Using Spyc
Using Spyc is trivial:
```
<?php
require_once "spyc.php";
$Data = Spyc::YAMLLoad('spyc.yaml');
```
or (if you prefer functional syntax)
```
<?php
require_once "spyc.php";
$Data = spyc_load_file('spyc.yaml');
```
## Donations, anyone?
If you find Spyc useful, I'm accepting Bitcoin donations (who doesn't these days?) at 193bEkLP7zMrNLZm9UdUet4puGD5mQiLai
+29
View File
@@ -0,0 +1,29 @@
<?php
/**
* Spyc -- A Simple PHP YAML Class
* @version 0.6.2
* @author Vlad Andersen <vlad.andersen@gmail.com>
* @author Chris Wanstrath <chris@ozmm.org>
* @link https://github.com/mustangostang/spyc/
* @copyright Copyright 2005-2006 Chris Wanstrath, 2006-2011 Vlad Andersen
* @license http://www.opensource.org/licenses/mit-license.php MIT License
* @package Spyc
*/
if (!class_exists('Mustangostang\Spyc')) {
require_once dirname(__FILE__) . '/src/Spyc.php';
}
class_alias('Mustangostang\Spyc', 'Spyc');
require_once dirname(__FILE__) . '/includes/functions.php';
// Enable use of Spyc from command line
// The syntax is the following: php Spyc.php spyc.yaml
do {
if (PHP_SAPI != 'cli') break;
if (empty ($_SERVER['argc']) || $_SERVER['argc'] < 2) break;
if (empty ($_SERVER['PHP_SELF']) || FALSE === strpos ($_SERVER['PHP_SELF'], 'Spyc.php') ) break;
$file = $argv[1];
echo json_encode (spyc_load_file ($file));
} while (0);
+26
View File
@@ -0,0 +1,26 @@
{
"name": "wp-cli/mustangostang-spyc",
"description": "A simple YAML loader/dumper class for PHP (WP-CLI fork)",
"type": "library",
"homepage": "https://github.com/mustangostang/spyc/",
"authors" : [{
"name": "mustangostang",
"email": "vlad.andersen@gmail.com"
}],
"license": "MIT",
"require": {
"php": ">=5.3.1"
},
"autoload": {
"psr-4": { "Mustangostang\\": "src/" },
"files": [ "includes/functions.php" ]
},
"require-dev": {
"phpunit/phpunit": "4.3.*@dev"
},
"extra": {
"branch-alias": {
"dev-master": "0.5.x-dev"
}
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
#
# S P Y C
# a simple php yaml class
#
# Feel free to dump an array to YAML, and then to load that YAML back into an
# array. This is a good way to test the limitations of the parser and maybe
# learn some basic YAML.
#
include('../Spyc.php');
$array[] = 'Sequence item';
$array['The Key'] = 'Mapped value';
$array[] = array('A sequence','of a sequence');
$array[] = array('first' => 'A sequence','second' => 'of mapped values');
$array['Mapped'] = array('A sequence','which is mapped');
$array['A Note'] = 'What if your text is too long?';
$array['Another Note'] = 'If that is the case, the dumper will probably fold your text by using a block. Kinda like this.';
$array['The trick?'] = 'The trick is that we overrode the default indent, 2, to 4 and the default wordwrap, 40, to 60.';
$array['Old Dog'] = "And if you want\n to preserve line breaks, \ngo ahead!";
$array['key:withcolon'] = "Should support this to";
$yaml = Spyc::YAMLDump($array,4,60);
+21
View File
@@ -0,0 +1,21 @@
<?php
#
# S P Y C
# a simple php yaml class
#
# license: [MIT License, http://www.opensource.org/licenses/mit-license.php]
#
include('../Spyc.php');
$array = Spyc::YAMLLoad('../spyc.yaml');
echo '<pre><a href="spyc.yaml">spyc.yaml</a> loaded into PHP:<br/>';
print_r($array);
echo '</pre>';
echo '<pre>YAML Data dumped back:<br/>';
echo Spyc::YAMLDump($array);
echo '</pre>';
+44
View File
@@ -0,0 +1,44 @@
<?php
/**
* Spyc -- A Simple PHP YAML Class
* @version 0.6.2
* @author Vlad Andersen <vlad.andersen@gmail.com>
* @author Chris Wanstrath <chris@ozmm.org>
* @link https://github.com/mustangostang/spyc/
* @copyright Copyright 2005-2006 Chris Wanstrath, 2006-2011 Vlad Andersen
* @license http://www.opensource.org/licenses/mit-license.php MIT License
* @package Spyc
*/
if (!function_exists('spyc_load')) {
/**
* Parses YAML to array.
* @param string $string YAML string.
* @return array
*/
function spyc_load ($string) {
return Spyc::YAMLLoadString($string);
}
}
if (!function_exists('spyc_load_file')) {
/**
* Parses YAML to array.
* @param string $file Path to YAML file.
* @return array
*/
function spyc_load_file ($file) {
return Spyc::YAMLLoad($file);
}
}
if (!function_exists('spyc_dump')) {
/**
* Dumps array to YAML.
* @param array $data Array.
* @return string
*/
function spyc_dump ($data) {
return Spyc::YAMLDump($data, false, false, true);
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
php5to4 ("../spyc.php", 'spyc-latest.php4');
function php5to4 ($src, $dest) {
$code = file_get_contents ($src);
$code = preg_replace ('#(public|private|protected)\s+\$#i', 'var \$', $code);
$code = preg_replace ('#(public|private|protected)\s+static\s+\$#i', 'var \$', $code);
$code = preg_replace ('#(public|private|protected)\s+function#i', 'function', $code);
$code = preg_replace ('#(public|private|protected)\s+static\s+function#i', 'function', $code);
$code = preg_replace ('#throw new Exception\\(([^)]*)\\)#i', 'trigger_error($1,E_USER_ERROR)', $code);
$code = str_replace ('self::', '$this->', $code);
$f = fopen ($dest, 'w');
fwrite($f, $code);
fclose ($f);
print "Written to $dest.\n";
}
File diff suppressed because it is too large Load Diff
+162
View File
@@ -0,0 +1,162 @@
<?php
#
# S P Y C
# a simple php yaml class
# v0.3
#
# author: [chris wanstrath, chris@ozmm.org]
# websites: [http://www.yaml.org, http://spyc.sourceforge.net/]
# license: [MIT License, http://www.opensource.org/licenses/mit-license.php]
# copyright: (c) 2005-2006 Chris Wanstrath
#
# We're gonna load a file into memory and see if we get what we expect.
# If not, we're gonna complain.
#
# Pretty lo-fi. Let's see if we can't get some unit testing going in the next,
# I dunno, 20 months? Alright. Go team.
#
error_reporting(E_ALL);
include('spyc.php4');
$yaml = Spyc::YAMLLoad('../spyc.yaml');
// print_r ($yaml);
# Added in .2
if ($yaml[1040] != "Ooo, a numeric key!")
die('Key: 1040 failed');
# Test mappings / types
if ($yaml['String'] != "Anyone's name, really.")
die('Key: String failed');
if ($yaml['Int'] !== 13)
die('Key: Int failed');
if ($yaml['True'] !== true)
die('Key: True failed');
if ($yaml['False'] !== false)
die('Key: False failed');
if ($yaml['Zero'] !== 0)
die('Key: Zero failed');
if (isset($yaml['Null']))
die('Key: Null failed');
if ($yaml['Float'] !== 5.34)
die('Key: Float failed');
# Test sequences
if ($yaml[0] != "PHP Class")
die('Sequence 0 failed');
if ($yaml[1] != "Basic YAML Loader")
die('Sequence 1 failed');
if ($yaml[2] != "Very Basic YAML Dumper")
die('Sequence 2 failed');
# A sequence of a sequence
if ($yaml[3] != array("YAML is so easy to learn.",
"Your config files will never be the same."))
die('Sequence 3 failed');
# Sequence of mappings
if ($yaml[4] != array("cpu" => "1.5ghz", "ram" => "1 gig",
"os" => "os x 10.4.1"))
die('Sequence 4 failed');
# Mapped sequence
if ($yaml['domains'] != array("yaml.org", "php.net"))
die("Key: 'domains' failed");
# A sequence like this.
if ($yaml[5] != array("program" => "Adium", "platform" => "OS X",
"type" => "Chat Client"))
die('Sequence 5 failed');
# A folded block as a mapped value
if ($yaml['no time'] != "There isn't any time for your tricks!\nDo you understand?")
die("Key: 'no time' failed");
# A literal block as a mapped value
if ($yaml['some time'] != "There is nothing but time\nfor your tricks.")
die("Key: 'some time' failed");
# Crazy combinations
if ($yaml['databases'] != array( array("name" => "spartan", "notes" =>
array( "Needs to be backed up",
"Needs to be normalized" ),
"type" => "mysql" )))
die("Key: 'databases' failed");
# You can be a bit tricky
if ($yaml["if: you'd"] != "like")
die("Key: 'if: you\'d' failed");
# Inline sequences
if ($yaml[6] != array("One", "Two", "Three", "Four"))
die("Sequence 6 failed");
# Nested Inline Sequences
if ($yaml[7] != array("One", array("Two", "And", "Three"), "Four", "Five"))
die("Sequence 7 failed");
# Nested Nested Inline Sequences
if ($yaml[8] != array( "This", array("Is", "Getting", array("Ridiculous", "Guys")),
"Seriously", array("Show", "Mercy")))
die("Sequence 8 failed");
# Inline mappings
if ($yaml[9] != array("name" => "chris", "age" => "young", "brand" => "lucky strike"))
die("Sequence 9 failed");
# Nested inline mappings
if ($yaml[10] != array("name" => "mark", "age" => "older than chris",
"brand" => array("marlboro", "lucky strike")))
die("Sequence 10 failed");
# References -- they're shaky, but functional
if ($yaml['dynamic languages'] != array('Perl', 'Python', 'PHP', 'Ruby'))
die("Key: 'dynamic languages' failed");
if ($yaml['compiled languages'] != array('C/C++', 'Java'))
die("Key: 'compiled languages' failed");
if ($yaml['all languages'] != array(
array('Perl', 'Python', 'PHP', 'Ruby'),
array('C/C++', 'Java')
))
die("Key: 'all languages' failed");
# Added in .2.2: Escaped quotes
if ($yaml[11] != "you know, this shouldn't work. but it does.")
die("Sequence 11 failed.");
if ($yaml[12] != "that's my value.")
die("Sequence 12 failed.");
if ($yaml[13] != "again, that's my value.")
die("Sequence 13 failed.");
if ($yaml[14] != "here's to \"quotes\", boss.")
die("Sequence 14 failed.");
if ($yaml[15] != array( 'name' => "Foo, Bar's", 'age' => 20))
die("Sequence 15 failed.");
if ($yaml[16] != array( 0 => "a", 1 => array (0 => 1, 1 => 2), 2 => "b"))
die("Sequence 16 failed.");
if ($yaml['endloop'] != "Does this line in the end indeed make Spyc go to an infinite loop?")
die("[endloop] failed.");
print "spyc.yaml parsed correctly\n";
?>
+219
View File
@@ -0,0 +1,219 @@
#
# S P Y C
# a simple php yaml class
#
# authors: [vlad andersen (vlad.andersen@gmail.com), chris wanstrath (chris@ozmm.org)]
# websites: [http://www.yaml.org, http://spyc.sourceforge.net/]
# license: [MIT License, http://www.opensource.org/licenses/mit-license.php]
# copyright: (c) 2005-2006 Chris Wanstrath, 2006-2014 Vlad Andersen
#
# spyc.yaml - A file containing the YAML that Spyc understands.
---
# Mappings - with proper types
String: Anyone's name, really.
Int: 13
BadHex: f0xf3
Hex: 0xf3
True: true
False: false
Zero: 0
Null: NULL
NotNull: 'null'
NotTrue: 'y'
NotBoolTrue: 'true'
NotInt: '5'
Float: 5.34
Negative: -90
SmallFloat: 0.7
NewLine: \n
QuotedNewLine: "\n"
# A sequence
- PHP Class
- Basic YAML Loader
- Very Basic YAML Dumper
# A sequence of a sequence
-
- YAML is so easy to learn.
- Your config files will never be the same.
# Sequence of mappings
-
cpu: 1.5ghz
ram: 1 gig
os : os x 10.4.1
# Mapped sequence
domains:
- yaml.org
- php.net
# A sequence like this.
- program: Adium
platform: OS X
type: Chat Client
# A folded block as a mapped value
no time: >
There isn't any time
for your tricks!
Do you understand?
# A literal block as a mapped value
some time: |
There is nothing but time
for your tricks.
# Crazy combinations
databases:
- name: spartan
notes:
- Needs to be backed up
- Needs to be normalized
type: mysql
# You can be a bit tricky
"if: you'd": like
# Inline sequences
- [One, Two, Three, Four]
# Nested Inline Sequences
- [One, [Two, And, Three], Four, Five]
# Nested Nested Inline Sequences
- [This, [Is, Getting, [Ridiculous, Guys]], Seriously, [Show, Mercy]]
# Inline mappings
- {name: chris, age: young, brand: lucky strike}
# Nested inline mappings
- {name: mark, age: older than chris, brand: [marlboro, lucky strike]}
# References -- they're shaky, but functional
dynamic languages: &DLANGS
- Perl
- Python
- PHP
- Ruby
compiled languages: &CLANGS
- C/C++
- Java
all languages:
- *DLANGS
- *CLANGS
# Added in .2.2: Escaped quotes
- you know, this shouldn't work. but it does.
- 'that''s my value.'
- 'again, that\'s my value.'
- "here's to \"quotes\", boss."
# added in .2.3
- {name: "Foo, Bar's", age: 20}
# Added in .2.4: bug [ 1418193 ] Quote Values in Nested Arrays
- [a, ['1', "2"], b]
# Add in .5.2: Quoted new line values.
- "First line\nSecond line\nThird line"
# Added in .2.4: malformed YAML
all
javascripts: [dom1.js, dom.js]
# Added in .2
1040: Ooo, a numeric key! # And working comments? Wow! Colons in comments: a menace (0.3).
hash_1: Hash #and a comment
hash_2: "Hash #and a comment"
"hash#3": "Hash (#) can appear in key too"
float_test: 1.0
float_test_with_quotes: '1.0'
float_inverse_test: 001
a_really_large_number: 115792089237316195423570985008687907853269984665640564039457584007913129639936 # 2^256
int array: [ 1, 2, 3 ]
array on several lines:
[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
10, 11, 12, 13, 14, 15, 16, 17, 18, 19 ]
morelesskey: "<value>"
array_of_zero: [0]
sophisticated_array_of_zero: {rx: {tx: [0]} }
switches:
- { row: 0, col: 0, func: {tx: [0, 1]} }
empty_sequence: [ ]
empty_hash: { }
special_characters: "[{]]{{]]"
asterisks: "*"
empty_key:
:
key: value
trailing_colon: "foo:"
multiline_items:
- type: SomeItem
values: [blah, blah, blah,
blah]
ints: [2, 54, 12,
2143]
many_lines: |
A quick
fox
jumped
over
a lazy
dog
werte:
1: nummer 1
0: Stunde 0
noindent_records:
- record1: value1
- record2: value2
"a:1": [1000]
"a:2":
- 2000
a:3: [3000]
complex_unquoted_key:
a:b:''test': value
array with commas:
["0","1"]
invoice: ["Something", "", '', "Something else"]
quotes: ['Something', "Nothing", 'Anything', "Thing"]
# [Endloop]
endloop: |
Does this line in the end indeed make Spyc go to an infinite loop?
File diff suppressed because it is too large Load Diff
+196
View File
@@ -0,0 +1,196 @@
<?php
require_once ("../Spyc.php");
class DumpTest extends PHPUnit_Framework_TestCase {
private $files_to_test = array();
public function setUp() {
$this->files_to_test = array ('../spyc.yaml', 'failing1.yaml', 'indent_1.yaml', 'quotes.yaml');
}
public function testShortSyntax() {
$dump = spyc_dump(array ('item1', 'item2', 'item3'));
$awaiting = "- item1\n- item2\n- item3\n";
$this->assertEquals ($awaiting, $dump);
}
public function testDump() {
foreach ($this->files_to_test as $file) {
$yaml = spyc_load(file_get_contents($file));
$dump = Spyc::YAMLDump ($yaml);
$yaml_after_dump = Spyc::YAMLLoad ($dump);
$this->assertEquals ($yaml, $yaml_after_dump);
}
}
public function testDumpWithQuotes() {
$Spyc = new Spyc();
$Spyc->setting_dump_force_quotes = true;
foreach ($this->files_to_test as $file) {
$yaml = $Spyc->load(file_get_contents($file));
$dump = $Spyc->dump ($yaml);
$yaml_after_dump = Spyc::YAMLLoad ($dump);
$this->assertEquals ($yaml, $yaml_after_dump);
}
}
public function testDumpArrays() {
$dump = Spyc::YAMLDump(array ('item1', 'item2', 'item3'));
$awaiting = "---\n- item1\n- item2\n- item3\n";
$this->assertEquals ($awaiting, $dump);
}
public function testNull() {
$dump = Spyc::YAMLDump(array('a' => 1, 'b' => null, 'c' => 3));
$awaiting = "---\na: 1\nb: null\nc: 3\n";
$this->assertEquals ($awaiting, $dump);
}
public function testNext() {
$array = array("aaa", "bbb", "ccc");
#set arrays internal pointer to next element
next($array);
$dump = Spyc::YAMLDump($array);
$awaiting = "---\n- aaa\n- bbb\n- ccc\n";
$this->assertEquals ($awaiting, $dump);
}
public function testDumpingMixedArrays() {
$array = array();
$array[] = 'Sequence item';
$array['The Key'] = 'Mapped value';
$array[] = array('A sequence','of a sequence');
$array[] = array('first' => 'A sequence','second' => 'of mapped values');
$array['Mapped'] = array('A sequence','which is mapped');
$array['A Note'] = 'What if your text is too long?';
$array['Another Note'] = 'If that is the case, the dumper will probably fold your text by using a block. Kinda like this.';
$array['The trick?'] = 'The trick is that we overrode the default indent, 2, to 4 and the default wordwrap, 40, to 60.';
$array['Old Dog'] = "And if you want\n to preserve line breaks, \ngo ahead!";
$array['key:withcolon'] = "Should support this to";
$yaml = Spyc::YAMLDump($array,4,60);
}
public function testMixed() {
$dump = Spyc::YAMLDump(array(0 => 1, 'b' => 2, 1 => 3));
$awaiting = "---\n0: 1\nb: 2\n1: 3\n";
$this->assertEquals ($awaiting, $dump);
}
public function testDumpNumerics() {
$dump = Spyc::YAMLDump(array ('404', '405', '500'));
$awaiting = "---\n- \"404\"\n- \"405\"\n- \"500\"\n";
$this->assertEquals ($awaiting, $dump);
}
public function testDumpAsterisks() {
$dump = Spyc::YAMLDump(array ('*'));
$awaiting = "---\n- '*'\n";
$this->assertEquals ($awaiting, $dump);
}
public function testDumpAmpersands() {
$dump = Spyc::YAMLDump(array ('some' => '&foo'));
$awaiting = "---\nsome: '&foo'\n";
$this->assertEquals ($awaiting, $dump);
}
public function testDumpExclamations() {
$dump = Spyc::YAMLDump(array ('some' => '!foo'));
$awaiting = "---\nsome: '!foo'\n";
$this->assertEquals ($awaiting, $dump);
}
public function testDumpExclamations2() {
$dump = Spyc::YAMLDump(array ('some' => 'foo!'));
$awaiting = "---\nsome: foo!\n";
$this->assertEquals ($awaiting, $dump);
}
public function testDumpApostrophes() {
$dump = Spyc::YAMLDump(array ('some' => "'Biz' pimpt bedrijventerreinen"));
$awaiting = "---\nsome: \"'Biz' pimpt bedrijventerreinen\"\n";
$this->assertEquals ($awaiting, $dump);
}
public function testDumpNumericHashes() {
$dump = Spyc::YAMLDump(array ("titel"=> array("0" => "", 1 => "Dr.", 5 => "Prof.", 6 => "Prof. Dr.")));
$awaiting = "---\ntitel:\n 0: \"\"\n 1: Dr.\n 5: Prof.\n 6: Prof. Dr.\n";
$this->assertEquals ($awaiting, $dump);
}
public function testEmpty() {
$dump = Spyc::YAMLDump(array("foo" => array()));
$awaiting = "---\nfoo: [ ]\n";
$this->assertEquals ($awaiting, $dump);
}
public function testHashesInKeys() {
$dump = Spyc::YAMLDump(array ('#color' => '#ffffff'));
$awaiting = "---\n\"#color\": '#ffffff'\n";
$this->assertEquals ($awaiting, $dump);
}
public function testParagraph() {
$dump = Spyc::YAMLDump(array ('key' => "|\n value"));
$awaiting = "---\nkey: |\n value\n";
$this->assertEquals ($awaiting, $dump);
}
public function testParagraphTwo() {
$dump = Spyc::YAMLDump(array ('key' => 'Congrats, pimpt bedrijventerreinen pimpt bedrijventerreinen pimpt bedrijventerreinen!'));
$awaiting = "---\nkey: >\n Congrats, pimpt bedrijventerreinen pimpt\n bedrijventerreinen pimpt\n bedrijventerreinen!\n";
$this->assertEquals ($awaiting, $dump);
}
public function testString() {
$dump = Spyc::YAMLDump(array ('key' => array('key_one' => 'Congrats, pimpt bedrijventerreinen!')));
$awaiting = "---\nkey:\n key_one: Congrats, pimpt bedrijventerreinen!\n";
$this->assertEquals ($awaiting, $dump);
}
public function testStringLong() {
$dump = Spyc::YAMLDump(array ('key' => array('key_one' => 'Congrats, pimpt bedrijventerreinen pimpt bedrijventerreinen pimpt bedrijventerreinen!')));
$awaiting = "---\nkey:\n key_one: >\n Congrats, pimpt bedrijventerreinen pimpt\n bedrijventerreinen pimpt\n bedrijventerreinen!\n";
$this->assertEquals ($awaiting, $dump);
}
public function testStringDoubleQuote() {
$dump = Spyc::YAMLDump(array ('key' => array('key_one' => array('key_two' => '"Système d\'e-réservation"'))));
$awaiting = "---\nkey:\n key_one:\n key_two: |\n Système d'e-réservation\n";
$this->assertEquals ($awaiting, $dump);
}
public function testLongStringDoubleQuote() {
$dump = Spyc::YAMLDump(array ('key' => array('key_one' => array('key_two' => '"Système d\'e-réservation bedrijventerreinen pimpt" bedrijventerreinen!'))));
$awaiting = "---\nkey:\n key_one:\n key_two: |\n \"Système d'e-réservation bedrijventerreinen pimpt\" bedrijventerreinen!\n";
$this->assertEquals ($awaiting, $dump);
}
public function testStringStartingWithSpace() {
$dump = Spyc::YAMLDump(array ('key' => array('key_one' => " Congrats, pimpt bedrijventerreinen \n pimpt bedrijventerreinen pimpt bedrijventerreinen!")));
$awaiting = "---\nkey:\n key_one: |\n Congrats, pimpt bedrijventerreinen\n pimpt bedrijventerreinen pimpt bedrijventerreinen!\n";
$this->assertEquals ($awaiting, $dump);
}
public function testPerCentOne() {
$dump = Spyc::YAMLDump(array ('key' => "%name%, pimpts bedrijventerreinen!"));
$awaiting = "---\nkey: '%name%, pimpts bedrijventerreinen!'\n";
$this->assertEquals ($awaiting, $dump);
}
public function testPerCentAndSimpleQuote() {
$dump = Spyc::YAMLDump(array ('key' => "%name%, pimpt's bedrijventerreinen!"));
$awaiting = "---\nkey: \"%name%, pimpt's bedrijventerreinen!\"\n";
$this->assertEquals ($awaiting, $dump);
}
public function testPerCentAndDoubleQuote() {
$dump = Spyc::YAMLDump(array ('key' => '%name%, pimpt\'s "bed"rijventerreinen!'));
$awaiting = "---\nkey: |\n %name%, pimpt's \"bed\"rijventerreinen!\n";
$this->assertEquals ($awaiting, $dump);
}
}
+70
View File
@@ -0,0 +1,70 @@
<?php
require_once ("../Spyc.php");
class IndentTest extends PHPUnit_Framework_TestCase {
protected $Y;
protected function setUp() {
$this->Y = Spyc::YAMLLoad("indent_1.yaml");
}
public function testIndent_1() {
$this->assertEquals (array ('child_1' => 2, 'child_2' => 0, 'child_3' => 1), $this->Y['root']);
}
public function testIndent_2() {
$this->assertEquals (array ('child_1' => 1, 'child_2' => 2), $this->Y['root2']);
}
public function testIndent_3() {
$this->assertEquals (array (array ('resolutions' => array (1024 => 768, 1920 => 1200), 'producer' => 'Nec')), $this->Y['display']);
}
public function testIndent_4() {
$this->assertEquals (array (
array ('resolutions' => array (1024 => 768)),
array ('resolutions' => array (1920 => 1200)),
), $this->Y['displays']);
}
public function testIndent_5() {
$this->assertEquals (array (array (
'row' => 0,
'col' => 0,
'headsets_affected' => array (
array (
'ports' => array (0),
'side' => 'left',
)
),
'switch_function' => array (
'ics_ptt' => true
)
)), $this->Y['nested_hashes_and_seqs']);
}
public function testIndent_6() {
$this->assertEquals (array (
'h' => array (
array ('a' => 'b', 'a1' => 'b1'),
array ('c' => 'd')
)
), $this->Y['easier_nest']);
}
public function testIndent_space() {
$this->assertEquals ("By four\n spaces", $this->Y['one_space']);
}
public function testListAndComment() {
$this->assertEquals (array ('one', 'two', 'three'), $this->Y['list_and_comment']);
}
public function testAnchorAndAlias() {
$this->assertEquals (array ('database' => 'rails_dev', 'adapter' => 'mysql', 'host' => 'localhost'), $this->Y['development']);
$this->assertEquals (array (1 => 'abc'), $this->Y['zzz']);
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
require_once ("../Spyc.php");
class LoadTest extends PHPUnit_Framework_TestCase {
public function testQuotes() {
$test_values = array(
"adjacent '''' \"\"\"\" quotes.",
"adjacent '''' quotes.",
"adjacent \"\"\"\" quotes.",
);
foreach($test_values as $value) {
$yaml = array($value);
$dump = Spyc::YAMLDump ($yaml);
$yaml_loaded = Spyc::YAMLLoad ($dump);
$this->assertEquals ($yaml, $yaml_loaded);
}
}
}
+401
View File
@@ -0,0 +1,401 @@
<?php
require_once ("../Spyc.php");
class ParseTest extends PHPUnit_Framework_TestCase {
protected $yaml;
protected function setUp() {
$this->yaml = spyc_load_file('../spyc.yaml');
}
public function testMergeHashKeys() {
$Expected = array (
array ('step' => array('instrument' => 'Lasik 2000', 'pulseEnergy' => 5.4, 'pulseDuration' => 12, 'repetition' => 1000, 'spotSize' => '1mm')),
array ('step' => array('instrument' => 'Lasik 2000', 'pulseEnergy' => 5.4, 'pulseDuration' => 12, 'repetition' => 1000, 'spotSize' => '2mm')),
);
$Actual = spyc_load_file ('indent_1.yaml');
$this->assertEquals ($Expected, $Actual['steps']);
}
public function testDeathMasks() {
$Expected = array ('sad' => 2, 'magnificent' => 4);
$Actual = spyc_load_file ('indent_1.yaml');
$this->assertEquals ($Expected, $Actual['death masks are']);
}
public function testDevDb() {
$Expected = array ('adapter' => 'mysql', 'host' => 'localhost', 'database' => 'rails_dev');
$Actual = spyc_load_file ('indent_1.yaml');
$this->assertEquals ($Expected, $Actual['development']);
}
public function testNumericKey() {
$this->assertEquals ("Ooo, a numeric key!", $this->yaml[1040]);
}
public function testMappingsString() {
$this->assertEquals ("Anyone's name, really.", $this->yaml['String']);
}
public function testMappingsInt() {
$this->assertSame (13, $this->yaml['Int']);
}
public function testMappingsHex() {
$this->assertSame (243, $this->yaml['Hex']);
$this->assertSame ('f0xf3', $this->yaml['BadHex']);
}
public function testMappingsBooleanTrue() {
$this->assertSame (true, $this->yaml['True']);
}
public function testMappingsBooleanFalse() {
$this->assertSame (false, $this->yaml['False']);
}
public function testMappingsZero() {
$this->assertSame (0, $this->yaml['Zero']);
}
public function testMappingsNull() {
$this->assertSame (null, $this->yaml['Null']);
}
public function testMappingsNotNull() {
$this->assertSame ('null', $this->yaml['NotNull']);
}
public function testMappingsFloat() {
$this->assertSame (5.34, $this->yaml['Float']);
}
public function testMappingsNegative() {
$this->assertSame (-90, $this->yaml['Negative']);
}
public function testMappingsSmallFloat() {
$this->assertSame (0.7, $this->yaml['SmallFloat']);
}
public function testNewline() {
$this->assertSame ('\n', $this->yaml['NewLine']);
}
public function testQuotedNewline() {
$this->assertSame ("\n", $this->yaml['QuotedNewLine']);
}
public function testSeq0() {
$this->assertEquals ("PHP Class", $this->yaml[0]);
}
public function testSeq1() {
$this->assertEquals ("Basic YAML Loader", $this->yaml[1]);
}
public function testSeq2() {
$this->assertEquals ("Very Basic YAML Dumper", $this->yaml[2]);
}
public function testSeq3() {
$this->assertEquals (array("YAML is so easy to learn.",
"Your config files will never be the same."), $this->yaml[3]);
}
public function testSeqMap() {
$this->assertEquals (array("cpu" => "1.5ghz", "ram" => "1 gig",
"os" => "os x 10.4.1"), $this->yaml[4]);
}
public function testMappedSequence() {
$this->assertEquals (array("yaml.org", "php.net"), $this->yaml['domains']);
}
public function testAnotherSequence() {
$this->assertEquals (array("program" => "Adium", "platform" => "OS X",
"type" => "Chat Client"), $this->yaml[5]);
}
public function testFoldedBlock() {
$this->assertEquals ("There isn't any time for your tricks!\nDo you understand?", $this->yaml['no time']);
}
public function testLiteralAsMapped() {
$this->assertEquals ("There is nothing but time\nfor your tricks.", $this->yaml['some time']);
}
public function testCrazy() {
$this->assertEquals (array( array("name" => "spartan", "notes" =>
array( "Needs to be backed up",
"Needs to be normalized" ),
"type" => "mysql" )), $this->yaml['databases']);
}
public function testColons() {
$this->assertEquals ("like", $this->yaml["if: you'd"]);
}
public function testInline() {
$this->assertEquals (array("One", "Two", "Three", "Four"), $this->yaml[6]);
}
public function testNestedInline() {
$this->assertEquals (array("One", array("Two", "And", "Three"), "Four", "Five"), $this->yaml[7]);
}
public function testNestedNestedInline() {
$this->assertEquals (array( "This", array("Is", "Getting", array("Ridiculous", "Guys")),
"Seriously", array("Show", "Mercy")), $this->yaml[8]);
}
public function testInlineMappings() {
$this->assertEquals (array("name" => "chris", "age" => "young", "brand" => "lucky strike"), $this->yaml[9]);
}
public function testNestedInlineMappings() {
$this->assertEquals (array("name" => "mark", "age" => "older than chris",
"brand" => array("marlboro", "lucky strike")), $this->yaml[10]);
}
public function testReferences() {
$this->assertEquals (array('Perl', 'Python', 'PHP', 'Ruby'), $this->yaml['dynamic languages']);
}
public function testReferences2() {
$this->assertEquals (array('C/C++', 'Java'), $this->yaml['compiled languages']);
}
public function testReferences3() {
$this->assertEquals (array(
array('Perl', 'Python', 'PHP', 'Ruby'),
array('C/C++', 'Java')
), $this->yaml['all languages']);
}
public function testEscapedQuotes() {
$this->assertEquals ("you know, this shouldn't work. but it does.", $this->yaml[11]);
}
public function testEscapedQuotes_2() {
$this->assertEquals ( "that's my value.", $this->yaml[12]);
}
public function testEscapedQuotes_3() {
$this->assertEquals ("again, that's my value.", $this->yaml[13]);
}
public function testQuotes() {
$this->assertEquals ("here's to \"quotes\", boss.", $this->yaml[14]);
}
public function testQuoteSequence() {
$this->assertEquals ( array( 'name' => "Foo, Bar's", 'age' => 20), $this->yaml[15]);
}
public function testShortSequence() {
$this->assertEquals (array( 0 => "a", 1 => array (0 => 1, 1 => 2), 2 => "b"), $this->yaml[16]);
}
public function testQuotedNewlines() {
$this->assertEquals ("First line\nSecond line\nThird line", $this->yaml[17]);
}
public function testHash_1() {
$this->assertEquals ("Hash", $this->yaml['hash_1']);
}
public function testHash_2() {
$this->assertEquals ('Hash #and a comment', $this->yaml['hash_2']);
}
public function testHash_3() {
$this->assertEquals ('Hash (#) can appear in key too', $this->yaml['hash#3']);
}
public function testEndloop() {
$this->assertEquals ("Does this line in the end indeed make Spyc go to an infinite loop?", $this->yaml['endloop']);
}
public function testReallyLargeNumber() {
$this->assertEquals ('115792089237316195423570985008687907853269984665640564039457584007913129639936', $this->yaml['a_really_large_number']);
}
public function testFloatWithZeros() {
$this->assertSame ('1.0', $this->yaml['float_test']);
}
public function testFloatWithQuotes() {
$this->assertSame ('1.0', $this->yaml['float_test_with_quotes']);
}
public function testFloatInverse() {
$this->assertEquals ('001', $this->yaml['float_inverse_test']);
}
public function testIntArray() {
$this->assertEquals (array (1, 2, 3), $this->yaml['int array']);
}
public function testArrayOnSeveralLines() {
$this->assertEquals (array (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19), $this->yaml['array on several lines']);
}
public function testArrayWithCommas() {
$this->assertEquals(array (0, 1), $this->yaml['array with commas']);
}
public function testmoreLessKey() {
$this->assertEquals ('<value>', $this->yaml['morelesskey']);
}
public function testArrayOfZero() {
$this->assertSame (array(0), $this->yaml['array_of_zero']);
}
public function testSophisticatedArrayOfZero() {
$this->assertSame (array('rx' => array ('tx' => array (0))), $this->yaml['sophisticated_array_of_zero']);
}
public function testSwitches() {
$this->assertEquals (array (array ('row' => 0, 'col' => 0, 'func' => array ('tx' => array(0, 1)))), $this->yaml['switches']);
}
public function testEmptySequence() {
$this->assertSame (array(), $this->yaml['empty_sequence']);
}
public function testEmptyHash() {
$this->assertSame (array(), $this->yaml['empty_hash']);
}
public function testEmptykey() {
$this->assertSame (array('' => array ('key' => 'value')), $this->yaml['empty_key']);
}
public function testMultilines() {
$this->assertSame (array(array('type' => 'SomeItem', 'values' => array ('blah', 'blah', 'blah', 'blah'), 'ints' => array(2, 54, 12, 2143))), $this->yaml['multiline_items']);
}
public function testManyNewlines() {
$this->assertSame ('A quick
fox
jumped
over
a lazy
dog', $this->yaml['many_lines']);
}
public function testWerte() {
$this->assertSame (array ('1' => 'nummer 1', '0' => 'Stunde 0'), $this->yaml['werte']);
}
/* public function testNoIndent() {
$this->assertSame (array(
array ('record1'=>'value1'),
array ('record2'=>'value2')
)
, $this->yaml['noindent_records']);
} */
public function testColonsInKeys() {
$this->assertSame (array (1000), $this->yaml['a:1']);
}
public function testColonsInKeys2() {
$this->assertSame (array (2000), $this->yaml['a:2']);
}
public function testUnquotedColonsInKeys() {
$this->assertSame (array (3000), $this->yaml['a:3']);
}
public function testComplicatedKeyWithColon() {
$this->assertSame(array("a:b:''test'" => 'value'), $this->yaml['complex_unquoted_key']);
}
public function testKeysInMappedValueException() {
$this->setExpectedException('Exception');
Spyc::YAMLLoad('x: y: z:');
}
public function testKeysInValueException() {
$this->setExpectedException('Exception');
Spyc::YAMLLoad('x: y: z');
}
public function testSpecialCharacters() {
$this->assertSame ('[{]]{{]]', $this->yaml['special_characters']);
}
public function testAngleQuotes() {
$Quotes = Spyc::YAMLLoad('quotes.yaml');
$this->assertEquals (array ('html_tags' => array ('<br>', '<p>'), 'html_content' => array ('<p>hello world</p>', 'hello<br>world'), 'text_content' => array ('hello world')),
$Quotes);
}
public function testFailingColons() {
$Failing = Spyc::YAMLLoad('failing1.yaml');
$this->assertSame (array ('MyObject' => array ('Prop1' => array ('key1:val1'))),
$Failing);
}
public function testQuotesWithComments() {
$Expected = 'bar';
$Actual = spyc_load_file ('comments.yaml');
$this->assertEquals ($Expected, $Actual['foo']);
}
public function testArrayWithComments() {
$Expected = array ('x', 'y', 'z');
$Actual = spyc_load_file ('comments.yaml');
$this->assertEquals ($Expected, $Actual['arr']);
}
public function testAfterArrayWithKittens() {
$Expected = 'kittens';
$Actual = spyc_load_file ('comments.yaml');
$this->assertEquals ($Expected, $Actual['bar']);
}
// Plain characters http://www.yaml.org/spec/1.2/spec.html#id2789510
public function testKai() {
$Expected = array('-example' => 'value');
$Actual = spyc_load_file ('indent_1.yaml');
$this->assertEquals ($Expected, $Actual['kai']);
}
public function testKaiList() {
$Expected = array ('-item', '-item', '-item');
$Actual = spyc_load_file ('indent_1.yaml');
$this->assertEquals ($Expected, $Actual['kai_list_of_items']);
}
public function testDifferentQuoteTypes() {
$expected = array ('Something', "", "", "Something else");
$this->assertSame ($expected, $this->yaml['invoice']);
}
public function testDifferentQuoteTypes2() {
$expected = array ('Something', "Nothing", "Anything", "Thing");
$this->assertSame ($expected, $this->yaml['quotes']);
}
// Separation spaces http://www.yaml.org/spec/1.2/spec.html#id2778394
public function testMultipleArrays() {
$expected = array(array(array('x')));
$this->assertSame($expected, Spyc::YAMLLoad("- - - x"));
}
}
@@ -0,0 +1,78 @@
<?php
require_once ("../Spyc.php");
function roundTrip($a) { return Spyc::YAMLLoad(Spyc::YAMLDump(array('x' => $a))); }
class RoundTripTest extends PHPUnit_Framework_TestCase {
protected function setUp() {
}
public function testNull() {
$this->assertEquals (array ('x' => null), roundTrip (null));
}
public function testY() {
$this->assertEquals (array ('x' => 'y'), roundTrip ('y'));
}
public function testExclam() {
$this->assertEquals (array ('x' => '!yeah'), roundTrip ('!yeah'));
}
public function test5() {
$this->assertEquals (array ('x' => '5'), roundTrip ('5'));
}
public function testSpaces() {
$this->assertEquals (array ('x' => 'x '), roundTrip ('x '));
}
public function testApostrophes() {
$this->assertEquals (array ('x' => "'biz'"), roundTrip ("'biz'"));
}
public function testNewLines() {
$this->assertEquals (array ('x' => "\n"), roundTrip ("\n"));
}
public function testHashes() {
$this->assertEquals (array ('x' => array ("#color" => '#fff')), roundTrip (array ("#color" => '#fff')));
}
public function testPreserveString() {
$result1 = roundTrip ('0');
$result2 = roundTrip ('true');
$this->assertTrue (is_string ($result1['x']));
$this->assertTrue (is_string ($result2['x']));
}
public function testPreserveBool() {
$result = roundTrip (true);
$this->assertTrue (is_bool ($result['x']));
}
public function testPreserveInteger() {
$result = roundTrip (0);
$this->assertTrue (is_int ($result['x']));
}
public function testWordWrap() {
$this->assertEquals (array ('x' => "aaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), roundTrip ("aaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"));
}
public function testABCD() {
$this->assertEquals (array ('a', 'b', 'c', 'd'), Spyc::YAMLLoad(Spyc::YAMLDump(array('a', 'b', 'c', 'd'))));
}
public function testABCD2() {
$a = array('a', 'b', 'c', 'd'); // Create a simple list
$b = Spyc::YAMLDump($a); // Dump the list as YAML
$c = Spyc::YAMLLoad($b); // Load the dumped YAML
$d = Spyc::YAMLDump($c); // Re-dump the data
$this->assertSame($b, $d);
}
}
+3
View File
@@ -0,0 +1,3 @@
foo: 'bar' #Comment
arr: ['x', 'y', 'z'] # Comment here
bar: kittens
+2
View File
@@ -0,0 +1,2 @@
MyObject:
Prop1: {key1:val1}
+70
View File
@@ -0,0 +1,70 @@
root:
child_1: 2
child_2: 0
child_3: 1
root2:
child_1: 1
# A comment
child_2: 2
displays:
- resolutions:
1024: 768
- resolutions:
1920: 1200
display:
- resolutions:
1024: 768
1920: 1200
producer: "Nec"
nested_hashes_and_seqs:
- { row: 0, col: 0, headsets_affected: [{ports: [0], side: left}], switch_function: {ics_ptt: true} }
easier_nest: { h: [{a: b, a1: b1}, {c: d}] }
one_space: |
By four
spaces
steps:
- step: &id001
instrument: Lasik 2000
pulseEnergy: 5.4
pulseDuration: 12
repetition: 1000
spotSize: 1mm
- step:
<<: *id001
spotSize: 2mm
death masks are:
sad: 2
<<: {magnificent: 4}
login: &login
adapter: mysql
host: localhost
development:
database: rails_dev
<<: *login
"key": "value:"
colon_only: ":"
list_and_comment: [one, two, three] # comment
kai:
-example: value
kai_list_of_items:
- -item
- '-item'
-item
&foo bar:
1: "abc"
zzz: *foo
+8
View File
@@ -0,0 +1,8 @@
html_tags:
- <br>
- <p>
html_content:
- <p>hello world</p>
- hello<br>world
text_content:
- hello world
+4
View File
@@ -0,0 +1,4 @@
.idea
vendor
.*.swp
composer.lock
+26
View File
@@ -0,0 +1,26 @@
sudo: false
dist: trusty
language: php
php:
- 5.4
- 5.5
- 5.6
- 7.0
- 7.1
matrix:
include:
- dist: precise
php: 5.3
before_script:
- php -m
- php --info | grep -i 'intl\|icu\|pcre'
script: phpunit --debug
notifications:
email:
on_success: never
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2010 James Logsdon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+80
View File
@@ -0,0 +1,80 @@
PHP Command Line Tools
======================
[![Build Status](https://travis-ci.org/wp-cli/php-cli-tools.png?branch=master)](https://travis-ci.org/wp-cli/php-cli-tools)
A collection of functions and classes to assist with command line development.
Requirements
* PHP >= 5.3
Suggested PHP extensions
* mbstring - Used for calculating string widths.
Function List
-------------
* `cli\out($msg, ...)`
* `cli\out_padded($msg, ...)`
* `cli\err($msg, ...)`
* `cli\line($msg = '', ...)`
* `cli\input()`
* `cli\prompt($question, $default = false, $marker = ':')`
* `cli\choose($question, $choices = 'yn', $default = 'n')`
* `cli\menu($items, $default = false, $title = 'Choose an Item')`
Progress Indicators
-------------------
* `cli\notify\Dots($msg, $dots = 3, $interval = 100)`
* `cli\notify\Spinner($msg, $interval = 100)`
* `cli\progress\Bar($msg, $total, $interval = 100)`
Tabular Display
---------------
* `cli\Table::__construct(array $headers = null, array $rows = null)`
* `cli\Table::setHeaders(array $headers)`
* `cli\Table::setRows(array $rows)`
* `cli\Table::setRenderer(cli\table\Renderer $renderer)`
* `cli\Table::addRow(array $row)`
* `cli\Table::sort($column)`
* `cli\Table::display()`
The display function will detect if output is piped and, if it is, render a tab delimited table instead of the ASCII
table rendered for visual display.
You can also explicitly set the renderer used by calling `cli\Table::setRenderer()` and giving it an instance of one
of the concrete `cli\table\Renderer` classes.
Tree Display
------------
* `cli\Tree::__construct()`
* `cli\Tree::setData(array $data)`
* `cli\Tree::setRenderer(cli\tree\Renderer $renderer)`
* `cli\Tree::render()`
* `cli\Tree::display()`
Argument Parser
---------------
Argument parsing uses a simple framework for taking a list of command line arguments,
usually straight from `$_SERVER['argv']`, and parses the input against a set of
defined rules.
Check `examples/arguments.php` for an example.
Usage
-----
See `examples/` directory for examples.
Todo
----
* Expand this README
* Add doc blocks to rest of code
+31
View File
@@ -0,0 +1,31 @@
{
"name": "wp-cli/php-cli-tools",
"type": "library",
"description": "Console utilities for PHP",
"keywords": ["console", "cli"],
"homepage": "http://github.com/wp-cli/php-cli-tools",
"license": "MIT",
"authors": [
{
"name": "Daniel Bachhuber",
"email": "daniel@handbuilt.co",
"role": "Maintainer"
},
{
"name": "James Logsdon",
"email": "jlogsdon@php.net",
"role": "Developer"
}
],
"require": {
"php": ">= 5.3.0"
},
"autoload": {
"psr-0": {
"cli": "lib/"
},
"files": [
"lib/cli/cli.php"
]
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
/**
* Sample invocations:
*
* # php example_args.php -vC ./ --version
* {"verbose":true,"cache":".\/","version":true}
* # php example_args.php -vC --version
* PHP Warning: [cli\Arguments] no value given for -C
* # php example_args.php -vC multi word --version
* {"verbose":true,"cache":"multi word","version":true}
*
*/
require 'common.php';
$strict = in_array('--strict', $_SERVER['argv']);
$arguments = new \cli\Arguments(compact('strict'));
$arguments->addFlag(array('verbose', 'v'), 'Turn on verbose output');
$arguments->addFlag('version', 'Display the version');
$arguments->addFlag(array('quiet', 'q'), 'Disable all output');
$arguments->addFlag(array('help', 'h'), 'Show this help screen');
$arguments->addOption(array('cache', 'C'), array(
'default' => getcwd(),
'description' => 'Set the cache directory'));
$arguments->addOption(array('name', 'n'), array(
'default' => 'James',
'description' => 'Set a name with a really long description and a default so we can see what line wrapping looks like which is probably a goo idea'));
$arguments->parse();
if ($arguments['help']) {
echo $arguments->getHelpScreen();
echo "\n\n";
}
echo $arguments->asJSON() . "\n";
+32
View File
@@ -0,0 +1,32 @@
<?php
// Samples. Lines marked with * should be colored in output
// php examples/colors.php
// * All output is run through \cli\Colors::colorize before display
// * All output is run through \cli\Colors::colorize before display
// * All output is run through \cli\Colors::colorize before display
// * All output is run through \cli\Colors::colorize before display
// All output is run through \cli\Colors::colorize before display
// * All output is run through \cli\Colors::colorize before display
// php examples/colors.php | cat
// All output is run through \cli\Colors::colorize before display
// * All output is run through \cli\Colors::colorize before display
// All output is run through \cli\Colors::colorize before display
// * All output is run through \cli\Colors::colorize before display
// All output is run through \cli\Colors::colorize before display
// All output is run through \cli\Colors::colorize before display
require_once 'common.php';
\cli\line(' %C%5All output is run through %Y%6\cli\Colors::colorize%C%5 before display%n');
echo \cli\Colors::colorize(' %C%5All output is run through %Y%6\cli\Colors::colorize%C%5 before display%n', true) . "\n";
echo \cli\Colors::colorize(' %C%5All output is run through %Y%6\cli\Colors::colorize%C%5 before display%n') . "\n";
\cli\Colors::enable(); // Forcefully enable
\cli\line(' %C%5All output is run through %Y%6\cli\Colors::colorize%C%5 before display%n');
\cli\Colors::disable(); // Disable forcefully!
\cli\line(' %C%5All output is run through %Y%6\cli\Colors::colorize%C%5 before display%n', true);
\cli\Colors::enable(false); // Enable, but not forcefully
\cli\line(' %C%5All output is run through %Y%6\cli\Colors::colorize%C%5 before display%n');
+37
View File
@@ -0,0 +1,37 @@
<?php
if (php_sapi_name() != 'cli') {
die('Must run from command line');
}
error_reporting(E_ALL | E_STRICT);
ini_set('display_errors', 1);
ini_set('log_errors', 0);
ini_set('html_errors', 0);
foreach(array(__DIR__ . '/../vendor', __DIR__ . '/../../../../vendor') as $vendorDir) {
if(is_dir($vendorDir)) {
require_once $vendorDir . '/autoload.php';
break;
}
}
function test_notify(cli\Notify $notify, $cycle = 1000000, $sleep = null) {
for ($i = 0; $i < $cycle; $i++) {
$notify->tick();
if ($sleep) usleep($sleep);
}
$notify->finish();
}
function test_notify_msg(cli\Notify $notify, $cycle = 1000000, $sleep = null) {
$notify->display();
for ($i = 0; $i < $cycle; $i++) {
// Sleep before tick to simulate time-intensive work and give time
// for the initial message to display before it is changed
if ($sleep) usleep($sleep);
$msg = sprintf(' Finished step %d', $i + 1);
$notify->tick(1, $msg);
}
$notify->finish();
}
+24
View File
@@ -0,0 +1,24 @@
<?php
require_once 'common.php';
$menu = array(
'output' => 'Output Examples',
'notify' => 'cli\Notify Examples',
'progress' => 'cli\Progress Examples',
'table' => 'cli\Table Example',
'colors' => 'cli\Colors example',
'quit' => 'Quit',
);
while (true) {
$choice = \cli\menu($menu, null, 'Choose an example');
\cli\line();
if ($choice == 'quit') {
break;
}
include "${choice}.php";
\cli\line();
}
+12
View File
@@ -0,0 +1,12 @@
<?php
require_once 'common.php';
\cli\line("========\nDots\n");
test_notify(new \cli\notify\Dots(' \cli\notify\Dots cycles through a set number of dots'));
test_notify(new \cli\notify\Dots(' You can disable the delay if ticks take longer than a few milliseconds', 5, 0), 10, 100000);
\cli\line("\n========\nSpinner\n");
test_notify(new \cli\notify\Spinner(' \cli\notify\Spinner cycles through a set of characters to emulate a spinner'));
+18
View File
@@ -0,0 +1,18 @@
<?php
require_once 'common.php';
\cli\out(" \\cli\\out sends output to STDOUT\n");
\cli\out(" It does not automatically append a new line\n");
\cli\out(" It does accept any number of %s which are then %s to %s for formatting\n", 'arguments', 'passed', 'sprintf');
\cli\out(" Alternatively, {:a} can use an {:b} as the second argument.\n\n", array('a' => 'you', 'b' => 'array'));
\cli\err(' \cli\err sends output to STDERR');
\cli\err(' It does automatically append a new line');
\cli\err(' It does accept any number of %s which are then %s to %s for formatting', 'arguments', 'passed', 'sprintf');
\cli\err(" Alternatively, {:a} can use an {:b} as the second argument.\n", array('a' => 'you', 'b' => 'array'));
\cli\line(' \cli\line forwards to \cli\out for output');
\cli\line(' It does automatically append a new line');
\cli\line(' It does accept any number of %s which are then %s to %s for formatting', 'arguments', 'passed', 'sprintf');
\cli\line(" Alternatively, {:a} can use an {:b} as the second argument.\n", array('a' => 'you', 'b' => 'array'));
+7
View File
@@ -0,0 +1,7 @@
<?php
require_once 'common.php';
test_notify(new \cli\progress\Bar(' \cli\progress\Bar displays a progress bar', 1000000));
test_notify(new \cli\progress\Bar(' It sizes itself dynamically', 1000000));
test_notify_msg(new \cli\progress\Bar(' It can even change its message', 5), 5, 1000000);
+42
View File
@@ -0,0 +1,42 @@
<?php
require_once 'common.php';
/*
* Please notice that the data has to be an 0-based array,
* not an associative one if you intend to work with custom
* column widths.
*/
$headers = array('First Name', 'Last Name', 'City', 'State');
$data = array(
array('Maryam', 'Elliott', 'Elizabeth City', 'SD'),
array('Jerry', 'Washington', 'Bessemer', 'ME'),
array('Allegra', 'Hopkins', 'Altoona', 'ME'),
array('Audrey', 'Oneil', 'Dalton', 'SK'),
array('Ruth', 'Mcpherson', 'San Francisco', 'ID'),
array('Odessa', 'Tate', 'Chattanooga', 'FL'),
array('Violet', 'Nielsen', 'Valdosta', 'AB'),
array('Summer', 'Rollins', 'Revere', 'SK'),
array('Mufutau', 'Bowers', 'Scottsbluff', 'WI'),
array('Grace', 'Rosario', 'Garden Grove', 'KY'),
array('Amanda', 'Berry', 'La Habra', 'AZ'),
array('Cassady', 'York', 'Fulton', 'BC'),
array('Heather', 'Terrell', 'Statesboro', 'SC'),
array('Dominic', 'Jimenez', 'West Valley City', 'ME'),
array('Rhonda', 'Potter', 'Racine', 'BC'),
array('Nathan', 'Velazquez', 'Cedarburg', 'BC'),
array('Richard', 'Fletcher', 'Corpus Christi', 'BC'),
array('Cheyenne', 'Rios', 'Broken Arrow', 'VA'),
array('Velma', 'Clemons', 'Helena', 'IL'),
array('Samuel', 'Berry', 'Lawrenceville', 'NU'),
array('Marcia', 'Swanson', 'Fontana', 'QC'),
array('Zachary', 'Silva', 'Port Washington', 'MB'),
array('Hilary', 'Chambers', 'Suffolk', 'HI'),
array('Idola', 'Carroll', 'West Sacramento', 'QC'),
array('Kirestin', 'Stephens', 'Fitchburg', 'AB'),
);
$table = new \cli\Table();
$table->setHeaders($headers);
$table->setRows($data);
$table->setRenderer(new \cli\table\Ascii([10, 10, 20, 5]));
$table->display();
+72
View File
@@ -0,0 +1,72 @@
<?php
require_once 'common.php';
$data = array(
'Test' => array(
'Something Cool' => array(
'This is a 3rd layer',
),
'This is a 2nd layer',
),
'Other test' => array(
'This is awesome' => array(
'This is also cool',
'This is even cooler',
'Wow like what is this' => array(
'Awesome eh?',
'Totally' => array(
'Yep!'
),
),
),
),
);
printf("ASCII:\n");
/**
* ASCII should look something like this:
*
* -Test
* |\-Something Cool
* ||\-This is a 3rd layer
* |\-This is a 2nd layer
* \-Other test
* \-This is awesome
* \-This is also cool
* \-This is even cooler
* \-Wow like what is this
* \-Awesome eh?
* \-Totally
* \-Yep!
*/
$tree = new \cli\Tree;
$tree->setData($data);
$tree->setRenderer(new \cli\tree\Ascii);
$tree->display();
printf("\nMarkdown:\n");
/**
* Markdown looks like this:
*
* - Test
* - Something Cool
* - This is a 3rd layer
* - This is a 2nd layer
* - Other test
* - This is awesome
* - This is also cool
* - This is even cooler
* - Wow like what is this
* - Awesome eh?
* - Totally
* - Yep!
*/
$tree = new \cli\Tree;
$tree->setData($data);
$tree->setRenderer(new \cli\tree\Markdown(4));
$tree->display();
+70
View File
@@ -0,0 +1,70 @@
<?php
/**
* An example application using php-cli-tools and Buzz
*/
require_once __DIR__ . '/vendor/autoload.php';
define('BUZZ_PATH', realpath('../Buzz'));
define('SCRIPT_NAME', array_shift($argv));
require_once BUZZ_PATH . '/lib/Buzz/ClassLoader.php';
Buzz\ClassLoader::register();
class HttpConsole {
protected $_host;
protected $_prompt;
public function __construct($host) {
$this->_host = 'http://' . $host;
$this->_prompt = '%K' . $this->_host . '%n/%K>%n ';
}
public function handleRequest($type, $path) {
$request = new Buzz\Message\Request($type, $path, $this->_host);
$response = new Buzz\Message\Response;
$client = new Buzz\Client\FileGetContents();
$client->send($request, $response);
// Display headers
foreach ($response->getHeaders() as $i => $header) {
if ($i == 0) {
\cli\line('%G{:header}%n', compact('header'));
continue;
}
list($key, $value) = explode(': ', $header, 2);
\cli\line('%W{:key}%n: {:value}', compact('key', 'value'));
}
\cli\line("\n");
print $response->getContent() . "\n";
switch ($type) {
}
}
public function run() {
while (true) {
$cmd = \cli\prompt($this->_prompt, false, null);
if (preg_match('/^(HEAD|GET|POST|PUT|DELETE) (\S+)$/', $cmd, $matches)) {
$this->handleRequest($matches[1], $matches[2]);
continue;
}
if ($cmd == '\q') {
break;
}
}
}
}
try {
$console = new HttpConsole(array_shift($argv) ?: '127.0.0.1:80');
$console->run();
} catch (\Exception $e) {
\cli\err("\n\n%R" . $e->getMessage() . "%n\n");
}
?>
+488
View File
@@ -0,0 +1,488 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli;
use cli\arguments\Argument;
use cli\arguments\HelpScreen;
use cli\arguments\InvalidArguments;
use cli\arguments\Lexer;
/**
* Parses command line arguments.
*/
class Arguments implements \ArrayAccess {
protected $_flags = array();
protected $_options = array();
protected $_strict = false;
protected $_input = array();
protected $_invalid = array();
protected $_parsed;
protected $_lexer;
/**
* Initializes the argument parser. If you wish to change the default behaviour
* you may pass an array of options as the first argument. Valid options are
* `'help'` and `'strict'`, each a boolean.
*
* `'help'` is `true` by default, `'strict'` is false by default.
*
* @param array $options An array of options for this parser.
*/
public function __construct($options = array()) {
$options += array(
'strict' => false,
'input' => array_slice($_SERVER['argv'], 1)
);
$this->_input = $options['input'];
$this->setStrict($options['strict']);
if (isset($options['flags'])) {
$this->addFlags($options['flags']);
}
if (isset($options['options'])) {
$this->addOptions($options['options']);
}
}
/**
* Get the list of arguments found by the defined definitions.
*
* @return array
*/
public function getArguments() {
if (!isset($this->_parsed)) {
$this->parse();
}
return $this->_parsed;
}
public function getHelpScreen() {
return new HelpScreen($this);
}
/**
* Encodes the parsed arguments as JSON.
*
* @return string
*/
public function asJSON() {
return json_encode($this->_parsed);
}
/**
* Returns true if a given argument was parsed.
*
* @param mixed $offset An Argument object or the name of the argument.
* @return bool
*/
public function offsetExists($offset) {
if ($offset instanceOf Argument) {
$offset = $offset->key;
}
return array_key_exists($offset, $this->_parsed);
}
/**
* Get the parsed argument's value.
*
* @param mixed $offset An Argument object or the name of the argument.
* @return mixed
*/
public function offsetGet($offset) {
if ($offset instanceOf Argument) {
$offset = $offset->key;
}
if (isset($this->_parsed[$offset])) {
return $this->_parsed[$offset];
}
}
/**
* Sets the value of a parsed argument.
*
* @param mixed $offset An Argument object or the name of the argument.
* @param mixed $value The value to set
*/
public function offsetSet($offset, $value) {
if ($offset instanceOf Argument) {
$offset = $offset->key;
}
$this->_parsed[$offset] = $value;
}
/**
* Unset a parsed argument.
*
* @param mixed $offset An Argument object or the name of the argument.
*/
public function offsetUnset($offset) {
if ($offset instanceOf Argument) {
$offset = $offset->key;
}
unset($this->_parsed[$offset]);
}
/**
* Adds a flag (boolean argument) to the argument list.
*
* @param mixed $flag A string representing the flag, or an array of strings.
* @param array $settings An array of settings for this flag.
* @setting string description A description to be shown in --help.
* @setting bool default The default value for this flag.
* @setting bool stackable Whether the flag is repeatable to increase the value.
* @setting array aliases Other ways to trigger this flag.
* @return $this
*/
public function addFlag($flag, $settings = array()) {
if (is_string($settings)) {
$settings = array('description' => $settings);
}
if (is_array($flag)) {
$settings['aliases'] = $flag;
$flag = array_shift($settings['aliases']);
}
if (isset($this->_flags[$flag])) {
$this->_warn('flag already exists: ' . $flag);
return $this;
}
$settings += array(
'default' => false,
'stackable' => false,
'description' => null,
'aliases' => array()
);
$this->_flags[$flag] = $settings;
return $this;
}
/**
* Add multiple flags at once. The input array should be keyed with the
* primary flag character, and the values should be the settings array
* used by {addFlag}.
*
* @param array $flags An array of flags to add
* @return $this
*/
public function addFlags($flags) {
foreach ($flags as $flag => $settings) {
if (is_numeric($flag)) {
$this->_warn('No flag character given');
continue;
}
$this->addFlag($flag, $settings);
}
return $this;
}
/**
* Adds an option (string argument) to the argument list.
*
* @param mixed $option A string representing the option, or an array of strings.
* @param array $settings An array of settings for this option.
* @setting string description A description to be shown in --help.
* @setting bool default The default value for this option.
* @setting array aliases Other ways to trigger this option.
* @return $this
*/
public function addOption($option, $settings = array()) {
if (is_string($settings)) {
$settings = array('description' => $settings);
}
if (is_array($option)) {
$settings['aliases'] = $option;
$option = array_shift($settings['aliases']);
}
if (isset($this->_options[$option])) {
$this->_warn('option already exists: ' . $option);
return $this;
}
$settings += array(
'default' => null,
'description' => null,
'aliases' => array()
);
$this->_options[$option] = $settings;
return $this;
}
/**
* Add multiple options at once. The input array should be keyed with the
* primary option string, and the values should be the settings array
* used by {addOption}.
*
* @param array $options An array of options to add
* @return $this
*/
public function addOptions($options) {
foreach ($options as $option => $settings) {
if (is_numeric($option)) {
$this->_warn('No option string given');
continue;
}
$this->addOption($option, $settings);
}
return $this;
}
/**
* Enable or disable strict mode. If strict mode is active any invalid
* arguments found by the parser will throw `cli\arguments\InvalidArguments`.
*
* Even if strict is disabled, invalid arguments are logged and can be
* retrieved with `cli\Arguments::getInvalidArguments()`.
*
* @param bool $strict True to enable, false to disable.
* @return $this
*/
public function setStrict($strict) {
$this->_strict = (bool)$strict;
return $this;
}
/**
* Get the list of invalid arguments the parser found.
*
* @return array
*/
public function getInvalidArguments() {
return $this->_invalid;
}
/**
* Get a flag by primary matcher or any defined aliases.
*
* @param mixed $flag Either a string representing the flag or an
* cli\arguments\Argument object.
* @return array
*/
public function getFlag($flag) {
if ($flag instanceOf Argument) {
$obj = $flag;
$flag = $flag->value;
}
if (isset($this->_flags[$flag])) {
return $this->_flags[$flag];
}
foreach ($this->_flags as $master => $settings) {
if (in_array($flag, (array)$settings['aliases'])) {
if (isset($obj)) {
$obj->key = $master;
}
$cache[$flag] =& $settings;
return $settings;
}
}
}
public function getFlags() {
return $this->_flags;
}
public function hasFlags() {
return !empty($this->_flags);
}
/**
* Returns true if the given argument is defined as a flag.
*
* @param mixed $argument Either a string representing the flag or an
* cli\arguments\Argument object.
* @return bool
*/
public function isFlag($argument) {
return (null !== $this->getFlag($argument));
}
/**
* Returns true if the given flag is stackable.
*
* @param mixed $flag Either a string representing the flag or an
* cli\arguments\Argument object.
* @return bool
*/
public function isStackable($flag) {
$settings = $this->getFlag($flag);
return isset($settings) && (true === $settings['stackable']);
}
/**
* Get an option by primary matcher or any defined aliases.
*
* @param mixed $option Either a string representing the option or an
* cli\arguments\Argument object.
* @return array
*/
public function getOption($option) {
if ($option instanceOf Argument) {
$obj = $option;
$option = $option->value;
}
if (isset($this->_options[$option])) {
return $this->_options[$option];
}
foreach ($this->_options as $master => $settings) {
if (in_array($option, (array)$settings['aliases'])) {
if (isset($obj)) {
$obj->key = $master;
}
return $settings;
}
}
}
public function getOptions() {
return $this->_options;
}
public function hasOptions() {
return !empty($this->_options);
}
/**
* Returns true if the given argument is defined as an option.
*
* @param mixed $argument Either a string representing the option or an
* cli\arguments\Argument object.
* @return bool
*/
public function isOption($argument) {
return (null != $this->getOption($argument));
}
/**
* Parses the argument list with the given options. The returned argument list
* will use either the first long name given or the first name in the list
* if a long name is not given.
*
* @return array
* @throws arguments\InvalidArguments
*/
public function parse() {
$this->_invalid = array();
$this->_parsed = array();
$this->_lexer = new Lexer($this->_input);
$this->_applyDefaults();
foreach ($this->_lexer as $argument) {
if ($this->_parseFlag($argument)) {
continue;
}
if ($this->_parseOption($argument)) {
continue;
}
array_push($this->_invalid, $argument->raw);
}
if ($this->_strict && !empty($this->_invalid)) {
throw new InvalidArguments($this->_invalid);
}
}
/**
* This applies the default values, if any, of all of the
* flags and options, so that if there is a default value
* it will be available.
*/
private function _applyDefaults() {
foreach($this->_flags as $flag => $settings) {
$this[$flag] = $settings['default'];
}
foreach($this->_options as $option => $settings) {
// If the default is 0 we should still let it be set.
if (!empty($settings['default']) || $settings['default'] === 0) {
$this[$option] = $settings['default'];
}
}
}
private function _warn($message) {
trigger_error('[' . __CLASS__ .'] ' . $message, E_USER_WARNING);
}
private function _parseFlag($argument) {
if (!$this->isFlag($argument)) {
return false;
}
if ($this->isStackable($argument)) {
if (!isset($this[$argument])) {
$this[$argument->key] = 0;
}
$this[$argument->key] += 1;
} else {
$this[$argument->key] = true;
}
return true;
}
private function _parseOption($option) {
if (!$this->isOption($option)) {
return false;
}
// Peak ahead to make sure we get a value.
if ($this->_lexer->end() || !$this->_lexer->peek->isValue) {
$optionSettings = $this->getOption($option->key);
if (empty($optionSettings['default']) && $optionSettings !== 0) {
// Oops! Got no value and no default , throw a warning and continue.
$this->_warn('no value given for ' . $option->raw);
$this[$option->key] = null;
} else {
// No value and we have a default, so we set to the default
$this[$option->key] = $optionSettings['default'];
}
return true;
}
// Store as array and join to string after looping for values
$values = array();
// Loop until we find a flag in peak-ahead
foreach ($this->_lexer as $value) {
array_push($values, $value->raw);
if (!$this->_lexer->end() && !$this->_lexer->peek->isValue) {
break;
}
}
$this[$option->key] = join($values, ' ');
return true;
}
}
+279
View File
@@ -0,0 +1,279 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli;
/**
* Change the color of text.
*
* Reference: http://graphcomp.com/info/specs/ansi_col.html#colors
*/
class Colors {
static protected $_colors = array(
'color' => array(
'black' => 30,
'red' => 31,
'green' => 32,
'yellow' => 33,
'blue' => 34,
'magenta' => 35,
'cyan' => 36,
'white' => 37
),
'style' => array(
'bright' => 1,
'dim' => 2,
'underline' => 4,
'blink' => 5,
'reverse' => 7,
'hidden' => 8
),
'background' => array(
'black' => 40,
'red' => 41,
'green' => 42,
'yellow' => 43,
'blue' => 44,
'magenta' => 45,
'cyan' => 46,
'white' => 47
)
);
static protected $_enabled = null;
static protected $_string_cache = array();
static public function enable($force = true) {
self::$_enabled = $force === true ? true : null;
}
static public function disable($force = true) {
self::$_enabled = $force === true ? false : null;
}
/**
* Check if we should colorize output based on local flags and shell type.
*
* Only check the shell type if `Colors::$_enabled` is null and `$colored` is null.
*/
static public function shouldColorize($colored = null) {
return self::$_enabled === true ||
(self::$_enabled !== false &&
($colored === true ||
($colored !== false && Streams::isTty())));
}
/**
* Set the color.
*
* @param string $color The name of the color or style to set.
* @return string
*/
static public function color($color) {
if (!is_array($color)) {
$color = compact('color');
}
$color += array('color' => null, 'style' => null, 'background' => null);
if ($color['color'] == 'reset') {
return "\033[0m";
}
$colors = array();
foreach (array('color', 'style', 'background') as $type) {
$code = $color[$type];
if (isset(self::$_colors[$type][$code])) {
$colors[] = self::$_colors[$type][$code];
}
}
if (empty($colors)) {
$colors[] = 0;
}
return "\033[" . join(';', $colors) . "m";
}
/**
* Colorize a string using helpful string formatters. If the `Streams::$out` points to a TTY coloring will be enabled,
* otherwise disabled. You can control this check with the `$colored` parameter.
*
* @param string $string
* @param boolean $colored Force enable or disable the colorized output. If left as `null` the TTY will control coloring.
* @return string
*/
static public function colorize($string, $colored = null) {
$passed = $string;
if (!self::shouldColorize($colored)) {
$return = self::decolorize( $passed, 2 /*keep_encodings*/ );
self::cacheString($passed, $return);
return $return;
}
$md5 = md5($passed);
if (isset(self::$_string_cache[$md5]['colorized'])) {
return self::$_string_cache[$md5]['colorized'];
}
$string = str_replace('%%', '%¾', $string);
foreach (self::getColors() as $key => $value) {
$string = str_replace($key, self::color($value), $string);
}
$string = str_replace('%¾', '%', $string);
self::cacheString($passed, $string);
return $string;
}
/**
* Remove color information from a string.
*
* @param string $string A string with color information.
* @param int $keep Optional. If the 1 bit is set, color tokens (eg "%n") won't be stripped. If the 2 bit is set, color encodings (ANSI escapes) won't be stripped. Default 0.
* @return string A string with color information removed.
*/
static public function decolorize( $string, $keep = 0 ) {
if ( ! ( $keep & 1 ) ) {
// Get rid of color tokens if they exist
$string = str_replace('%%', '%¾', $string);
$string = str_replace(array_keys(self::getColors()), '', $string);
$string = str_replace('%¾', '%', $string);
}
if ( ! ( $keep & 2 ) ) {
// Remove color encoding if it exists
foreach (self::getColors() as $key => $value) {
$string = str_replace(self::color($value), '', $string);
}
}
return $string;
}
/**
* Cache the original, colorized, and decolorized versions of a string.
*
* @param string $passed The original string before colorization.
* @param string $colorized The string after running through self::colorize.
* @param string $deprecated Optional. Not used. Default null.
*/
static public function cacheString( $passed, $colorized, $deprecated = null ) {
self::$_string_cache[md5($passed)] = array(
'passed' => $passed,
'colorized' => $colorized,
'decolorized' => self::decolorize($passed), // Not very useful but keep for BC.
);
}
/**
* Return the length of the string without color codes.
*
* @param string $string the string to measure
* @return int
*/
static public function length($string) {
return safe_strlen( self::decolorize( $string ) );
}
/**
* Return the width (length in characters) of the string without color codes if enabled.
*
* @param string $string The string to measure.
* @param bool $pre_colorized Optional. Set if the string is pre-colorized. Default false.
* @param string|bool $encoding Optional. The encoding of the string. Default false.
* @return int
*/
static public function width( $string, $pre_colorized = false, $encoding = false ) {
return strwidth( $pre_colorized || self::shouldColorize() ? self::decolorize( $string, $pre_colorized ? 1 /*keep_tokens*/ : 0 ) : $string, $encoding );
}
/**
* Pad the string to a certain display length.
*
* @param string $string The string to pad.
* @param int $length The display length.
* @param bool $pre_colorized Optional. Set if the string is pre-colorized. Default false.
* @param string|bool $encoding Optional. The encoding of the string. Default false.
* @param int $pad_type Optional. Can be STR_PAD_RIGHT, STR_PAD_LEFT, or STR_PAD_BOTH. If pad_type is not specified it is assumed to be STR_PAD_RIGHT.
* @return string
*/
static public function pad( $string, $length, $pre_colorized = false, $encoding = false, $pad_type = STR_PAD_RIGHT ) {
$real_length = self::width( $string, $pre_colorized, $encoding );
$diff = strlen( $string ) - $real_length;
$length += $diff;
return str_pad( $string, $length, ' ', $pad_type );
}
/**
* Get the color mapping array.
*
* @return array Array of color tokens mapped to colors and styles.
*/
static public function getColors() {
return array(
'%y' => array('color' => 'yellow'),
'%g' => array('color' => 'green'),
'%b' => array('color' => 'blue'),
'%r' => array('color' => 'red'),
'%p' => array('color' => 'magenta'),
'%m' => array('color' => 'magenta'),
'%c' => array('color' => 'cyan'),
'%w' => array('color' => 'white'),
'%k' => array('color' => 'black'),
'%n' => array('color' => 'reset'),
'%Y' => array('color' => 'yellow', 'style' => 'bright'),
'%G' => array('color' => 'green', 'style' => 'bright'),
'%B' => array('color' => 'blue', 'style' => 'bright'),
'%R' => array('color' => 'red', 'style' => 'bright'),
'%P' => array('color' => 'magenta', 'style' => 'bright'),
'%M' => array('color' => 'magenta', 'style' => 'bright'),
'%C' => array('color' => 'cyan', 'style' => 'bright'),
'%W' => array('color' => 'white', 'style' => 'bright'),
'%K' => array('color' => 'black', 'style' => 'bright'),
'%N' => array('color' => 'reset', 'style' => 'bright'),
'%3' => array('background' => 'yellow'),
'%2' => array('background' => 'green'),
'%4' => array('background' => 'blue'),
'%1' => array('background' => 'red'),
'%5' => array('background' => 'magenta'),
'%6' => array('background' => 'cyan'),
'%7' => array('background' => 'white'),
'%0' => array('background' => 'black'),
'%F' => array('style' => 'blink'),
'%U' => array('style' => 'underline'),
'%8' => array('style' => 'reverse'),
'%9' => array('style' => 'bright'),
'%_' => array('style' => 'bright')
);
}
/**
* Get the cached string values.
*
* @return array The cached string values.
*/
static public function getStringCache() {
return self::$_string_cache;
}
/**
* Clear the string cache.
*/
static public function clearStringCache() {
self::$_string_cache = array();
}
}
+44
View File
@@ -0,0 +1,44 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli;
abstract class Memoize {
protected $_memoCache = array();
public function __get($name) {
if (isset($this->_memoCache[$name])) {
return $this->_memoCache[$name];
}
// Hide probable private methods
if (0 == strncmp($name, '_', 1)) {
return ($this->_memoCache[$name] = null);
}
if (!method_exists($this, $name)) {
return ($this->_memoCache[$name] = null);
}
$method = array($this, $name);
($this->_memoCache[$name] = call_user_func($method));
return $this->_memoCache[$name];
}
protected function _unmemo($name) {
if ($name === true) {
$this->_memoCache = array();
} else {
unset($this->_memoCache[$name]);
}
}
}
+185
View File
@@ -0,0 +1,185 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli;
use cli\Streams;
/**
* The `Notify` class is the basis of all feedback classes, such as Indicators
* and Progress meters. The default behaviour is to refresh output after 100ms
* have passed. This is done to preventing the screen from flickering and keep
* slowdowns from output to a minimum.
*
* The most basic form of Notifier has no maxim, and simply displays a series
* of characters to indicate progress is being made.
*/
abstract class Notify {
protected $_current = 0;
protected $_first = true;
protected $_interval;
protected $_message;
protected $_start;
protected $_timer;
/**
* Instatiates a Notification object.
*
* @param string $msg The text to display next to the Notifier.
* @param int $interval The interval in milliseconds between updates.
*/
public function __construct($msg, $interval = 100) {
$this->_message = $msg;
$this->_interval = (int)$interval;
}
/**
* This method should be used to print out the Notifier. This method is
* called from `cli\Notify::tick()` after `cli\Notify::$_interval` has passed.
*
* @abstract
* @param boolean $finish
* @see cli\Notify::tick()
*/
abstract public function display($finish = false);
/**
* Reset the notifier state so the same instance can be used in multiple loops.
*/
public function reset() {
$this->_current = 0;
$this->_first = true;
$this->_start = null;
$this->_timer = null;
}
/**
* Returns the formatted tick count.
*
* @return string The formatted tick count.
*/
public function current() {
return number_format($this->_current);
}
/**
* Calculates the time elapsed since the Notifier was first ticked.
*
* @return int The elapsed time in seconds.
*/
public function elapsed() {
if (!$this->_start) {
return 0;
}
$elapsed = time() - $this->_start;
return $elapsed;
}
/**
* Calculates the speed (number of ticks per second) at which the Notifier
* is being updated.
*
* @return int The number of ticks performed in 1 second.
*/
public function speed() {
static $tick, $iteration = 0, $speed = 0;
if (!$this->_start) {
return 0;
} else if (!$tick) {
$tick = $this->_start;
}
$now = microtime(true);
$span = $now - $tick;
if ($span > 1) {
$iteration++;
$tick = $now;
$speed = ($this->_current / $iteration) / $span;
}
return $speed;
}
/**
* Takes a time span given in seconds and formats it for display. The
* returned string will be in MM:SS form.
*
* @param int $time The time span in seconds to format.
* @return string The formatted time span.
*/
public function formatTime($time) {
return floor($time / 60) . ':' . str_pad($time % 60, 2, 0, STR_PAD_LEFT);
}
/**
* Finish our Notification display. Should be called after the Notifier is
* no longer needed.
*
* @see cli\Notify::display()
*/
public function finish() {
Streams::out("\r");
$this->display(true);
Streams::line();
}
/**
* Increments are tick counter by the given amount. If no amount is provided,
* the ticker is incremented by 1.
*
* @param int $increment The amount to increment by.
*/
public function increment($increment = 1) {
$this->_current += $increment;
}
/**
* Determines whether the display should be updated or not according to
* our interval setting.
*
* @return boolean `true` if the display should be updated, `false` otherwise.
*/
public function shouldUpdate() {
$now = microtime(true) * 1000;
if (empty($this->_timer)) {
$this->_start = (int)(($this->_timer = $now) / 1000);
return true;
}
if (($now - $this->_timer) > $this->_interval) {
$this->_timer = $now;
return true;
}
return false;
}
/**
* This method is the meat of all Notifiers. First we increment the ticker
* and then update the display if enough time has passed since our last tick.
*
* @param int $increment The amount to increment by.
* @see cli\Notify::increment()
* @see cli\Notify::shouldUpdate()
* @see cli\Notify::display()
*/
public function tick($increment = 1) {
$this->increment($increment);
if ($this->shouldUpdate()) {
Streams::out("\r");
$this->display();
}
}
}
+134
View File
@@ -0,0 +1,134 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli;
/**
* A more complex type of Notifier, `Progress` Notifiers always have a maxim
* value and generally show some form of percent complete or estimated time
* to completion along with the standard Notifier displays.
*
* @see cli\Notify
*/
abstract class Progress extends \cli\Notify {
protected $_total = 0;
/**
* Instantiates a Progress Notifier.
*
* @param string $msg The text to display next to the Notifier.
* @param int $total The total number of ticks we will be performing.
* @param int $interval The interval in milliseconds between updates.
* @see cli\Progress::setTotal()
*/
public function __construct($msg, $total, $interval = 100) {
parent::__construct($msg, $interval);
$this->setTotal($total);
}
/**
* Set the max increments for this progress notifier.
*
* @param int $total The total number of times this indicator should be `tick`ed.
* @throws \InvalidArgumentException Thrown if the `$total` is less than 0.
*/
public function setTotal($total) {
$this->_total = (int)$total;
if ($this->_total < 0) {
throw new \InvalidArgumentException('Maximum value out of range, must be positive.');
}
}
/**
* Reset the progress state so the same instance can be used in multiple loops.
*/
public function reset($total = null) {
parent::reset();
if ($total) {
$this->setTotal($total);
}
}
/**
* Behaves in a similar manner to `cli\Notify::current()`, but the output
* is padded to match the length of `cli\Progress::total()`.
*
* @return string The formatted and padded tick count.
* @see cli\Progress::total()
*/
public function current() {
$size = strlen($this->total());
return str_pad(parent::current(), $size);
}
/**
* Returns the formatted total expected ticks.
*
* @return string The formatted total ticks.
*/
public function total() {
return number_format($this->_total);
}
/**
* Calculates the estimated total time for the tick count to reach the
* total ticks given.
*
* @return int The estimated total number of seconds for all ticks to be
* completed. This is not the estimated time left, but total.
* @see cli\Notify::speed()
* @see cli\Notify::elapsed()
*/
public function estimated() {
$speed = $this->speed();
if (!$speed || !$this->elapsed()) {
return 0;
}
$estimated = round($this->_total / $speed);
return $estimated;
}
/**
* Forces the current tick count to the total ticks given at instatiation
* time before passing on to `cli\Notify::finish()`.
*/
public function finish() {
$this->_current = $this->_total;
parent::finish();
}
/**
* Increments are tick counter by the given amount. If no amount is provided,
* the ticker is incremented by 1.
*
* @param int $increment The amount to increment by.
*/
public function increment($increment = 1) {
$this->_current = min($this->_total, $this->_current + $increment);
}
/**
* Calculate the percentage completed.
*
* @return float The percent completed.
*/
public function percent() {
if ($this->_total == 0) {
return 1;
}
return ($this->_current / $this->_total);
}
}
+117
View File
@@ -0,0 +1,117 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli;
/**
* The `Shell` class is a utility class for shell related tasks such as
* information on width.
*/
class Shell {
/**
* Returns the number of columns the current shell has for display.
*
* @return int The number of columns.
* @todo Test on more systems.
*/
static public function columns() {
static $columns;
if ( getenv( 'PHP_CLI_TOOLS_TEST_SHELL_COLUMNS_RESET' ) ) {
$columns = null;
}
if ( null === $columns ) {
if ( function_exists( 'exec' ) ) {
if ( self::is_windows() ) {
// Cater for shells such as Cygwin and Git bash where `mode CON` returns an incorrect value for columns.
if ( ( $shell = getenv( 'SHELL' ) ) && preg_match( '/(?:bash|zsh)(?:\.exe)?$/', $shell ) && getenv( 'TERM' ) ) {
$columns = (int) exec( 'tput cols' );
}
if ( ! $columns ) {
$return_var = -1;
$output = array();
exec( 'mode CON', $output, $return_var );
if ( 0 === $return_var && $output ) {
// Look for second line ending in ": <number>" (searching for "Columns:" will fail on non-English locales).
if ( preg_match( '/:\s*[0-9]+\n[^:]+:\s*([0-9]+)\n/', implode( "\n", $output ), $matches ) ) {
$columns = (int) $matches[1];
}
}
}
} else {
if ( ! ( $columns = (int) getenv( 'COLUMNS' ) ) ) {
$size = exec( '/usr/bin/env stty size 2>/dev/null' );
if ( '' !== $size && preg_match( '/[0-9]+ ([0-9]+)/', $size, $matches ) ) {
$columns = (int) $matches[1];
}
if ( ! $columns ) {
if ( getenv( 'TERM' ) ) {
$columns = (int) exec( '/usr/bin/env tput cols 2>/dev/null' );
}
}
}
}
}
if ( ! $columns ) {
$columns = 80; // default width of cmd window on Windows OS
}
}
return $columns;
}
/**
* Checks whether the output of the current script is a TTY or a pipe / redirect
*
* Returns true if STDOUT output is being redirected to a pipe or a file; false is
* output is being sent directly to the terminal.
*
* If an env variable SHELL_PIPE exists, returned result depends it's
* value. Strings like 1, 0, yes, no, that validate to booleans are accepted.
*
* To enable ASCII formatting even when shell is piped, use the
* ENV variable SHELL_PIPE=0
*
* @return bool
*/
static public function isPiped() {
$shellPipe = getenv('SHELL_PIPE');
if ($shellPipe !== false) {
return filter_var($shellPipe, FILTER_VALIDATE_BOOLEAN);
} else {
return (function_exists('posix_isatty') && !posix_isatty(STDOUT));
}
}
/**
* Uses `stty` to hide input/output completely.
* @param boolean $hidden Will hide/show the next data. Defaults to true.
*/
static public function hide($hidden = true) {
system( 'stty ' . ( $hidden? '-echo' : 'echo' ) );
}
/**
* Is this shell in Windows?
*
* @return bool
*/
static private function is_windows() {
return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
}
}
?>
+279
View File
@@ -0,0 +1,279 @@
<?php
namespace cli;
class Streams {
protected static $out = STDOUT;
protected static $in = STDIN;
protected static $err = STDERR;
static function _call( $func, $args ) {
$method = __CLASS__ . '::' . $func;
return call_user_func_array( $method, $args );
}
static public function isTty() {
return (function_exists('posix_isatty') && posix_isatty(static::$out));
}
/**
* Handles rendering strings. If extra scalar arguments are given after the `$msg`
* the string will be rendered with `sprintf`. If the second argument is an `array`
* then each key in the array will be the placeholder name. Placeholders are of the
* format {:key}.
*
* @param string $msg The message to render.
* @param mixed ... Either scalar arguments or a single array argument.
* @return string The rendered string.
*/
public static function render( $msg ) {
$args = func_get_args();
// No string replacement is needed
if( count( $args ) == 1 || ( is_string( $args[1] ) && '' === $args[1] ) ) {
return Colors::shouldColorize() ? Colors::colorize( $msg ) : $msg;
}
// If the first argument is not an array just pass to sprintf
if( !is_array( $args[1] ) ) {
// Colorize the message first so sprintf doesn't bitch at us
if ( Colors::shouldColorize() ) {
$args[0] = Colors::colorize( $args[0] );
}
// Escape percent characters for sprintf
$args[0] = preg_replace('/(%([^\w]|$))/', "%$1", $args[0]);
return call_user_func_array( 'sprintf', $args );
}
// Here we do named replacement so formatting strings are more understandable
foreach( $args[1] as $key => $value ) {
$msg = str_replace( '{:' . $key . '}', $value, $msg );
}
return Colors::shouldColorize() ? Colors::colorize( $msg ) : $msg;
}
/**
* Shortcut for printing to `STDOUT`. The message and parameters are passed
* through `sprintf` before output.
*
* @param string $msg The message to output in `printf` format.
* @param mixed ... Either scalar arguments or a single array argument.
* @return void
* @see \cli\render()
*/
public static function out( $msg ) {
fwrite( static::$out, self::_call( 'render', func_get_args() ) );
}
/**
* Pads `$msg` to the width of the shell before passing to `cli\out`.
*
* @param string $msg The message to pad and pass on.
* @param mixed ... Either scalar arguments or a single array argument.
* @return void
* @see cli\out()
*/
public static function out_padded( $msg ) {
$msg = self::_call( 'render', func_get_args() );
self::out( str_pad( $msg, \cli\Shell::columns() ) );
}
/**
* Prints a message to `STDOUT` with a newline appended. See `\cli\out` for
* more documentation.
*
* @see cli\out()
*/
public static function line( $msg = '' ) {
// func_get_args is empty if no args are passed even with the default above.
$args = array_merge( func_get_args(), array( '' ) );
$args[0] .= "\n";
self::_call( 'out', $args );
}
/**
* Shortcut for printing to `STDERR`. The message and parameters are passed
* through `sprintf` before output.
*
* @param string $msg The message to output in `printf` format. With no string,
* a newline is printed.
* @param mixed ... Either scalar arguments or a single array argument.
* @return void
*/
public static function err( $msg = '' ) {
// func_get_args is empty if no args are passed even with the default above.
$args = array_merge( func_get_args(), array( '' ) );
$args[0] .= "\n";
fwrite( static::$err, self::_call( 'render', $args ) );
}
/**
* Takes input from `STDIN` in the given format. If an end of transmission
* character is sent (^D), an exception is thrown.
*
* @param string $format A valid input format. See `fscanf` for documentation.
* If none is given, all input up to the first newline
* is accepted.
* @param boolean $hide If true will hide what the user types in.
* @return string The input with whitespace trimmed.
* @throws \Exception Thrown if ctrl-D (EOT) is sent as input.
*/
public static function input( $format = null, $hide = false ) {
if ( $hide )
Shell::hide();
if( $format ) {
fscanf( static::$in, $format . "\n", $line );
} else {
$line = fgets( static::$in );
}
if ( $hide ) {
Shell::hide( false );
echo "\n";
}
if( $line === false ) {
throw new \Exception( 'Caught ^D during input' );
}
return trim( $line );
}
/**
* Displays an input prompt. If no default value is provided the prompt will
* continue displaying until input is received.
*
* @param string $question The question to ask the user.
* @param bool|string $default A default value if the user provides no input.
* @param string $marker A string to append to the question and default value
* on display.
* @param boolean $hide Optionally hides what the user types in.
* @return string The users input.
* @see cli\input()
*/
public static function prompt( $question, $default = null, $marker = ': ', $hide = false ) {
if( $default && strpos( $question, '[' ) === false ) {
$question .= ' [' . $default . ']';
}
while( true ) {
self::out( $question . $marker );
$line = self::input( null, $hide );
if ( trim( $line ) !== '' )
return $line;
if( $default !== false )
return $default;
}
}
/**
* Presents a user with a multiple choice question, useful for 'yes/no' type
* questions (which this public static function defaults too).
*
* @param string $question The question to ask the user.
* @param string $choice A string of characters allowed as a response. Case is ignored.
* @param string $default The default choice. NULL if a default is not allowed.
* @return string The users choice.
* @see cli\prompt()
*/
public static function choose( $question, $choice = 'yn', $default = 'n' ) {
if( !is_string( $choice ) ) {
$choice = join( '', $choice );
}
// Make every choice character lowercase except the default
$choice = str_ireplace( $default, strtoupper( $default ), strtolower( $choice ) );
// Seperate each choice with a forward-slash
$choices = trim( join( '/', preg_split( '//', $choice ) ), '/' );
while( true ) {
$line = self::prompt( sprintf( '%s? [%s]', $question, $choices ), $default, '' );
if( stripos( $choice, $line ) !== false ) {
return strtolower( $line );
}
if( !empty( $default ) ) {
return strtolower( $default );
}
}
}
/**
* Displays an array of strings as a menu where a user can enter a number to
* choose an option. The array must be a single dimension with either strings
* or objects with a `__toString()` method.
*
* @param array $items The list of items the user can choose from.
* @param string $default The index of the default item.
* @param string $title The message displayed to the user when prompted.
* @return string The index of the chosen item.
* @see cli\line()
* @see cli\input()
* @see cli\err()
*/
public static function menu( $items, $default = null, $title = 'Choose an item' ) {
$map = array_values( $items );
if( $default && strpos( $title, '[' ) === false && isset( $items[$default] ) ) {
$title .= ' [' . $items[$default] . ']';
}
foreach( $map as $idx => $item ) {
self::line( ' %d. %s', $idx + 1, (string)$item );
}
self::line();
while( true ) {
fwrite( static::$out, sprintf( '%s: ', $title ) );
$line = self::input();
if( is_numeric( $line ) ) {
$line--;
if( isset( $map[$line] ) ) {
return array_search( $map[$line], $items );
}
if( $line < 0 || $line >= count( $map ) ) {
self::err( 'Invalid menu selection: out of range' );
}
} else if( isset( $default ) ) {
return $default;
}
}
}
/**
* Sets one of the streams (input, output, or error) to a `stream` type resource.
*
* Valid $whichStream values are:
* - 'in' (default: STDIN)
* - 'out' (default: STDOUT)
* - 'err' (default: STDERR)
*
* Any custom streams will be closed for you on shutdown, so please don't close stream
* resources used with this method.
*
* @param string $whichStream The stream property to update
* @param resource $stream The new stream resource to use
* @return void
* @throws \Exception Thrown if $stream is not a resource of the 'stream' type.
*/
public static function setStream( $whichStream, $stream ) {
if( !is_resource( $stream ) || get_resource_type( $stream ) !== 'stream' ) {
throw new \Exception( 'Invalid resource type!' );
}
if( property_exists( __CLASS__, $whichStream ) ) {
static::${$whichStream} = $stream;
}
register_shutdown_function( function() use ($stream) {
fclose( $stream );
} );
}
}
+257
View File
@@ -0,0 +1,257 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli;
use cli\Shell;
use cli\Streams;
use cli\table\Ascii;
use cli\table\Renderer;
use cli\table\Tabular;
/**
* The `Table` class is used to display data in a tabular format.
*/
class Table {
protected $_renderer;
protected $_headers = array();
protected $_footers = array();
protected $_width = array();
protected $_rows = array();
/**
* Initializes the `Table` class.
*
* There are 3 ways to instantiate this class:
*
* 1. Pass an array of strings as the first parameter for the column headers
* and a 2-dimensional array as the second parameter for the data rows.
* 2. Pass an array of hash tables (string indexes instead of numerical)
* where each hash table is a row and the indexes of the *first* hash
* table are used as the header values.
* 3. Pass nothing and use `setHeaders()` and `addRow()` or `setRows()`.
*
* @param array $headers Headers used in this table. Optional.
* @param array $rows The rows of data for this table. Optional.
* @param array $footers Footers used in this table. Optional.
*/
public function __construct(array $headers = null, array $rows = null, array $footers = null) {
if (!empty($headers)) {
// If all the rows is given in $headers we use the keys from the
// first row for the header values
if ($rows === null) {
$rows = $headers;
$keys = array_keys(array_shift($headers));
$headers = array();
foreach ($keys as $header) {
$headers[$header] = $header;
}
}
$this->setHeaders($headers);
$this->setRows($rows);
}
if (!empty($footers)) {
$this->setFooters($footers);
}
if (Shell::isPiped()) {
$this->setRenderer(new Tabular());
} else {
$this->setRenderer(new Ascii());
}
}
public function resetTable()
{
$this->_headers = array();
$this->_width = array();
$this->_rows = array();
$this->_footers = array();
return $this;
}
/**
* Sets the renderer used by this table.
*
* @param table\Renderer $renderer The renderer to use for output.
* @see table\Renderer
* @see table\Ascii
* @see table\Tabular
*/
public function setRenderer(Renderer $renderer) {
$this->_renderer = $renderer;
}
/**
* Loops through the row and sets the maximum width for each column.
*
* @param array $row The table row.
* @return array $row
*/
protected function checkRow(array $row) {
foreach ($row as $column => $str) {
$width = Colors::width( $str, $this->isAsciiPreColorized( $column ) );
if (!isset($this->_width[$column]) || $width > $this->_width[$column]) {
$this->_width[$column] = $width;
}
}
return $row;
}
/**
* Output the table to `STDOUT` using `cli\line()`.
*
* If STDOUT is a pipe or redirected to a file, should output simple
* tab-separated text. Otherwise, renders table with ASCII table borders
*
* @uses cli\Shell::isPiped() Determine what format to output
*
* @see cli\Table::renderRow()
*/
public function display() {
foreach( $this->getDisplayLines() as $line ) {
Streams::line( $line );
}
}
/**
* Get the table lines to output.
*
* @see cli\Table::display()
* @see cli\Table::renderRow()
*
* @return array
*/
public function getDisplayLines() {
$this->_renderer->setWidths($this->_width, $fallback = true);
$border = $this->_renderer->border();
$out = array();
if (isset($border)) {
$out[] = $border;
}
$out[] = $this->_renderer->row($this->_headers);
if (isset($border)) {
$out[] = $border;
}
foreach ($this->_rows as $row) {
$row = $this->_renderer->row($row);
$row = explode( PHP_EOL, $row );
$out = array_merge( $out, $row );
}
if (isset($border)) {
$out[] = $border;
}
if ($this->_footers) {
$out[] = $this->_renderer->row($this->_footers);
if (isset($border)) {
$out[] = $border;
}
}
return $out;
}
/**
* Sort the table by a column. Must be called before `cli\Table::display()`.
*
* @param int $column The index of the column to sort by.
*/
public function sort($column) {
if (!isset($this->_headers[$column])) {
trigger_error('No column with index ' . $column, E_USER_NOTICE);
return;
}
usort($this->_rows, function($a, $b) use ($column) {
return strcmp($a[$column], $b[$column]);
});
}
/**
* Set the headers of the table.
*
* @param array $headers An array of strings containing column header names.
*/
public function setHeaders(array $headers) {
$this->_headers = $this->checkRow($headers);
}
/**
* Set the footers of the table.
*
* @param array $footers An array of strings containing column footers names.
*/
public function setFooters(array $footers) {
$this->_footers = $this->checkRow($footers);
}
/**
* Add a row to the table.
*
* @param array $row The row data.
* @see cli\Table::checkRow()
*/
public function addRow(array $row) {
$this->_rows[] = $this->checkRow($row);
}
/**
* Clears all previous rows and adds the given rows.
*
* @param array $rows A 2-dimensional array of row data.
* @see cli\Table::addRow()
*/
public function setRows(array $rows) {
$this->_rows = array();
foreach ($rows as $row) {
$this->addRow($row);
}
}
public function countRows() {
return count($this->_rows);
}
/**
* Set whether items in an Ascii table are pre-colorized.
*
* @param bool|array $precolorized A boolean to set all columns in the table as pre-colorized, or an array of booleans keyed by column index (number) to set individual columns as pre-colorized.
* @see cli\Ascii::setPreColorized()
*/
public function setAsciiPreColorized( $pre_colorized ) {
if ( $this->_renderer instanceof Ascii ) {
$this->_renderer->setPreColorized( $pre_colorized );
}
}
/**
* Is a column in an Ascii table pre-colorized?
*
* @param int $column Column index to check.
* @return bool True if whole Ascii table is marked as pre-colorized, or if the individual column is pre-colorized; else false.
* @see cli\Ascii::isPreColorized()
*/
private function isAsciiPreColorized( $column ) {
if ( $this->_renderer instanceof Ascii ) {
return $this->_renderer->isPreColorized( $column );
}
return false;
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author Ryan Sullivan <rsullivan@connectstudios.com>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli;
/**
* The `Tree` class is used to display data in a tree-like format.
*/
class Tree {
protected $_renderer;
protected $_data = array();
/**
* Sets the renderer used by this tree.
*
* @param tree\Renderer $renderer The renderer to use for output.
* @see tree\Renderer
* @see tree\Ascii
* @see tree\Markdown
*/
public function setRenderer(tree\Renderer $renderer) {
$this->_renderer = $renderer;
}
/**
* Set the data.
* Format:
* [
* 'Label' => [
* 'Thing' => ['Thing'],
* ],
* 'Thing',
* ]
* @param array $data
*/
public function setData(array $data)
{
$this->_data = $data;
}
/**
* Render the tree and return it as a string.
*
* @return string|null
*/
public function render()
{
return $this->_renderer->render($this->_data);
}
/**
* Display the rendered tree
*/
public function display()
{
echo $this->render();
}
}
@@ -0,0 +1,137 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli\arguments;
use cli\Memoize;
/**
* Represents an Argument or a value and provides several helpers related to parsing an argument list.
*/
class Argument extends Memoize {
/**
* The canonical name of this argument, used for aliasing.
*
* @param string
*/
public $key;
private $_argument;
private $_raw;
/**
* @param string $argument The raw argument, leading dashes included.
*/
public function __construct($argument) {
$this->_raw = $argument;
$this->key =& $this->_argument;
if ($this->isLong) {
$this->_argument = substr($this->_raw, 2);
} else if ($this->isShort) {
$this->_argument = substr($this->_raw, 1);
} else {
$this->_argument = $this->_raw;
}
}
/**
* Returns the raw input as a string.
*
* @return string
*/
public function __toString() {
return (string)$this->_raw;
}
/**
* Returns the formatted argument string.
*
* @return string
*/
public function value() {
return $this->_argument;
}
/**
* Returns the raw input.
*
* @return mixed
*/
public function raw() {
return $this->_raw;
}
/**
* Returns true if the string matches the pattern for long arguments.
*
* @return bool
*/
public function isLong() {
return (0 == strncmp($this->_raw, '--', 2));
}
/**
* Returns true if the string matches the pattern for short arguments.
*
* @return bool
*/
public function isShort() {
return !$this->isLong && (0 == strncmp($this->_raw, '-', 1));
}
/**
* Returns true if the string matches the pattern for arguments.
*
* @return bool
*/
public function isArgument() {
return $this->isShort() || $this->isLong();
}
/**
* Returns true if the string matches the pattern for values.
*
* @return bool
*/
public function isValue() {
return !$this->isArgument;
}
/**
* Returns true if the argument is short but contains several characters. Each
* character is considered a separate argument.
*
* @return bool
*/
public function canExplode() {
return $this->isShort && strlen($this->_argument) > 1;
}
/**
* Returns all but the first character of the argument, removing them from the
* objects representation at the same time.
*
* @return array
*/
public function exploded() {
$exploded = array();
for ($i = strlen($this->_argument); $i > 0; $i--) {
array_push($exploded, $this->_argument[$i - 1]);
}
$this->_argument = array_pop($exploded);
$this->_raw = '-' . $this->_argument;
return $exploded;
}
}
@@ -0,0 +1,122 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli\arguments;
use cli\Arguments;
/**
* Arguments help screen renderer
*/
class HelpScreen {
protected $_flags = array();
protected $_maxFlag = 0;
protected $_options = array();
protected $_maxOption = 0;
public function __construct(Arguments $arguments) {
$this->setArguments($arguments);
}
public function __toString() {
return $this->render();
}
public function setArguments(Arguments $arguments) {
$this->consumeArgumentFlags($arguments);
$this->consumeArgumentOptions($arguments);
}
public function consumeArgumentFlags(Arguments $arguments) {
$data = $this->_consume($arguments->getFlags());
$this->_flags = $data[0];
$this->_flagMax = $data[1];
}
public function consumeArgumentOptions(Arguments $arguments) {
$data = $this->_consume($arguments->getOptions());
$this->_options = $data[0];
$this->_optionMax = $data[1];
}
public function render() {
$help = array();
array_push($help, $this->_renderFlags());
array_push($help, $this->_renderOptions());
return join($help, "\n\n");
}
private function _renderFlags() {
if (empty($this->_flags)) {
return null;
}
return "Flags\n" . $this->_renderScreen($this->_flags, $this->_flagMax);
}
private function _renderOptions() {
if (empty($this->_options)) {
return null;
}
return "Options\n" . $this->_renderScreen($this->_options, $this->_optionMax);
}
private function _renderScreen($options, $max) {
$help = array();
foreach ($options as $option => $settings) {
$formatted = ' ' . str_pad($option, $max);
$dlen = 80 - 4 - $max;
$description = str_split($settings['description'], $dlen);
$formatted.= ' ' . array_shift($description);
if ($settings['default']) {
$formatted .= ' [default: ' . $settings['default'] . ']';
}
$pad = str_repeat(' ', $max + 3);
while ($desc = array_shift($description)) {
$formatted .= "\n${pad}${desc}";
}
array_push($help, $formatted);
}
return join($help, "\n");
}
private function _consume($options) {
$max = 0;
$out = array();
foreach ($options as $option => $settings) {
$names = array('--' . $option);
foreach ($settings['aliases'] as $alias) {
array_push($names, '-' . $alias);
}
$names = join($names, ', ');
$max = max(strlen($names), $max);
$out[$names] = $settings;
}
return array($out, $max);
}
}
@@ -0,0 +1,43 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli\arguments;
/**
* Thrown when undefined arguments are detected in strict mode.
*/
class InvalidArguments extends \InvalidArgumentException {
protected $arguments;
/**
* @param array $arguments A list of arguments that do not fit the profile.
*/
public function __construct(array $arguments) {
$this->arguments = $arguments;
$this->message = $this->_generateMessage();
}
/**
* Get the arguments that caused the exception.
*
* @return array
*/
public function getArguments() {
return $this->arguments;
}
private function _generateMessage() {
return 'unknown argument' .
(count($this->arguments) > 1 ? 's' : '') .
': ' . join($this->arguments, ', ');
}
}
+123
View File
@@ -0,0 +1,123 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli\arguments;
use cli\Memoize;
class Lexer extends Memoize implements \Iterator {
private $_items = array();
private $_index = 0;
private $_length = 0;
private $_first = true;
/**
* @param array $items A list of strings to process as tokens.
*/
public function __construct(array $items) {
$this->_items = $items;
$this->_length = count($items);
}
/**
* The current token.
*
* @return string
*/
public function current() {
return $this->_item;
}
/**
* Peek ahead to the next token without moving the cursor.
*
* @return Argument
*/
public function peek() {
return new Argument($this->_items[0]);
}
/**
* Move the cursor forward 1 element if it is valid.
*/
public function next() {
if ($this->valid()) {
$this->_shift();
}
}
/**
* Return the current position of the cursor.
*
* @return int
*/
public function key() {
return $this->_index;
}
/**
* Move forward 1 element and, if the method hasn't been called before, reset
* the cursor's position to 0.
*/
public function rewind() {
$this->_shift();
if ($this->_first) {
$this->_index = 0;
$this->_first = false;
}
}
/**
* Returns true if the cursor has not reached the end of the list.
*
* @return bool
*/
public function valid() {
return ($this->_index < $this->_length);
}
/**
* Push an element to the front of the stack.
*
* @param mixed $item The value to set
*/
public function unshift($item) {
array_unshift($this->_items, $item);
$this->_length += 1;
}
/**
* Returns true if the cursor is at the end of the list.
*
* @return bool
*/
public function end() {
return ($this->_index + 1) == $this->_length;
}
private function _shift() {
$this->_item = new Argument(array_shift($this->_items));
$this->_index += 1;
$this->_explode();
$this->_unmemo('peek');
}
private function _explode() {
if (!$this->_item->canExplode) {
return false;
}
foreach ($this->_item->exploded as $piece) {
$this->unshift('-' . $piece);
}
}
}
+415
View File
@@ -0,0 +1,415 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli;
/**
* Handles rendering strings. If extra scalar arguments are given after the `$msg`
* the string will be rendered with `sprintf`. If the second argument is an `array`
* then each key in the array will be the placeholder name. Placeholders are of the
* format {:key}.
*
* @param string $msg The message to render.
* @param mixed ... Either scalar arguments or a single array argument.
* @return string The rendered string.
*/
function render( $msg ) {
return Streams::_call( 'render', func_get_args() );
}
/**
* Shortcut for printing to `STDOUT`. The message and parameters are passed
* through `sprintf` before output.
*
* @param string $msg The message to output in `printf` format.
* @param mixed ... Either scalar arguments or a single array argument.
* @return void
* @see \cli\render()
*/
function out( $msg ) {
Streams::_call( 'out', func_get_args() );
}
/**
* Pads `$msg` to the width of the shell before passing to `cli\out`.
*
* @param string $msg The message to pad and pass on.
* @param mixed ... Either scalar arguments or a single array argument.
* @return void
* @see cli\out()
*/
function out_padded( $msg ) {
Streams::_call( 'out_padded', func_get_args() );
}
/**
* Prints a message to `STDOUT` with a newline appended. See `\cli\out` for
* more documentation.
*
* @see cli\out()
*/
function line( $msg = '' ) {
Streams::_call( 'line', func_get_args() );
}
/**
* Shortcut for printing to `STDERR`. The message and parameters are passed
* through `sprintf` before output.
*
* @param string $msg The message to output in `printf` format. With no string,
* a newline is printed.
* @param mixed ... Either scalar arguments or a single array argument.
* @return void
*/
function err( $msg = '' ) {
Streams::_call( 'err', func_get_args() );
}
/**
* Takes input from `STDIN` in the given format. If an end of transmission
* character is sent (^D), an exception is thrown.
*
* @param string $format A valid input format. See `fscanf` for documentation.
* If none is given, all input up to the first newline
* is accepted.
* @return string The input with whitespace trimmed.
* @throws \Exception Thrown if ctrl-D (EOT) is sent as input.
*/
function input( $format = null ) {
return Streams::input( $format );
}
/**
* Displays an input prompt. If no default value is provided the prompt will
* continue displaying until input is received.
*
* @param string $question The question to ask the user.
* @param string $default A default value if the user provides no input.
* @param string $marker A string to append to the question and default value on display.
* @param boolean $hide If the user input should be hidden
* @return string The users input.
* @see cli\input()
*/
function prompt( $question, $default = false, $marker = ': ', $hide = false ) {
return Streams::prompt( $question, $default, $marker, $hide );
}
/**
* Presents a user with a multiple choice question, useful for 'yes/no' type
* questions (which this function defaults too).
*
* @param string $question The question to ask the user.
* @param string $choice
* @param string|null $default The default choice. NULL if a default is not allowed.
* @internal param string $valid A string of characters allowed as a response. Case
* is ignored.
* @return string The users choice.
* @see cli\prompt()
*/
function choose( $question, $choice = 'yn', $default = 'n' ) {
return Streams::choose( $question, $choice, $default );
}
/**
* Does the same as {@see choose()}, but always asks yes/no and returns a boolean
*
* @param string $question The question to ask the user.
* @param bool|null $default The default choice, in a boolean format.
* @return bool
*/
function confirm( $question, $default = false ) {
if ( is_bool( $default ) ) {
$default = $default? 'y' : 'n';
}
$result = choose( $question, 'yn', $default );
return $result == 'y';
}
/**
* Displays an array of strings as a menu where a user can enter a number to
* choose an option. The array must be a single dimension with either strings
* or objects with a `__toString()` method.
*
* @param array $items The list of items the user can choose from.
* @param string $default The index of the default item.
* @param string $title The message displayed to the user when prompted.
* @return string The index of the chosen item.
* @see cli\line()
* @see cli\input()
* @see cli\err()
*/
function menu( $items, $default = null, $title = 'Choose an item' ) {
return Streams::menu( $items, $default, $title );
}
/**
* Attempts an encoding-safe way of getting string length. If intl extension or PCRE with '\X' or mb_string extension aren't
* available, falls back to basic strlen.
*
* @param string $str The string to check.
* @param string|bool $encoding Optional. The encoding of the string. Default false.
* @return int Numeric value that represents the string's length
*/
function safe_strlen( $str, $encoding = false ) {
// Allow for selective testings - "1" bit set tests grapheme_strlen(), "2" preg_match_all( '/\X/u' ), "4" mb_strlen(), "other" strlen().
$test_safe_strlen = getenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN' );
// Assume UTF-8 if no encoding given - `grapheme_strlen()` will return null if given non-UTF-8 string.
if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_icu() && null !== ( $length = grapheme_strlen( $str ) ) ) {
if ( ! $test_safe_strlen || ( $test_safe_strlen & 1 ) ) {
return $length;
}
}
// Assume UTF-8 if no encoding given - `preg_match_all()` will return false if given non-UTF-8 string.
if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_pcre_x() && false !== ( $length = preg_match_all( '/\X/u', $str, $dummy /*needed for PHP 5.3*/ ) ) ) {
if ( ! $test_safe_strlen || ( $test_safe_strlen & 2 ) ) {
return $length;
}
}
// Legacy encodings and old PHPs will reach here.
if ( function_exists( 'mb_strlen' ) && ( $encoding || function_exists( 'mb_detect_encoding' ) ) ) {
if ( ! $encoding ) {
$encoding = mb_detect_encoding( $str, null, true /*strict*/ );
}
$length = $encoding ? mb_strlen( $str, $encoding ) : mb_strlen( $str ); // mbstring funcs can fail if given `$encoding` arg that evals to false.
if ( 'UTF-8' === $encoding ) {
// Subtract combining characters.
$length -= preg_match_all( get_unicode_regexs( 'm' ), $str, $dummy /*needed for PHP 5.3*/ );
}
if ( ! $test_safe_strlen || ( $test_safe_strlen & 4 ) ) {
return $length;
}
}
return strlen( $str );
}
/**
* Attempts an encoding-safe way of getting a substring. If intl extension or PCRE with '\X' or mb_string extension aren't
* available, falls back to substr().
*
* @param string $str The input string.
* @param int $start The starting position of the substring.
* @param int|bool|null $length Optional, unless $is_width is set. Maximum length of the substring. Default false. Negative not supported.
* @param int|bool $is_width Optional. If set and encoding is UTF-8, $length (which must be specified) is interpreted as spacing width. Default false.
* @param string|bool $encoding Optional. The encoding of the string. Default false.
* @return bool|string False if given unsupported args, otherwise substring of string specified by start and length parameters
*/
function safe_substr( $str, $start, $length = false, $is_width = false, $encoding = false ) {
// Negative $length or $is_width and $length not specified not supported.
if ( $length < 0 || ( $is_width && ( null === $length || false === $length ) ) ) {
return false;
}
// Need this for normalization below and other uses.
$safe_strlen = safe_strlen( $str, $encoding );
// Normalize `$length` when not specified - PHP 5.3 substr takes false as full length, PHP > 5.3 takes null.
if ( null === $length || false === $length ) {
$length = $safe_strlen;
}
// Normalize `$start` - various methods treat this differently.
if ( $start > $safe_strlen ) {
return '';
}
if ( $start < 0 && -$start > $safe_strlen ) {
$start = 0;
}
// Allow for selective testings - "1" bit set tests grapheme_substr(), "2" preg_split( '/\X/' ), "4" mb_substr(), "8" substr().
$test_safe_substr = getenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR' );
// Assume UTF-8 if no encoding given - `grapheme_substr()` will return false (not null like `grapheme_strlen()`) if given non-UTF-8 string.
if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_icu() && false !== ( $try = grapheme_substr( $str, $start, $length ) ) ) {
if ( ! $test_safe_substr || ( $test_safe_substr & 1 ) ) {
return $is_width ? _safe_substr_eaw( $try, $length ) : $try;
}
}
// Assume UTF-8 if no encoding given - `preg_split()` returns a one element array if given non-UTF-8 string (PHP bug) so need to check `preg_last_error()`.
if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_pcre_x() ) {
if ( false !== ( $try = preg_split( '/(\X)/u', $str, $safe_strlen + 1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ) ) && ! preg_last_error() ) {
$try = implode( '', array_slice( $try, $start, $length ) );
if ( ! $test_safe_substr || ( $test_safe_substr & 2 ) ) {
return $is_width ? _safe_substr_eaw( $try, $length ) : $try;
}
}
}
// Legacy encodings and old PHPs will reach here.
if ( function_exists( 'mb_substr' ) && ( $encoding || function_exists( 'mb_detect_encoding' ) ) ) {
if ( ! $encoding ) {
$encoding = mb_detect_encoding( $str, null, true /*strict*/ );
}
// Bug: not adjusting for combining chars.
$try = $encoding ? mb_substr( $str, $start, $length, $encoding ) : mb_substr( $str, $start, $length ); // mbstring funcs can fail if given `$encoding` arg that evals to false.
if ( 'UTF-8' === $encoding && $is_width ) {
$try = _safe_substr_eaw( $try, $length );
}
if ( ! $test_safe_substr || ( $test_safe_substr & 4 ) ) {
return $try;
}
}
return substr( $str, $start, $length );
}
/**
* Internal function used by `safe_substr()` to adjust for East Asian double-width chars.
*
* @return string
*/
function _safe_substr_eaw( $str, $length ) {
// Set the East Asian Width regex.
$eaw_regex = get_unicode_regexs( 'eaw' );
// If there's any East Asian double-width chars...
if ( preg_match( $eaw_regex, $str ) ) {
// Note that if the length ends in the middle of a double-width char, the char is excluded, not included.
// See if it's all EAW.
if ( function_exists( 'mb_substr' ) && preg_match_all( $eaw_regex, $str, $dummy /*needed for PHP 5.3*/ ) === $length ) {
// Just halve the length so (rounded down to a minimum of 1).
$str = mb_substr( $str, 0, max( (int) ( $length / 2 ), 1 ), 'UTF-8' );
} else {
// Explode string into an array of UTF-8 chars. Based on core `_mb_substr()` in "wp-includes/compat.php".
$chars = preg_split( '/([\x00-\x7f\xc2-\xf4][^\x00-\x7f\xc2-\xf4]*)/', $str, $length + 1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY );
$cnt = min( count( $chars ), $length );
$width = $length;
for ( $length = 0; $length < $cnt && $width > 0; $length++ ) {
$width -= preg_match( $eaw_regex, $chars[ $length ] ) ? 2 : 1;
}
// Round down to a minimum of 1.
if ( $width < 0 && $length > 1 ) {
$length--;
}
return join( '', array_slice( $chars, 0, $length ) );
}
}
return $str;
}
/**
* An encoding-safe way of padding string length for display
*
* @param string $string The string to pad.
* @param int $length The length to pad it to.
* @param string|bool $encoding Optional. The encoding of the string. Default false.
* @return string
*/
function safe_str_pad( $string, $length, $encoding = false ) {
$real_length = strwidth( $string, $encoding );
$diff = strlen( $string ) - $real_length;
$length += $diff;
return str_pad( $string, $length );
}
/**
* Get width of string, ie length in characters, taking into account multi-byte and mark characters for UTF-8, and multi-byte for non-UTF-8.
*
* @param string $string The string to check.
* @param string|bool $encoding Optional. The encoding of the string. Default false.
* @return int The string's width.
*/
function strwidth( $string, $encoding = false ) {
// Set the East Asian Width and Mark regexs.
list( $eaw_regex, $m_regex ) = get_unicode_regexs();
// Allow for selective testings - "1" bit set tests grapheme_strlen(), "2" preg_match_all( '/\X/u' ), "4" mb_strwidth(), "other" safe_strlen().
$test_strwidth = getenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH' );
// Assume UTF-8 if no encoding given - `grapheme_strlen()` will return null if given non-UTF-8 string.
if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_icu() && null !== ( $width = grapheme_strlen( $string ) ) ) {
if ( ! $test_strwidth || ( $test_strwidth & 1 ) ) {
return $width + preg_match_all( $eaw_regex, $string, $dummy /*needed for PHP 5.3*/ );
}
}
// Assume UTF-8 if no encoding given - `preg_match_all()` will return false if given non-UTF-8 string.
if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_pcre_x() && false !== ( $width = preg_match_all( '/\X/u', $string, $dummy /*needed for PHP 5.3*/ ) ) ) {
if ( ! $test_strwidth || ( $test_strwidth & 2 ) ) {
return $width + preg_match_all( $eaw_regex, $string, $dummy /*needed for PHP 5.3*/ );
}
}
// Legacy encodings and old PHPs will reach here.
if ( function_exists( 'mb_strwidth' ) && ( $encoding || function_exists( 'mb_detect_encoding' ) ) ) {
if ( ! $encoding ) {
$encoding = mb_detect_encoding( $string, null, true /*strict*/ );
}
$width = $encoding ? mb_strwidth( $string, $encoding ) : mb_strwidth( $string ); // mbstring funcs can fail if given `$encoding` arg that evals to false.
if ( 'UTF-8' === $encoding ) {
// Subtract combining characters.
$width -= preg_match_all( $m_regex, $string, $dummy /*needed for PHP 5.3*/ );
}
if ( ! $test_strwidth || ( $test_strwidth & 4 ) ) {
return $width;
}
}
return safe_strlen( $string, $encoding );
}
/**
* Returns whether ICU is modern enough not to flake out.
*
* @return bool
*/
function can_use_icu() {
static $can_use_icu = null;
if ( null === $can_use_icu ) {
// Choosing ICU 54, Unicode 7.0.
$can_use_icu = defined( 'INTL_ICU_VERSION' ) && version_compare( INTL_ICU_VERSION, '54.1', '>=' ) && function_exists( 'grapheme_strlen' ) && function_exists( 'grapheme_substr' );
}
return $can_use_icu;
}
/**
* Returns whether PCRE Unicode extended grapheme cluster '\X' is available for use.
*
* @return bool
*/
function can_use_pcre_x() {
static $can_use_pcre_x = null;
if ( null === $can_use_pcre_x ) {
// '\X' introduced (as Unicde extended grapheme cluster) in PCRE 8.32 - see https://vcs.pcre.org/pcre/code/tags/pcre-8.32/ChangeLog?view=markup line 53.
// Older versions of PCRE were bundled with PHP <= 5.3.23 & <= 5.4.13.
$pcre_version = substr( PCRE_VERSION, 0, strspn( PCRE_VERSION, '0123456789.' ) ); // Remove any trailing date stuff.
$can_use_pcre_x = version_compare( $pcre_version, '8.32', '>=' ) && false !== @preg_match( '/\X/u', '' );
}
return $can_use_pcre_x;
}
/**
* Get the regexs generated from Unicode data.
*
* @param string $idx Optional. Return a specific regex only. Default null.
* @return array|string Returns keyed array if not given $idx or $idx doesn't exist, otherwise the specific regex string.
*/
function get_unicode_regexs( $idx = null ) {
static $eaw_regex; // East Asian Width regex. Characters that count as 2 characters as they're "wide" or "fullwidth". See http://www.unicode.org/reports/tr11/tr11-19.html
static $m_regex; // Mark characters regex (Unicode property "M") - mark combining "Mc", mark enclosing "Me" and mark non-spacing "Mn" chars that should be ignored for spacing purposes.
if ( null === $eaw_regex ) {
// Load both regexs generated from Unicode data.
require __DIR__ . '/unicode/regex.php';
}
if ( null !== $idx ) {
if ( 'eaw' === $idx ) {
return $eaw_regex;
}
if ( 'm' === $idx ) {
return $m_regex;
}
}
return array( $eaw_regex, $m_regex, );
}
+66
View File
@@ -0,0 +1,66 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli\notify;
use cli\Notify;
use cli\Streams;
/**
* A Notifer that displays a string of periods.
*/
class Dots extends Notify {
protected $_dots;
protected $_format = '{:msg}{:dots} ({:elapsed}, {:speed}/s)';
protected $_iteration;
/**
* Instatiates a Notification object.
*
* @param string $msg The text to display next to the Notifier.
* @param int $dots The number of dots to iterate through.
* @param int $interval The interval in milliseconds between updates.
* @throws \InvalidArgumentException
*/
public function __construct($msg, $dots = 3, $interval = 100) {
parent::__construct($msg, $interval);
$this->_dots = (int)$dots;
if ($this->_dots <= 0) {
throw new \InvalidArgumentException('Dot count out of range, must be positive.');
}
}
/**
* Prints the correct number of dots to `STDOUT` with the time elapsed and
* tick speed.
*
* @param boolean $finish `true` if this was called from
* `cli\Notify::finish()`, `false` otherwise.
* @see cli\out_padded()
* @see cli\Notify::formatTime()
* @see cli\Notify::speed()
*/
public function display($finish = false) {
$repeat = $this->_dots;
if (!$finish) {
$repeat = $this->_iteration++ % $repeat;
}
$msg = $this->_message;
$dots = str_pad(str_repeat('.', $repeat), $this->_dots);
$speed = number_format(round($this->speed()));
$elapsed = $this->formatTime($this->elapsed());
Streams::out_padded($this->_format, compact('msg', 'dots', 'speed', 'elapsed'));
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli\notify;
use cli\Notify;
use cli\Streams;
/**
* The `Spinner` Notifier displays an ASCII spinner.
*/
class Spinner extends Notify {
protected $_chars = '-\|/';
protected $_format = '{:msg} {:char} ({:elapsed}, {:speed}/s)';
protected $_iteration = 0;
/**
* Prints the current spinner position to `STDOUT` with the time elapsed
* and tick speed.
*
* @param boolean $finish `true` if this was called from
* `cli\Notify::finish()`, `false` otherwise.
* @see cli\out_padded()
* @see cli\Notify::formatTime()
* @see cli\Notify::speed()
*/
public function display($finish = false) {
$msg = $this->_message;
$idx = $this->_iteration++ % strlen($this->_chars);
$char = $this->_chars[$idx];
$speed = number_format(round($this->speed()));
$elapsed = $this->formatTime($this->elapsed());
Streams::out_padded($this->_format, compact('msg', 'char', 'elapsed', 'speed'));
}
}
+85
View File
@@ -0,0 +1,85 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli\progress;
use cli;
use cli\Notify;
use cli\Progress;
use cli\Shell;
use cli\Streams;
/**
* Displays a progress bar spanning the entire shell.
*
* Basic format:
*
* ^MSG PER% [======================= ] 00:00 / 00:00$
*/
class Bar extends Progress {
protected $_bars = '=>';
protected $_formatMessage = '{:msg} {:percent}% [';
protected $_formatTiming = '] {:elapsed} / {:estimated}';
protected $_format = '{:msg}{:bar}{:timing}';
/**
* Prints the progress bar to the screen with percent complete, elapsed time
* and estimated total time.
*
* @param boolean $finish `true` if this was called from
* `cli\Notify::finish()`, `false` otherwise.
* @see cli\out()
* @see cli\Notify::formatTime()
* @see cli\Notify::elapsed()
* @see cli\Progress::estimated();
* @see cli\Progress::percent()
* @see cli\Shell::columns()
*/
public function display($finish = false) {
$_percent = $this->percent();
$percent = str_pad(floor($_percent * 100), 3);
$msg = $this->_message;
$msg = Streams::render($this->_formatMessage, compact('msg', 'percent'));
$estimated = $this->formatTime($this->estimated());
$elapsed = str_pad($this->formatTime($this->elapsed()), strlen($estimated));
$timing = Streams::render($this->_formatTiming, compact('elapsed', 'estimated'));
$size = Shell::columns();
$size -= strlen($msg . $timing);
if ( $size < 0 ) {
$size = 0;
}
$bar = str_repeat($this->_bars[0], floor($_percent * $size)) . $this->_bars[1];
// substr is needed to trim off the bar cap at 100%
$bar = substr(str_pad($bar, $size, ' '), 0, $size);
Streams::out($this->_format, compact('msg', 'bar', 'timing'));
}
/**
* This method augments the base definition from cli\Notify to optionally
* allow passing a new message.
*
* @param int $increment The amount to increment by.
* @param string $msg The text to display next to the Notifier. (optional)
* @see cli\Notify::tick()
*/
public function tick($increment = 1, $msg = null) {
if ($msg) {
$this->_message = $msg;
}
Notify::tick($increment);
}
}
+227
View File
@@ -0,0 +1,227 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli\table;
use cli\Colors;
use cli\Shell;
/**
* The ASCII renderer renders tables with ASCII borders.
*/
class Ascii extends Renderer {
protected $_characters = array(
'corner' => '+',
'line' => '-',
'border' => '|',
'padding' => ' ',
);
protected $_border = null;
protected $_constraintWidth = null;
protected $_pre_colorized = false;
/**
* Set the widths of each column in the table.
*
* @param array $widths The widths of the columns.
* @param bool $fallback Whether to use these values as fallback only.
*/
public function setWidths(array $widths, $fallback = false) {
if ($fallback) {
foreach ( $this->_widths as $index => $value ) {
$widths[$index] = $value;
}
}
$this->_widths = $widths;
if ( is_null( $this->_constraintWidth ) ) {
$this->_constraintWidth = (int) Shell::columns();
}
$col_count = count( $widths );
$col_borders_count = $col_count ? ( ( $col_count - 1 ) * strlen( $this->_characters['border'] ) ) : 0;
$table_borders_count = strlen( $this->_characters['border'] ) * 2;
$col_padding_count = $col_count * strlen( $this->_characters['padding'] ) * 2;
$max_width = $this->_constraintWidth - $col_borders_count - $table_borders_count - $col_padding_count;
if ( $widths && $max_width && array_sum( $widths ) > $max_width ) {
$avg = floor( $max_width / count( $widths ) );
$resize_widths = array();
$extra_width = 0;
foreach( $widths as $width ) {
if ( $width > $avg ) {
$resize_widths[] = $width;
} else {
$extra_width = $extra_width + ( $avg - $width );
}
}
if ( ! empty( $resize_widths ) && $extra_width ) {
$avg_extra_width = floor( $extra_width / count( $resize_widths ) );
foreach( $widths as &$width ) {
if ( in_array( $width, $resize_widths ) ) {
$width = $avg + $avg_extra_width;
array_shift( $resize_widths );
// Last item gets the cake
if ( empty( $resize_widths ) ) {
$width = 0; // Zero it so not in sum.
$width = $max_width - array_sum( $widths );
}
}
}
}
}
$this->_widths = $widths;
}
/**
* Set the constraint width for the table
*
* @param int $constraintWidth
*/
public function setConstraintWidth( $constraintWidth ) {
$this->_constraintWidth = $constraintWidth;
}
/**
* Set the characters used for rendering the Ascii table.
*
* The keys `corner`, `line` and `border` are used in rendering.
*
* @param $characters array Characters used in rendering.
*/
public function setCharacters(array $characters) {
$this->_characters = array_merge($this->_characters, $characters);
}
/**
* Render a border for the top and bottom and separating the headers from the
* table rows.
*
* @return string The table border.
*/
public function border() {
if (!isset($this->_border)) {
$this->_border = $this->_characters['corner'];
foreach ($this->_widths as $width) {
$this->_border .= str_repeat($this->_characters['line'], $width + 2);
$this->_border .= $this->_characters['corner'];
}
}
return $this->_border;
}
/**
* Renders a row for output.
*
* @param array $row The table row.
* @return string The formatted table row.
*/
public function row( array $row ) {
$extra_row_count = 0;
if ( count( $row ) > 0 ) {
$extra_rows = array_fill( 0, count( $row ), array() );
foreach( $row as $col => $value ) {
$value = str_replace( array( "\r\n", "\n" ), ' ', $value );
$col_width = $this->_widths[ $col ];
$encoding = function_exists( 'mb_detect_encoding' ) ? mb_detect_encoding( $value, null, true /*strict*/ ) : false;
$original_val_width = Colors::width( $value, self::isPreColorized( $col ), $encoding );
if ( $col_width && $original_val_width > $col_width ) {
$row[ $col ] = \cli\safe_substr( $value, 0, $col_width, true /*is_width*/, $encoding );
$value = \cli\safe_substr( $value, \cli\safe_strlen( $row[ $col ], $encoding ), null /*length*/, false /*is_width*/, $encoding );
$i = 0;
do {
$extra_value = \cli\safe_substr( $value, 0, $col_width, true /*is_width*/, $encoding );
$val_width = Colors::width( $extra_value, self::isPreColorized( $col ), $encoding );
if ( $val_width ) {
$extra_rows[ $col ][] = $extra_value;
$value = \cli\safe_substr( $value, \cli\safe_strlen( $extra_value, $encoding ), null /*length*/, false /*is_width*/, $encoding );
$i++;
if ( $i > $extra_row_count ) {
$extra_row_count = $i;
}
}
} while( $value );
}
}
}
$row = array_map(array($this, 'padColumn'), $row, array_keys($row));
array_unshift($row, ''); // First border
array_push($row, ''); // Last border
$ret = join($this->_characters['border'], $row);
if ( $extra_row_count ) {
foreach( $extra_rows as $col => $col_values ) {
while( count( $col_values ) < $extra_row_count ) {
$col_values[] = '';
}
}
do {
$row_values = array();
$has_more = false;
foreach( $extra_rows as $col => &$col_values ) {
$row_values[ $col ] = array_shift( $col_values );
if ( count( $col_values ) ) {
$has_more = true;
}
}
$row_values = array_map(array($this, 'padColumn'), $row_values, array_keys($row_values));
array_unshift($row_values, ''); // First border
array_push($row_values, ''); // Last border
$ret .= PHP_EOL . join($this->_characters['border'], $row_values);
} while( $has_more );
}
return $ret;
}
private function padColumn($content, $column) {
return $this->_characters['padding'] . Colors::pad( $content, $this->_widths[ $column ], $this->isPreColorized( $column ) ) . $this->_characters['padding'];
}
/**
* Set whether items are pre-colorized.
*
* @param bool|array $colorized A boolean to set all columns in the table as pre-colorized, or an array of booleans keyed by column index (number) to set individual columns as pre-colorized.
*/
public function setPreColorized( $pre_colorized ) {
$this->_pre_colorized = $pre_colorized;
}
/**
* Is a column pre-colorized?
*
* @param int $column Column index to check.
* @return bool True if whole table is marked as pre-colorized, or if the individual column is pre-colorized; else false.
*/
public function isPreColorized( $column ) {
if ( is_bool( $this->_pre_colorized ) ) {
return $this->_pre_colorized;
}
if ( is_array( $this->_pre_colorized ) && isset( $this->_pre_colorized[ $column ] ) ) {
return $this->_pre_colorized[ $column ];
}
return false;
}
}
+57
View File
@@ -0,0 +1,57 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace cli\table;
/**
* Table renderers are used to change how a table is displayed.
*/
abstract class Renderer {
protected $_widths = array();
public function __construct(array $widths = array()) {
$this->setWidths($widths);
}
/**
* Set the widths of each column in the table.
*
* @param array $widths The widths of the columns.
* @param bool $fallback Whether to use these values as fallback only.
*/
public function setWidths(array $widths, $fallback = false) {
if ($fallback) {
foreach ( $this->_widths as $index => $value ) {
$widths[$index] = $value;
}
}
$this->_widths = $widths;
}
/**
* Render a border for the top and bottom and separating the headers from the
* table rows.
*
* @return string The table border.
*/
public function border() {
return null;
}
/**
* Renders a row for output.
*
* @param array $row The table row.
* @return string The formatted table row.
*/
abstract public function row(array $row);
}

Some files were not shown because too many files have changed in this diff Show More