基础代码
This commit is contained in:
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
# Configuration file for nektos/act.
|
||||
# See https://github.com/nektos/act#configuration
|
||||
-P ubuntu-latest=shivammathur/node:latest
|
||||
+18
@@ -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
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
* @wp-cli/committers
|
||||
@@ -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.
|
||||
|
||||
-->
|
||||
@@ -0,0 +1,9 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: composer
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
||||
labels:
|
||||
- scope:distribution
|
||||
+19
@@ -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
@@ -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
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
@@ -0,0 +1,8 @@
|
||||
Contributing
|
||||
============
|
||||
|
||||
We appreciate you taking the initiative to contribute to this project.
|
||||
|
||||
Contributing isn’t 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.
|
||||
Vendored
+21
@@ -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.
|
||||
Vendored
+196
@@ -0,0 +1,196 @@
|
||||
wp-cli/i18n-command
|
||||
===================
|
||||
|
||||
Provides internationalization tools for WordPress projects.
|
||||
|
||||
[](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 isn’t 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 you’ve found a bug? We’d 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 there’s an existing resolution to it, or if it’s already been fixed in a newer version.
|
||||
|
||||
Once you’ve done a bit of searching and discovered there isn’t 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
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
"""
|
||||
+2773
File diff suppressed because it is too large
Load Diff
+21
@@ -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
@@ -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>
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 );
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 ) );
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 );
|
||||
}
|
||||
}
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
require:
|
||||
- i18n-command.php
|
||||
@@ -0,0 +1,2 @@
|
||||
vendor
|
||||
.DS_Store
|
||||
+21
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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>';
|
||||
@@ -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
@@ -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";
|
||||
}
|
||||
+1023
File diff suppressed because it is too large
Load Diff
+162
@@ -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
@@ -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?
|
||||
+1114
File diff suppressed because it is too large
Load Diff
+196
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
foo: 'bar' #Comment
|
||||
arr: ['x', 'y', 'z'] # Comment here
|
||||
bar: kittens
|
||||
@@ -0,0 +1,2 @@
|
||||
MyObject:
|
||||
Prop1: {key1:val1}
|
||||
@@ -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
|
||||
@@ -0,0 +1,8 @@
|
||||
html_tags:
|
||||
- <br>
|
||||
- <p>
|
||||
html_content:
|
||||
- <p>hello world</p>
|
||||
- hello<br>world
|
||||
text_content:
|
||||
- hello world
|
||||
@@ -0,0 +1,4 @@
|
||||
.idea
|
||||
vendor
|
||||
.*.swp
|
||||
composer.lock
|
||||
+26
@@ -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
|
||||
Vendored
+19
@@ -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
@@ -0,0 +1,80 @@
|
||||
PHP Command Line Tools
|
||||
======================
|
||||
|
||||
[](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
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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'));
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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, ', ');
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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, );
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user