mirror of
https://github.com/okdana/twigc.git
synced 2024-04-30 12:52:45 +02:00
Merge 0.3: Major code restructuring, new features, more documentation
Merge branch 'dev/0.3'
This commit is contained in:
commit
532b078376
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -1,17 +1,15 @@
|
|||
# PHP-related files
|
||||
.idea/
|
||||
.phpintel/
|
||||
/vendor/
|
||||
/phpunit.xml
|
||||
/twigc.phar
|
||||
|
||||
# OS X-generated files
|
||||
# macOS-generated files
|
||||
._*
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
Icon\?
|
||||
Icon?
|
||||
|
||||
# Windows-generated files
|
||||
Desktop.ini
|
||||
|
@ -19,7 +17,8 @@ ehthumbs.db
|
|||
Thumbs.db
|
||||
|
||||
# Editor-generated files
|
||||
.idea/
|
||||
.phpintel/
|
||||
*~
|
||||
.*.s[a-w][a-z]
|
||||
*.sublime-workspace
|
||||
|
||||
|
|
3
LICENCE
3
LICENCE
|
@ -1,6 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) dana geier <dana@dana.is>
|
||||
Copyright (c) dana <dana@dana.is>
|
||||
|
||||
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
|
||||
|
@ -18,4 +18,3 @@ 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.
|
||||
|
||||
|
|
38
Makefile
38
Makefile
|
@ -1,20 +1,45 @@
|
|||
##
|
||||
# This file is part of twigc.
|
||||
#
|
||||
# @author dana geier <dana@dana.is>
|
||||
# @author dana <dana@dana.is>
|
||||
# @license MIT
|
||||
|
||||
all: build
|
||||
all: build
|
||||
build: phar
|
||||
phar: clean twigc.phar
|
||||
test: test-unit test-integration
|
||||
|
||||
help:
|
||||
@echo 'Available targets:'
|
||||
@echo 'all ................ Equivalent to `build`'
|
||||
@echo 'build .............. Equivalent to `vendor`'
|
||||
@echo 'install ............ Install phar to `/usr/local/bin/twigc`'
|
||||
@echo 'clean .............. Remove phar'
|
||||
@echo 'distclean .......... Remove phar and vendor directory'
|
||||
@echo 'phar ............... Equivalent to `twigc.phar`'
|
||||
@echo 'test ............... Run unit and integration tests'
|
||||
@echo 'test-integration ... Run integration tests against phar'
|
||||
@echo 'test-unit .......... Run unit tests against source'
|
||||
@echo 'twigc.phar ......... Build phar'
|
||||
@echo 'vendor ............. Install vendor directory via Composer'
|
||||
|
||||
vendor:
|
||||
composer install
|
||||
|
||||
twigc.phar: vendor
|
||||
twigc.phar:
|
||||
composer install -q --no-dev
|
||||
php -d phar.readonly=0 bin/compile
|
||||
composer install -q
|
||||
|
||||
build: twigc.phar
|
||||
test-unit: vendor
|
||||
vendor/bin/phpunit
|
||||
|
||||
install: build
|
||||
test-integration: twigc.phar
|
||||
./twigc.phar --help | grep -q -- --version
|
||||
echo 'hello {{ name }}' | ./twigc.phar -j '{ "name": "foo" }' | grep -qF 'hello foo'
|
||||
echo 'hello {{ name }}' | ./twigc.phar -p name=foo | grep -qF 'hello foo'
|
||||
|
||||
install: twigc.phar
|
||||
cp twigc.phar /usr/local/bin/twigc
|
||||
|
||||
clean:
|
||||
|
@ -23,5 +48,4 @@ clean:
|
|||
distclean: clean
|
||||
rm -rf vendor/
|
||||
|
||||
.PHONY: all build install clean distclean
|
||||
|
||||
.PHONY: all clean distclean install test test-integration test-unit
|
||||
|
|
226
README.md
226
README.md
|
@ -5,99 +5,201 @@
|
|||
for interacting with Twig through shell scripts and other command-line
|
||||
applications.
|
||||
|
||||
## Usage overview
|
||||
|
||||
```
|
||||
Usage:
|
||||
twigc [options] [--] [<template>]
|
||||
|
||||
Arguments:
|
||||
template Twig template file to render (use `-` for STDIN)
|
||||
|
||||
Options:
|
||||
-h, --help Display this usage help
|
||||
-V, --version Display version information
|
||||
--credits Display dependency credits (including Twig version)
|
||||
-d, --dir=DIR Add search directory to loader (multiple values allowed)
|
||||
--env Treat environment variables as input data
|
||||
-e, --escape=ESCAPE Set autoescape environment option
|
||||
-j, --json=JSON Pass variables as JSON (dictionary string or file path)
|
||||
-p, --pair=PAIR Pass variable as key=value pair (multiple values allowed)
|
||||
--query=QUERY Pass variables as URL query string
|
||||
-s, --strict Enable strict_variables environment option
|
||||
```
|
||||
## Usage
|
||||
|
||||
**twigc** can render Twig templates supplied via either standard input or a file
|
||||
path. Input data can be passed to the template using a simple key=value syntax:
|
||||
path.
|
||||
|
||||
```
|
||||
Usage: twigc [options] [<template>]
|
||||
|
||||
Options:
|
||||
-h, --help Display this usage help and exit
|
||||
-V, --version Display version information and exit
|
||||
--credits Display dependency information and exit
|
||||
--cache <dir> Enable caching to specified directory
|
||||
-d, --dir <dir> Add specified search directory to loader
|
||||
-e, --escape <strategy> Specify default auto-escaping strategy
|
||||
-E, --env Derive input data from environment
|
||||
-j, --json <dict/file> Derive input data from specified JSON file or
|
||||
dictionary string
|
||||
-p, --pair <input> Derive input data from specified key=value pair
|
||||
--query <input> Derive input data from specified URL query string
|
||||
-s, --strict Throw exception when undefined variable is referenced
|
||||
```
|
||||
|
||||
### Passing input data
|
||||
|
||||
Input data can be passed to the template using a simple key=value syntax with
|
||||
`-p`:
|
||||
|
||||
```
|
||||
% twigc -p 'name=dana' <<< 'Hello, {{ name }}!'
|
||||
Hello, dana!
|
||||
```
|
||||
|
||||
Of course, only simple string values can be provided this way. For more complex
|
||||
data, you can use the JSON option:
|
||||
Of course, only basic string values can be provided this way. For more complex
|
||||
data, you can use the JSON option `-j`:
|
||||
|
||||
```
|
||||
% twigc -j '{ "numbers": [1, 2, 3] }' <<< '{{ numbers|join("... ") }}!'
|
||||
% twigc -j '{"numbers": [1, 2, 3]}' <<< '{{ numbers|join("... ") }}!'
|
||||
1... 2... 3!
|
||||
```
|
||||
|
||||
JSON data can also be provided by file path or on standard input:
|
||||
|
||||
```
|
||||
# JSON from file, template from standard input
|
||||
% cat numbers.json
|
||||
{ "numbers": [1, 2, 3] }
|
||||
{"numbers": [1, 2, 3]}
|
||||
% twigc -j numbers.json <<< '{{ numbers|join("... ") }}!'
|
||||
1... 2... 3!
|
||||
|
||||
# JSON from standard input, template from file
|
||||
% cat numbers.twig
|
||||
{{ numbers|join("... ") }}!
|
||||
% twigc -j - numbers.twig <<< '{ "numbers": [1, 2, 3] }'
|
||||
% twigc -j - numbers.twig <<< '{"numbers": [1, 2, 3]}'
|
||||
1... 2... 3!
|
||||
```
|
||||
|
||||
(**twigc** determines whether the argument to `-j` is a dictionary string or a
|
||||
file name based on whether the first character is a `{`; if you have a file name
|
||||
that looks like that, use the absolute path or put `./` in front of it — for
|
||||
example, `twigc -j './{myfile}.json'`.)
|
||||
|
||||
If
|
||||
[`variables_order`](http://php.net/manual/en/ini.core.php#ini.variables-order)
|
||||
is configured appropriately in your `php.ini`, you can use the `-E` option to
|
||||
inherit input data from the environment:
|
||||
|
||||
```
|
||||
% NAME=dana twigc -E <<< 'Hello, {{ NAME }}!'
|
||||
Hello, dana!
|
||||
```
|
||||
|
||||
(If you *don't* have `E` in `variables_order`, you'll get an error.)
|
||||
|
||||
Lastly, there's a `--query` option in case you want to pass input as a URL query
|
||||
string:
|
||||
|
||||
```
|
||||
% twigc --query '?foo=&name=dana&bar=' <<< 'Hello, {{ name }}!'
|
||||
Hello, dana!
|
||||
```
|
||||
|
||||
All of the aforementioned input options can be given multiple times and in any
|
||||
combination, but the values associated with each input type override other
|
||||
values with the same name using the following order of precedence (from lowest
|
||||
to highest): environment, query, JSON, pair. In other words:
|
||||
|
||||
```
|
||||
# Pair has higher precedence than environment
|
||||
% name=foo twigc -p name=bar -E <<< '{{ name }} wins'
|
||||
bar wins
|
||||
|
||||
# JSON has higher precedence than query
|
||||
% twigc -j '{"name": "foo"}' --query name=bar <<< '{{ name }} wins'
|
||||
foo wins
|
||||
|
||||
# Inputs of the same type have equal precedence and are taken in the order given
|
||||
% twigc -j '{"name": "foo"}' -p name=bar -p name=baz <<< '{{ name }} wins'
|
||||
baz wins
|
||||
```
|
||||
|
||||
### Configuring auto-escaping
|
||||
|
||||
Normally, input data is [auto-escaped](http://twig.sensiolabs.org/doc/api.html)
|
||||
based on the template file extension (or disabled by default if using standard
|
||||
input), but this is configurable:
|
||||
during rendering based on the template file extension (or disabled by default if
|
||||
using standard input), but this is configurable:
|
||||
|
||||
```
|
||||
# No auto-escaping by default on standard input
|
||||
% twigc -p 'html=<p>Hello!</p>' <<< '{{ html }}'
|
||||
<p>Hello!</p>
|
||||
|
||||
% twigc -p 'html=<p>Hello!</p>' -e 'html' <<< '{{ html }}'
|
||||
# Explicit HTML auto-escaping
|
||||
% twigc -e html -p 'html=<p>Hello!</p>' <<< '{{ html }}'
|
||||
<p>Hello!</p>
|
||||
|
||||
% twigc -p 'html=<p>Hello!</p>' -e 'js' <<< '{{ html }}'
|
||||
# Explicit JavaScript auto-escaping
|
||||
% twigc -e js -p 'html=<p>Hello!</p>' <<< '{{ html }}'
|
||||
\x3Cp\x3EHello\x21\x3C\x2Fp\x3E
|
||||
|
||||
% twigc -p 'html=<p>Hello!</p>' -e 'url' <<< '{{ html }}'
|
||||
# Explicit URL auto-escaping
|
||||
% twigc -e url -p 'html=<p>Hello!</p>' <<< '{{ html }}'
|
||||
%3Cp%3EHello%21%3C%2Fp%3E
|
||||
```
|
||||
|
||||
Of course, you can always control escaping from within the template using the
|
||||
[`escape`](https://twig.symfony.com/doc/2.x/filters/escape.html) filter.
|
||||
|
||||
The following auto-escape methods are available:
|
||||
|
||||
* **`none`** (aka **`false`**, **`no`**, **`never`**, &c.) —
|
||||
No escaping is performed; the input is rendered as-is. This is the default for
|
||||
templates taken from standard input and for files with unrecognised
|
||||
extensions.
|
||||
|
||||
* **`html`** (aka **`true`**, **`yes`**, **`always`**, &c.) —
|
||||
Ampersand-escaping as suitable for inclusion in an HTML body. This is the most
|
||||
common escaping method used for rendering Web pages with Twig, and the default
|
||||
method used by the `escape` filter.
|
||||
|
||||
* **`css`** —
|
||||
Hex-escaping as suitable for inclusion in a CSS value or identifier.
|
||||
|
||||
* **`html_attr`** —
|
||||
Ampersand-escaping as suitable for inclusion in an HTML attribute value. This
|
||||
is similar to the `html` method, but more characters are escaped.
|
||||
|
||||
* **`js`** —
|
||||
Hex-escaping as suitable for inclusion in a JavaScript string or identifier.
|
||||
|
||||
* **`json`** —
|
||||
Serialisation according to JSON rules. Strings are quoted and escaped,
|
||||
integers are left bare, &c. JSON escaping can often be used to produce strings
|
||||
for config files and even languages like C (though incompatibilities do exist
|
||||
— be careful).
|
||||
|
||||
* **`sh`** —
|
||||
Double-quoting and meta-character escaping according to shell rules. This
|
||||
method uses double-quoted strings rather than the single-quote method used by
|
||||
e.g. `escapeshellarg()` because it is more compatible with software that
|
||||
supports only a sub-set of the shell's string syntax (such as 'dotenv'
|
||||
libraries).
|
||||
|
||||
* **`url`** —
|
||||
Percent-escaping as suitable for inclusion in a URL path segment or query
|
||||
parameter.
|
||||
|
||||
### Enabling strict mode
|
||||
|
||||
By default, references in the template to undefined variables are silently
|
||||
ignored; you can make Twig return an error instead:
|
||||
ignored, but you can make Twig throw an exception with the `-s` option:
|
||||
|
||||
```
|
||||
% twigc <<< 'Hello, {{ name }}!'
|
||||
Hello, !
|
||||
|
||||
% twigc -s <<< 'Hello, {{ name }}!'
|
||||
[Twig_Error_Runtime]
|
||||
Variable "name" does not exist in "-" at line 1
|
||||
twigc: Variable "name" does not exist in "-" at line 1.
|
||||
```
|
||||
|
||||
If a template file name was provided, the file's parent directory is used for
|
||||
Twig's include search path; if standard input was used, no search path is set at
|
||||
all by default. In either case, one or more additional search paths can be
|
||||
explicitly supplied on the command line:
|
||||
Use of this option is recommended for reliability in scripting scenarios.
|
||||
|
||||
### Specifying search directories
|
||||
|
||||
If a template file name was provided, the file's parent directory is
|
||||
automatically added as an include search path; if standard input was used, no
|
||||
search path is set at all by default. In either case, one or more additional
|
||||
search paths can be explicitly supplied on the command line:
|
||||
|
||||
```
|
||||
% cat include.twig
|
||||
Hello!
|
||||
|
||||
% twigc <<< '{% include "include.twig" %}'
|
||||
[Twig_Error_Loader]
|
||||
Template "include.twig" is not defined in "-" at line 1.
|
||||
twigc: Template "include.twig" is not defined in "-" at line 1.
|
||||
|
||||
% twigc -d '.' <<< '{% include "include.twig" %}'
|
||||
Hello!
|
||||
|
@ -108,7 +210,7 @@ Hello!
|
|||
**twigc** is provided as a self-contained executable archive; to download it,
|
||||
see the [releases](https://github.com/okdana/twigc/releases) page.
|
||||
|
||||
Of course, you can also build and install it from source:
|
||||
Of course, you can also build it from source:
|
||||
|
||||
```
|
||||
% git clone https://github.com/okdana/twigc
|
||||
|
@ -118,12 +220,40 @@ Of course, you can also build and install it from source:
|
|||
|
||||
## Requirements
|
||||
|
||||
The **twigc** executable is bundled with Twig and all of its other dependencies;
|
||||
the only thing you need to run it is PHP version 5.5 or higher.
|
||||
The **twigc** executable archive is bundled with Twig and all of its other
|
||||
dependencies; the only thing you need to run it is PHP version 7.0 or higher.
|
||||
|
||||
## Limitations, todos, requests
|
||||
|
||||
Earlier versions of this tool were implemented in the manner of a Symfony
|
||||
Console application. I like Symfony components a lot, but the way Console
|
||||
handles arguments in particular leaves a great deal to be desired if you're
|
||||
trying to create something that works like a traditional UNIX CLI tool — the API
|
||||
is confused, the functionality is limited, and worst of all it's buggy. I ended
|
||||
up switching out the Console application/command/input components for a lazier
|
||||
and uglier, but ultimately better-behaved, design incorporating the GetOpt.php
|
||||
library.
|
||||
|
||||
If you have any ideas as to how to improve on this situation, please let me
|
||||
know.
|
||||
|
||||
I'd also like to achieve the following goals at some point:
|
||||
|
||||
* Add to Packagist
|
||||
* Improve unit/integration tests
|
||||
* Create man page
|
||||
* Create zsh completion function
|
||||
|
||||
In the longer term, if i get really bored, maybe i'll add more input and escape
|
||||
methods. One thing that occurred to me is a custom escape option that takes an
|
||||
arbitrary escape character and a mask of characters to be escaped with it. For
|
||||
example, `-e 'custom:%:%'` might escape `printf(3)` format strings. idk
|
||||
|
||||
Anyway, if you find a bug or have a request, please let me know.
|
||||
|
||||
## Licence and acknowledgements
|
||||
|
||||
**twigc** is available under the MIT licence. For information about the licences
|
||||
**twigc** itself is available under the MIT licence. For information about the licences
|
||||
of its dependencies, run `twigc --credits`.
|
||||
|
||||
The `\Dana\Twigc\PharCompiler` class used to build the executable archive is
|
||||
|
@ -134,17 +264,21 @@ based on
|
|||
|
||||
* [twigphp/Twig](https://github.com/twigphp/Twig) —
|
||||
The Twig project on GitHub.
|
||||
|
||||
* [farazdagi/twig-cli](https://github.com/farazdagi/twig-cli) —
|
||||
Another project that aims to bring Twig to the command line. It's actually
|
||||
quite similar in design (though no code is shared); it just didn't have the
|
||||
features i wanted.
|
||||
features i wanted and isn't actively developed.
|
||||
|
||||
* [twigjs/twig.js](https://github.com/twigjs/twig.js) —
|
||||
A pure JavaScript implementation of Twig. It comes with its own command-line
|
||||
tool, `twigjs`, which can be used to render Twig templates, but it's quite
|
||||
limited.
|
||||
|
||||
* [indigojs/twig-cli](https://github.com/indigojs/twig-cli) —
|
||||
Another command-line Twig renderer based on Twig.js. Its functionality is very
|
||||
similar to (almost exactly the same as?) `twigjs`.
|
||||
|
||||
* [mattrobenolt/jinja2-cli](https://github.com/mattrobenolt/jinja2-cli) —
|
||||
A command-line [Jinja2](http://jinja.pocoo.org/) renderer. Very comparable to
|
||||
**twigc** in terms of features.
|
||||
|
|
16
bin/compile
Normal file → Executable file
16
bin/compile
Normal file → Executable file
|
@ -4,21 +4,23 @@
|
|||
/**
|
||||
* This file is part of twigc.
|
||||
*
|
||||
* @author dana geier <dana@dana.is>
|
||||
* @author dana <dana@dana.is>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../src/bootstrap.php';
|
||||
|
||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||
use Dana\Twigc\PharCompiler;
|
||||
|
||||
$verbose = false;
|
||||
$verboseArgs = ['v', 'vv', 'vvv', 'verbose', 'debug'];
|
||||
|
||||
foreach ( $argv as $arg ) {
|
||||
if ( in_array(ltrim($arg, '-'), $verboseArgs, true) ) {
|
||||
$verbose = true;
|
||||
break;
|
||||
}
|
||||
if ( in_array(ltrim($arg, '-'), $verboseArgs, true) ) {
|
||||
$verbose = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
(new \Dana\Twigc\PharCompiler($verbose))->compile();
|
||||
|
||||
(new PharCompiler(new ConsoleOutput(), $verbose))->compile('twigc.phar');
|
||||
|
|
9
bin/twigc
Normal file → Executable file
9
bin/twigc
Normal file → Executable file
|
@ -4,11 +4,16 @@
|
|||
/**
|
||||
* This file is part of twigc.
|
||||
*
|
||||
* @author dana geier <dana@dana.is>
|
||||
* @author dana <dana@dana.is>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../src/bootstrap.php';
|
||||
|
||||
(new \Dana\Twigc\Application())->run();
|
||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||
use Dana\Twigc\Application;
|
||||
|
||||
$output = new ConsoleOutput();
|
||||
$app = new Application();
|
||||
|
||||
exit($app->run($output));
|
||||
|
|
|
@ -1,25 +1,33 @@
|
|||
{
|
||||
"name": "dana/twigc",
|
||||
"description": "A CLI utility for rendering Twig templates",
|
||||
"homepage": "https://github.com/okdana/twigc",
|
||||
"keywords": ["twig", "compile", "render", "template", "cli"],
|
||||
"license": "MIT",
|
||||
"_comments": {
|
||||
"symfony/finder": [
|
||||
"This is only required for the Phar build, actually!",
|
||||
"I added it to the regular requires just to fix a problem with Composer.",
|
||||
"Some day i'd like to put it back..."
|
||||
]
|
||||
},
|
||||
|
||||
"require": {
|
||||
"php": ">=5.5.0",
|
||||
"symfony/console": "^3.1",
|
||||
"twig/twig": "^1.24"
|
||||
},
|
||||
"name": "dana/twigc",
|
||||
"description": "CLI tool for rendering Twig templates",
|
||||
"homepage": "https://github.com/okdana/twigc",
|
||||
"keywords": ["twig", "compile", "render", "template", "cli"],
|
||||
"license": "MIT",
|
||||
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^5.4",
|
||||
"symfony/finder": "^3.1"
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.0",
|
||||
"symfony/console": "^4.0",
|
||||
"symfony/finder": "^4.0",
|
||||
"twig/twig": "^2.4",
|
||||
"ulrichsg/getopt-php": "^3.1"
|
||||
},
|
||||
|
||||
"autoload": {
|
||||
"psr-4": {"Dana\\Twigc\\": "src/Twigc"}
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^6.5"
|
||||
},
|
||||
|
||||
"bin": ["bin/twigc"]
|
||||
"autoload": {
|
||||
"psr-4": {"Dana\\Twigc\\": "src/Twigc"}
|
||||
},
|
||||
|
||||
"bin": ["bin/twigc"]
|
||||
}
|
||||
|
||||
|
|
868
composer.lock
generated
868
composer.lock
generated
File diff suppressed because it is too large
Load diff
29
phpunit.xml.dist
Normal file
29
phpunit.xml.dist
Normal file
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit
|
||||
bootstrap="src/bootstrap.php"
|
||||
colors="true"
|
||||
verbose="true"
|
||||
convertErrorsToExceptions="true"
|
||||
stopOnFailure="false"
|
||||
>
|
||||
<!-- Output settings -->
|
||||
<logging>
|
||||
<!-- Output code coverage information -->
|
||||
<log type="coverage-text" target="php://stdout" showUncoveredFiles="true" />
|
||||
<log type="coverage-html" target="coverage" showUncoveredFiles="true" />
|
||||
</logging>
|
||||
|
||||
<!-- Code-coverage filter -->
|
||||
<filter>
|
||||
<whitelist>
|
||||
<directory suffix=".php">src</directory>
|
||||
</whitelist>
|
||||
</filter>
|
||||
|
||||
<!-- Test suite -->
|
||||
<testsuites>
|
||||
<testsuite name="\Dana\Twigc test suite">
|
||||
<directory suffix="Test.php">tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
|
@ -3,97 +3,495 @@
|
|||
/**
|
||||
* This file is part of twigc.
|
||||
*
|
||||
* @author dana geier <dana@dana.is>
|
||||
* @author dana <dana@dana.is>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace Dana\Twigc;
|
||||
|
||||
use Symfony\Component\Console\Application as BaseApplication;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use GetOpt\{Argument,GetOpt,Operand,Option};
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Output\{ConsoleOutputInterface,OutputInterface};
|
||||
use Twig\Environment;
|
||||
use Twig\Loader\{ArrayLoader,FilesystemLoader};
|
||||
|
||||
use Dana\Twigc\ComposerHelper;
|
||||
|
||||
/**
|
||||
* twigc application container.
|
||||
* This class represents the entire `twigc` application.
|
||||
*
|
||||
* This class overrides a bunch of the default Console behaviour to make the
|
||||
* application work like a more traditional UNIX CLI tool.
|
||||
* To be completely honest, this feels really shitty to me, and i don't like it.
|
||||
* But after eliminating the standard Symfony\Console structure (due to
|
||||
* Console's woefully inadequate argument handling, amongst other things), i
|
||||
* find myself unsure of the best way to structure this, especially given how
|
||||
* simple the application is, and just kind of want to be done with it. I guess
|
||||
* this works for now, but i would welcome any improvements.
|
||||
*/
|
||||
class Application extends BaseApplication {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') {
|
||||
parent::__construct('twigc', \Dana\Twigc\Twigc::VERSION_NUMBER);
|
||||
}
|
||||
class Application {
|
||||
const NAME = 'twigc';
|
||||
const VERSION = '0.3.0';
|
||||
const BUILD_DATE = '%BUILD_DATE%'; // Replaced during build
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* In a normal Console application, this method handles the --version and
|
||||
* --help options. In our application, the default command handles all of
|
||||
* that.
|
||||
*/
|
||||
public function doRun(InputInterface $input, OutputInterface $output) {
|
||||
$name = $this->getCommandName($input);
|
||||
protected $name;
|
||||
protected $version;
|
||||
|
||||
if ( ! $name ) {
|
||||
$name = $this->defaultCommand;
|
||||
$input = new ArrayInput(['command' => $this->defaultCommand]);
|
||||
}
|
||||
/**
|
||||
* Construct the object.
|
||||
*
|
||||
* @param string|null $name
|
||||
* (optional) The name of the application, to be used in error messages and
|
||||
* the like.
|
||||
*
|
||||
* @param string|null $version
|
||||
* (optional) The version number of the application, to be used in the
|
||||
* `--version` output.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function __construct(string $name = null, string $version = null) {
|
||||
$this->name = $name ?? static::NAME;
|
||||
$this->version = $version ?? static::VERSION;
|
||||
}
|
||||
|
||||
$command = $this->find($name);
|
||||
/**
|
||||
* Run the application.
|
||||
*
|
||||
* This is mostly a wrapper around doRun() to handle error printing.
|
||||
*
|
||||
* @param OutputInterface $output
|
||||
* The output to write to.
|
||||
*
|
||||
* @param array|null $argv
|
||||
* (optional) Command-line arguments to the application (with the 0th member
|
||||
* as the application name).
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function run(OutputInterface $output, array $argv = null): int {
|
||||
if ( $output instanceof ConsoleOutputInterface ) {
|
||||
$error = $output->getErrorOutput();
|
||||
} else {
|
||||
$error = $output;
|
||||
}
|
||||
|
||||
$this->runningCommand = $command;
|
||||
$exitCode = $this->doRunCommand($command, $input, $output);
|
||||
$this->runningCommand = null;
|
||||
try {
|
||||
return $this->doRun($output, $argv);
|
||||
} catch ( \Exception $e ) {
|
||||
$error->writeln(sprintf(
|
||||
'%s: %s', $this->name,
|
||||
rtrim($e->getMessage(), "\r\n"))
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return $exitCode;
|
||||
}
|
||||
/**
|
||||
* Run the application (for real).
|
||||
*
|
||||
* @param OutputInterface $output
|
||||
* The output to write to.
|
||||
*
|
||||
* @param array|null $argv
|
||||
* (optional) Command-line arguments to the application (with the 0th member
|
||||
* as the application name).
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function doRun(OutputInterface $output, array $argv = null): int {
|
||||
$argv = $argv ?? $_SERVER['argv'];
|
||||
$getopt = $this->getGetOpt();
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDefinition() {
|
||||
$definition = parent::getDefinition();
|
||||
$definition->setArguments();
|
||||
return $definition;
|
||||
}
|
||||
$getopt->process(array_slice($argv, 1));
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* Since we're a one-command application, we always use the name of the
|
||||
* default command.
|
||||
*/
|
||||
protected function getCommandName(InputInterface $input) {
|
||||
return $this->getDefaultCommands()[0]->getName();
|
||||
}
|
||||
if ( $getopt->getOption('help') ) {
|
||||
$this->doHelp($output, $getopt);
|
||||
return 0;
|
||||
}
|
||||
if ( $getopt->getOption('version') ) {
|
||||
$this->doVersion($output);
|
||||
return 0;
|
||||
}
|
||||
if ( $getopt->getOption('credits') ) {
|
||||
$this->doCredits($output);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* Since we're a one-command application, we always use the definition of
|
||||
* the default command. This means that none of the built-in Console options
|
||||
* like --help and --ansi are automatically defined — the default command
|
||||
* must handle all of that.
|
||||
*/
|
||||
protected function getDefaultInputDefinition() {
|
||||
return $this->getDefaultCommands()[0]->getDefinition();
|
||||
}
|
||||
$inputData = [];
|
||||
$template = $getopt->getOperand('template');
|
||||
$dirs = $getopt->getOption('dir');
|
||||
$temp = false;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* Since we're a one-command application, we always return just the default
|
||||
* command.
|
||||
*/
|
||||
protected function getDefaultCommands() {
|
||||
return [new \Dana\Twigc\DefaultCommand()];
|
||||
}
|
||||
// If we're receiving data on standard input, and we didn't get a template,
|
||||
// assume `-` — we'll make sure this doesn't conflict with `-j` below
|
||||
if ( ! posix_isatty(\STDIN) ) {
|
||||
$template = $template ?? '-';
|
||||
}
|
||||
|
||||
// Add the template's parent directory if we're not using standard input
|
||||
if ( ($template ?? '-') !== '-' ) {
|
||||
$dirs = array_merge([dirname($template)], $dirs);
|
||||
}
|
||||
|
||||
if ( $template === null ) {
|
||||
$this->doHelp($output, $getopt, 'No template specified');
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Input data via environment
|
||||
if ( $getopt->getOption('env') ) {
|
||||
if ( empty($_ENV) && strpos(ini_get('variables_order'), 'E') === false ) {
|
||||
throw new \RuntimeException(
|
||||
"INI setting 'variables_order' must include 'E' to use option 'env'"
|
||||
);
|
||||
}
|
||||
$inputData = array_merge($inputData, $_ENV);
|
||||
}
|
||||
|
||||
// Input data via query string
|
||||
foreach ( $getopt->getOption('query') as $query ) {
|
||||
$query = ltrim($query, '?');
|
||||
$parsed = [];
|
||||
|
||||
parse_str($query, $parsed);
|
||||
|
||||
$inputData = array_merge($inputData, $parsed);
|
||||
}
|
||||
|
||||
// Input data via JSON
|
||||
foreach ( $getopt->getOption('json') as $json ) {
|
||||
// JSON supplied via standard input
|
||||
if ( $json === '-' ) {
|
||||
if ( $template === '-' ) {
|
||||
throw new \InvalidArgumentException(
|
||||
'Can not read both template and JSON input from stdin'
|
||||
);
|
||||
}
|
||||
|
||||
$json = file_get_contents('php://stdin');
|
||||
|
||||
// JSON supplied via file
|
||||
} elseif ( (ltrim($json)[0] ?? '') !== '{' ) {
|
||||
if ( ! file_exists($json) || is_dir($json) ) {
|
||||
throw new \InvalidArgumentException(
|
||||
"Missing or invalid JSON file: ${json}"
|
||||
);
|
||||
}
|
||||
$json = file_get_contents($json);
|
||||
}
|
||||
|
||||
// This check is here to prevent errors if the input is just empty
|
||||
if ( trim($json) !== '' ) {
|
||||
$json = json_decode($json, true);
|
||||
}
|
||||
|
||||
if ( ! is_array($json) ) {
|
||||
throw new \InvalidArgumentException(
|
||||
'JSON input must be a dictionary'
|
||||
);
|
||||
}
|
||||
|
||||
$inputData = array_merge($inputData, $json);
|
||||
}
|
||||
|
||||
// Input data via key=value pair
|
||||
foreach ( $getopt->getOption('pair') as $pair ) {
|
||||
$kv = explode('=', $pair, 2);
|
||||
|
||||
if ( count($kv) !== 2 ) {
|
||||
throw new \InvalidArgumentException(
|
||||
"Illegal key=value pair: ${pair}"
|
||||
);
|
||||
}
|
||||
|
||||
$inputData[$kv[0]] = $kv[1];
|
||||
}
|
||||
|
||||
// Template supplied via file path
|
||||
if ( $template !== '-' ) {
|
||||
$loader = new FilesystemLoader($dirs);
|
||||
// Template supplied via standard input
|
||||
} else {
|
||||
// If we've been supplied one or more search directories, we'll need to
|
||||
// write the template out to a temp directory so we can use the file-
|
||||
// system loader
|
||||
if ( $dirs ) {
|
||||
$temp = true;
|
||||
$template = implode('/', [
|
||||
sys_get_temp_dir(),
|
||||
implode('.', ['', $this->name, getmypid(), md5(time())]),
|
||||
$template,
|
||||
]);
|
||||
|
||||
mkdir(dirname($template));
|
||||
file_put_contents($template, file_get_contents('php://stdin'), \LOCK_EX);
|
||||
|
||||
$dirs = array_merge([dirname($template)], $dirs);
|
||||
$loader = new FilesystemLoader($dirs);
|
||||
|
||||
// Otherwise, we can just use the array loader, which is a little faster
|
||||
// and cleaner
|
||||
} else {
|
||||
$loader = new ArrayLoader([
|
||||
$template => file_get_contents('php://stdin'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Render
|
||||
try {
|
||||
$twig = new Environment($loader, [
|
||||
'cache' => $getopt->getOption('cache') ?? false,
|
||||
'debug' => false,
|
||||
'strict_variables' => (bool) $getopt->getOption('strict'),
|
||||
'autoescape' => $this->getEscaper(
|
||||
$getopt->getOption('escape'),
|
||||
$template
|
||||
),
|
||||
]);
|
||||
|
||||
$twig->getExtension('Twig_Extension_Core')->setEscaper(
|
||||
'json',
|
||||
function($twigEnv, $string, $charset) {
|
||||
return json_encode(
|
||||
$string,
|
||||
\JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE
|
||||
);
|
||||
}
|
||||
);
|
||||
$twig->getExtension('Twig_Extension_Core')->setEscaper(
|
||||
'sh',
|
||||
function($twigEnv, $string, $charset) {
|
||||
return '"' . addcslashes($string, '$`\\"') . '"';
|
||||
}
|
||||
);
|
||||
|
||||
$output->writeln(
|
||||
rtrim($twig->render(basename($template), $inputData), "\r\n")
|
||||
);
|
||||
|
||||
// Clean up
|
||||
} finally {
|
||||
if ( $temp ) {
|
||||
unlink($template);
|
||||
rmdir(dirname($template));
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the application's usage help.
|
||||
*
|
||||
* @param OutputInterface $output
|
||||
* The output to write to.
|
||||
*
|
||||
* @param GetOpt $getopt
|
||||
* The GetOpt instance from which to derive the usage help.
|
||||
*
|
||||
* @param string|null $message
|
||||
* (optional) An additional message to print above the usage help. This is
|
||||
* intended primarily for error messages.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function doHelp(
|
||||
OutputInterface $output,
|
||||
GetOpt $getopt,
|
||||
string $message = null
|
||||
): int {
|
||||
if ( $message !== null && $message !== '' ) {
|
||||
$output->writeln("{$this->name}: " . rtrim($message, "\r\n") . "\n");
|
||||
}
|
||||
$output->writeln(rtrim($getopt->getHelpText(), "\r\n"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the application's version information.
|
||||
*
|
||||
* @param OutputInterface $output The output to write to.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function doVersion(OutputInterface $output): int {
|
||||
$version = sprintf('%s version %s', $this->name, $this->version);
|
||||
|
||||
if ( strpos(static::BUILD_DATE, '%') === false ) {
|
||||
$version .= sprintf(' (built %s)', static::BUILD_DATE);
|
||||
}
|
||||
|
||||
$output->writeln($version);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the application's dependency information.
|
||||
*
|
||||
* @param OutputInterface $output The output to write to.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function doCredits(OutputInterface $output): int {
|
||||
$packages = (new ComposerHelper())->getPackages();
|
||||
|
||||
$table = new Table($output);
|
||||
$table->setStyle('compact');
|
||||
$table->getStyle()->setVerticalBorderChar('');
|
||||
$table->getStyle()->setCellRowContentFormat('%s ');
|
||||
$table->setHeaders(['name', 'version', 'licence']);
|
||||
|
||||
foreach ( $packages as $package ) {
|
||||
$table->addRow([
|
||||
$package->name,
|
||||
ltrim($package->version, 'v'),
|
||||
implode(', ', $package->license) ?: '?',
|
||||
]);
|
||||
}
|
||||
$table->render();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance of GetOpt configured for this application.
|
||||
*
|
||||
* @return GetOpt
|
||||
*/
|
||||
public function getGetOpt(): GetOpt {
|
||||
$getopt = new GetOpt(null, [
|
||||
GetOpt::SETTING_SCRIPT_NAME => $this->name,
|
||||
GetOpt::SETTING_STRICT_OPERANDS => true,
|
||||
]);
|
||||
$getopt->addOptions([
|
||||
Option::create('h', 'help', GetOpt::NO_ARGUMENT)
|
||||
->setDescription('Display this usage help and exit')
|
||||
,
|
||||
Option::create('V', 'version', GetOpt::NO_ARGUMENT)
|
||||
->setDescription('Display version information and exit')
|
||||
,
|
||||
Option::create(null, 'credits', GetOpt::NO_ARGUMENT)
|
||||
->setDescription('Display dependency information and exit')
|
||||
,
|
||||
Option::create(null, 'cache', GetOpt::REQUIRED_ARGUMENT)
|
||||
->setDescription('Enable caching to specified directory')
|
||||
->setArgumentName('dir')
|
||||
->setValidation('is_dir')
|
||||
,
|
||||
Option::create('d', 'dir', GetOpt::MULTIPLE_ARGUMENT)
|
||||
->setDescription('Add specified search directory to loader')
|
||||
->setArgumentName('dir')
|
||||
->setValidation('is_dir')
|
||||
,
|
||||
Option::create('e', 'escape', GetOpt::REQUIRED_ARGUMENT)
|
||||
->setArgumentName('strategy')
|
||||
->setDescription('Specify default auto-escaping strategy')
|
||||
,
|
||||
Option::create('E', 'env', GetOpt::NO_ARGUMENT)
|
||||
->setDescription('Derive input data from environment')
|
||||
,
|
||||
Option::create('j', 'json', GetOpt::MULTIPLE_ARGUMENT)
|
||||
->setArgumentName('dict/file')
|
||||
->setDescription('Derive input data from specified JSON file or dictionary string')
|
||||
,
|
||||
Option::create('p', 'pair', GetOpt::MULTIPLE_ARGUMENT)
|
||||
->setArgumentName('input')
|
||||
->setDescription('Derive input data from specified key=value pair')
|
||||
,
|
||||
Option::create(null, 'query', GetOpt::MULTIPLE_ARGUMENT)
|
||||
->setArgumentName('input')
|
||||
->setDescription('Derive input data from specified URL query string')
|
||||
,
|
||||
Option::create('s', 'strict', GetOpt::NO_ARGUMENT)
|
||||
->setDescription('Throw exception when undefined variable is referenced')
|
||||
,
|
||||
]);
|
||||
$getopt->addOperands([
|
||||
Operand::create('template', Operand::OPTIONAL),
|
||||
]);
|
||||
|
||||
return $getopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the correct Twig escape method given the provided options.
|
||||
*
|
||||
* @param string|null $escape
|
||||
* The user-provided escape option, or null if not provided.
|
||||
*
|
||||
* @param string|null $template
|
||||
* The name/path of the template file, or null if not provided. This is only
|
||||
* used when $escape is null or 'auto'.
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
public function getEscaper($escape, string $template = null) {
|
||||
$escape = $escape === null ? $escape : strtolower($escape);
|
||||
$template = $template ?? '';
|
||||
|
||||
if ( $escape === null || $escape === 'auto' ) {
|
||||
if (
|
||||
substr($template, -5) === '.twig'
|
||||
&&
|
||||
strpos(substr($template, 0, -5), '.')
|
||||
) {
|
||||
$ext = pathinfo(substr($template, 0, -5), \PATHINFO_EXTENSION);
|
||||
} else {
|
||||
$ext = pathinfo($template, \PATHINFO_EXTENSION);
|
||||
}
|
||||
|
||||
switch ( strtolower($ext) ) {
|
||||
case 'htm':
|
||||
case 'html':
|
||||
case 'phtml':
|
||||
case 'thtml':
|
||||
case 'xhtml':
|
||||
case 'template':
|
||||
case 'tmpl':
|
||||
case 'tpl':
|
||||
return 'html';
|
||||
case 'css':
|
||||
case 'scss':
|
||||
return 'css';
|
||||
case 'js':
|
||||
return 'js';
|
||||
case 'json':
|
||||
return 'json';
|
||||
case 'bash':
|
||||
case 'ksh':
|
||||
case 'sh':
|
||||
case 'zsh':
|
||||
return 'sh';
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Otherwise, try to parse the supplied method
|
||||
switch ( $escape ) {
|
||||
case 'f':
|
||||
case 'n':
|
||||
case 'none':
|
||||
case 'never':
|
||||
$escape = 'false';
|
||||
break;
|
||||
case 't':
|
||||
case 'y':
|
||||
case 'always':
|
||||
$escape = 'true';
|
||||
break;
|
||||
}
|
||||
|
||||
$bool = filter_var(
|
||||
$escape,
|
||||
\FILTER_VALIDATE_BOOLEAN,
|
||||
\FILTER_NULL_ON_FAILURE
|
||||
);
|
||||
|
||||
if ( $bool !== null ) {
|
||||
$escape = $bool ? 'html' : false;
|
||||
}
|
||||
|
||||
return $escape;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
88
src/Twigc/ComposerHelper.php
Normal file
88
src/Twigc/ComposerHelper.php
Normal file
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of twigc.
|
||||
*
|
||||
* @author dana <dana@dana.is>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace Dana\Twigc;
|
||||
|
||||
/**
|
||||
* Helper class with various functions for interacting with Composer's lock
|
||||
* file.
|
||||
*/
|
||||
class ComposerHelper {
|
||||
/**
|
||||
* Get an array of the installed non-dev packages listed in a Composer lock
|
||||
* file.
|
||||
*
|
||||
* @param string|null (optional) The path to the lock file to parse.
|
||||
*
|
||||
* @return object[]
|
||||
*/
|
||||
public function getPackages(string $lockFile = null) {
|
||||
$packages = $this->parseLockFile($lockFile)['packages'];
|
||||
return $this->massagePackages($packages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of the installed dev packages listed in a Composer lock file.
|
||||
*
|
||||
* @param string|null (optional) The path to the lock file to parse.
|
||||
*
|
||||
* @return object[]
|
||||
*/
|
||||
public function getDevPackages(string $lockFile = null) {
|
||||
$packages = $this->parseLockFile($lockFile)['packages-dev'];
|
||||
return $this->massagePackages($packages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of data representing a Composer lock file.
|
||||
*
|
||||
* @param string $path
|
||||
* (optional) The path to the lock file to parse. The default is the lock
|
||||
* file associated with the current project.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws \RuntimeException if composer.lock doesn't exist
|
||||
* @throws \RuntimeException if composer.lock can't be decoded
|
||||
*/
|
||||
public function parseLockFile(string $lockFile = null): array {
|
||||
$lockFile = $lockFile ?? __DIR__ . '/../../composer.lock';
|
||||
|
||||
if ( ! file_exists($lockFile) ) {
|
||||
throw new \RuntimeException('Missing ' . basename($lockFile));
|
||||
}
|
||||
|
||||
$lock = json_decode(file_get_contents($lockFile), true);
|
||||
|
||||
if ( empty($lock) || ! isset($lock['packages']) ) {
|
||||
throw new \RuntimeException('Error decoding ' . basename($lockFile));
|
||||
}
|
||||
|
||||
return $lock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort and object-ify an array of package data.
|
||||
*
|
||||
* @param array $packages Package data from composer.lock.
|
||||
*
|
||||
* @return object[]
|
||||
*/
|
||||
public function massagePackages(array $packages): array {
|
||||
usort($packages, function ($a, $b) {
|
||||
return strcasecmp($a['name'], $b['name']);
|
||||
});
|
||||
|
||||
foreach ( $packages as &$package ) {
|
||||
$package = (object) $package;
|
||||
}
|
||||
|
||||
return $packages;
|
||||
}
|
||||
}
|
|
@ -1,438 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of twigc.
|
||||
*
|
||||
* @author dana geier <dana@dana.is>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace Dana\Twigc;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\DescriptorHelper;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Default twigc command.
|
||||
*/
|
||||
class DefaultCommand extends Command {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure() {
|
||||
$this
|
||||
->setName('twigc')
|
||||
->setDescription('Compile a Twig template')
|
||||
->addArgument(
|
||||
'template',
|
||||
InputArgument::OPTIONAL,
|
||||
'Twig template file to render (use `-` for STDIN)'
|
||||
)
|
||||
->addOption(
|
||||
'help',
|
||||
'h',
|
||||
InputOption::VALUE_NONE,
|
||||
'Display this usage help'
|
||||
)
|
||||
->addOption(
|
||||
'version',
|
||||
'V',
|
||||
InputOption::VALUE_NONE,
|
||||
'Display version information'
|
||||
)
|
||||
->addOption(
|
||||
'credits',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Display dependency credits (including Twig version)'
|
||||
)
|
||||
->addOption(
|
||||
'cache',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Enable caching to specified directory'
|
||||
)
|
||||
->addOption(
|
||||
'dir',
|
||||
'd',
|
||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
'Add search directory to loader'
|
||||
)
|
||||
->addOption(
|
||||
'env',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Treat environment variables as input data'
|
||||
)
|
||||
->addOption(
|
||||
'escape',
|
||||
'e',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Set autoescape environment option'
|
||||
)
|
||||
->addOption(
|
||||
'json',
|
||||
'j',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Pass variables as JSON (dictionary string or file path)'
|
||||
)
|
||||
->addOption(
|
||||
'pair',
|
||||
'p',
|
||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
'Pass variable as key=value pair'
|
||||
)
|
||||
->addOption(
|
||||
'query',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Pass variables as URL query string'
|
||||
)
|
||||
->addOption(
|
||||
'strict',
|
||||
's',
|
||||
InputOption::VALUE_NONE,
|
||||
'Enable strict_variables environment option'
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output) {
|
||||
switch ( true ) {
|
||||
// Display usage help
|
||||
case $input->getOption('help'):
|
||||
return $this->doHelp($input, $output);
|
||||
// Display version information
|
||||
case $input->getOption('version'):
|
||||
return $this->doVersion($input, $output);
|
||||
// Display package credits
|
||||
case $input->getOption('credits'):
|
||||
return $this->doCredits($input, $output);
|
||||
}
|
||||
// Render Twig template
|
||||
return $this->doRender($input, $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* Overriding this prevents TextDescriptor from displaying the Help section.
|
||||
*/
|
||||
public function getProcessedHelp() {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays usage help.
|
||||
*
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function doHelp(InputInterface $input, OutputInterface $output) {
|
||||
(new DescriptorHelper())->describe($output, $this);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays version information.
|
||||
*
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function doVersion(InputInterface $input, OutputInterface $output) {
|
||||
$nameFmt = '<info>twigc</info>';
|
||||
$versionFmt = '<comment>%s</comment> (<comment>%s</comment> @ <comment>%s</comment>)';
|
||||
|
||||
$output->writeln(sprintf(
|
||||
"${nameFmt} version ${versionFmt}",
|
||||
\Dana\Twigc\Twigc::VERSION_NUMBER,
|
||||
\Dana\Twigc\Twigc::VERSION_COMMIT,
|
||||
\Dana\Twigc\Twigc::VERSION_DATE
|
||||
));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays package credits.
|
||||
*
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function doCredits(InputInterface $input, OutputInterface $output) {
|
||||
$installed = \Dana\Twigc\Twigc::getComposerPackages();
|
||||
|
||||
$table = new Table($output);
|
||||
$table->setStyle('compact');
|
||||
$table->getStyle()->setVerticalBorderChar('');
|
||||
$table->getStyle()->setCellRowContentFormat('%s ');
|
||||
$table->setHeaders(['name', 'version', 'licence']);
|
||||
|
||||
foreach ( $installed as $package ) {
|
||||
$table->addRow([
|
||||
$package->name,
|
||||
ltrim($package->version, 'v'),
|
||||
implode(', ', $package->license) ?: '?',
|
||||
]);
|
||||
}
|
||||
$table->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a Twig template.
|
||||
*
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function doRender(InputInterface $input, OutputInterface $output) {
|
||||
$inputData = [];
|
||||
$template = $input->getArgument('template');
|
||||
$template = $template === null ? '-' : $template;
|
||||
$cache = $input->getOption('cache');
|
||||
$cache = $cache === null ? false : $cache;
|
||||
$dirs = $template === '-' ? [] : [dirname($template)];
|
||||
$dirs = array_merge($dirs, $input->getOption('dir'));
|
||||
$temp = false;
|
||||
$strict = (bool) $input->getOption('strict');
|
||||
$escape = $input->getOption('escape');
|
||||
$inputs = [
|
||||
'json' => (int) ($input->getOption('json') !== null),
|
||||
'pair' => (int) (! empty($input->getOption('pair'))),
|
||||
'query' => (int) ($input->getOption('query') !== null),
|
||||
];
|
||||
|
||||
// If we're reading from STDIN, but STDIN is a TTY, print help and die
|
||||
if ( $template === '-' && posix_isatty(\STDIN) ) {
|
||||
$this->doHelp($input, $output);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Validate search directories
|
||||
foreach ( $dirs as $dir ) {
|
||||
if ( ! is_dir($dir) ) {
|
||||
throw new \InvalidArgumentException(
|
||||
"Illegal search directory: ${dir}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// If no escape option was supplied, try to auto-detect
|
||||
// (we could do this with Twig's 'filename' method, but i have some
|
||||
// control over this)
|
||||
if ( $escape === null ) {
|
||||
if ( substr($template, -5) === '.twig' ) {
|
||||
$ext = pathinfo(substr($template, -5), \PATHINFO_EXTENSION);
|
||||
} else {
|
||||
$ext = pathinfo($template, \PATHINFO_EXTENSION);
|
||||
}
|
||||
|
||||
switch ( strtolower($ext) ) {
|
||||
case 'template':
|
||||
case 'tmpl':
|
||||
case 'tpl':
|
||||
case 'htm':
|
||||
case 'html':
|
||||
case 'phtml':
|
||||
case 'thtml':
|
||||
case 'xhtml':
|
||||
$escape = 'html';
|
||||
break;
|
||||
case 'css':
|
||||
case 'scss':
|
||||
$escape = 'css';
|
||||
break;
|
||||
case 'js':
|
||||
$escape = 'js';
|
||||
break;
|
||||
case 'json':
|
||||
$escape = 'json';
|
||||
break;
|
||||
default:
|
||||
$escape = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Otherwise, try to parse the supplied method
|
||||
} else {
|
||||
// Normalise some boolean values
|
||||
$escape = strtolower($escape);
|
||||
$escape = $escape === 'f' ? 'false' : $escape;
|
||||
$escape = $escape === 'n' ? 'false' : $escape;
|
||||
$escape = $escape === 'none' ? 'false' : $escape;
|
||||
$escape = $escape === 'never' ? 'false' : $escape;
|
||||
$escape = $escape === 't' ? 'true' : $escape;
|
||||
$escape = $escape === 'y' ? 'true' : $escape;
|
||||
$escape = $escape === 'always' ? 'true' : $escape;
|
||||
|
||||
$bool = filter_var($escape, \FILTER_VALIDATE_BOOLEAN, \FILTER_NULL_ON_FAILURE);
|
||||
|
||||
if ( $bool !== null ) {
|
||||
$escape = $bool ? 'html' : false;
|
||||
}
|
||||
}
|
||||
|
||||
// Because Console doesn't allow us to see the order of options supplied
|
||||
// at the command line, there's no good way to sort out the precedence
|
||||
// amongst the different input methods... so let's just say we can only
|
||||
// use one of them at a time
|
||||
if ( array_sum($inputs) > 1 ) {
|
||||
throw new \InvalidArgumentException(
|
||||
'-j, -p, and --query options are mutually exclusive'
|
||||
);
|
||||
}
|
||||
|
||||
// Input data supplied via query string
|
||||
if ( ($query = $input->getOption('query')) !== null ) {
|
||||
if ( $query && $query[0] === '?' ) {
|
||||
$query = substr($query, 1);
|
||||
}
|
||||
parse_str($query, $inputData);
|
||||
|
||||
// Input data supplied via JSON
|
||||
} elseif ( ($json = $input->getOption('json')) !== null ) {
|
||||
$json = trim($json);
|
||||
|
||||
// JSON supplied via STDIN
|
||||
if ( $json === '-' ) {
|
||||
if ( $template === '-' ) {
|
||||
throw new \InvalidArgumentException(
|
||||
'Can not read both template and JSON input from STDIN'
|
||||
);
|
||||
}
|
||||
if ( posix_isatty(\STDIN) ) {
|
||||
throw new \InvalidArgumentException(
|
||||
'Expected JSON input on STDIN'
|
||||
);
|
||||
}
|
||||
$json = file_get_contents('php://stdin');
|
||||
|
||||
// JSON supplied via file
|
||||
} elseif ( $json && $json[0] !== '{' ) {
|
||||
if ( ! file_exists($json) || is_dir($json) ) {
|
||||
throw new \InvalidArgumentException(
|
||||
"Missing or illegal JSON file name: ${json}"
|
||||
);
|
||||
}
|
||||
$json = file_get_contents($json);
|
||||
}
|
||||
|
||||
// This check is here to prevent errors if the input is just empty
|
||||
if ( trim($json) !== '' ) {
|
||||
$inputData = json_decode($json, true);
|
||||
}
|
||||
|
||||
if ( ! is_array($inputData) ) {
|
||||
throw new \InvalidArgumentException(
|
||||
'JSON input must be a dictionary'
|
||||
);
|
||||
}
|
||||
|
||||
// Input data supplied via key=value pair
|
||||
} elseif ( count($input->getOption('pair')) ) {
|
||||
foreach ( $input->getOption('pair') as $pair ) {
|
||||
$kv = explode('=', $pair, 2);
|
||||
|
||||
if ( count($kv) !== 2 ) {
|
||||
throw new \InvalidArgumentException(
|
||||
"Illegal key=value pair: ${pair}"
|
||||
);
|
||||
}
|
||||
|
||||
$inputData[$kv[0]] = $kv[1];
|
||||
}
|
||||
}
|
||||
|
||||
if ( $input->getOption('env') ) {
|
||||
$inputData = array_merge($_ENV, $inputData);
|
||||
}
|
||||
|
||||
// Validate key names now
|
||||
foreach ( $inputData as $key => $value ) {
|
||||
if ( ! preg_match('#^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$#', $key) ) {
|
||||
throw new \InvalidArgumentException(
|
||||
"Illegal variable name: ${key}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Template supplied via STDIN
|
||||
if ( $template === '-' ) {
|
||||
// If we've been supplied one or more search directories, we'll need
|
||||
// to write the template out to a temp directory so we can use the
|
||||
// file-system loader
|
||||
if ( $dirs ) {
|
||||
$temp = true;
|
||||
$template = implode('/', [
|
||||
sys_get_temp_dir(),
|
||||
implode('.', ['twigc', getmypid(), md5(time())]),
|
||||
'-',
|
||||
]);
|
||||
|
||||
mkdir(dirname($template));
|
||||
file_put_contents($template, file_get_contents('php://stdin'), LOCK_EX);
|
||||
|
||||
$dirs = array_merge([dirname($template)], $dirs);
|
||||
|
||||
$loader = new \Twig_Loader_Filesystem($dirs);
|
||||
|
||||
// Otherwise, we can just use the array loader, which is a little
|
||||
// faster and cleaner
|
||||
} else {
|
||||
$loader = new \Twig_Loader_Array([
|
||||
$template => file_get_contents('php://stdin'),
|
||||
]);
|
||||
}
|
||||
|
||||
// Template supplied via file path
|
||||
} else {
|
||||
$loader = new \Twig_Loader_Filesystem($dirs);
|
||||
}
|
||||
|
||||
try {
|
||||
$twig = new \Twig_Environment($loader, [
|
||||
'cache' => $cache,
|
||||
'debug' => false,
|
||||
'strict_variables' => $strict,
|
||||
'autoescape' => $escape,
|
||||
]);
|
||||
|
||||
$twig->getExtension('core')->setEscaper(
|
||||
'json',
|
||||
function($twigEnv, $string, $charset) {
|
||||
return json_encode(
|
||||
$string,
|
||||
\JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
$output->writeln(
|
||||
rtrim($twig->render(basename($template), $inputData), "\r\n")
|
||||
);
|
||||
} finally {
|
||||
if ( $temp ) {
|
||||
unlink($template);
|
||||
rmdir(dirname($template));
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,18 +3,19 @@
|
|||
/**
|
||||
* This file is part of twigc.
|
||||
*
|
||||
* @author dana geier <dana@dana.is>
|
||||
* @author dana <dana@dana.is>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace Dana\Twigc;
|
||||
|
||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
use Dana\Twigc\Application;
|
||||
|
||||
/**
|
||||
* Compiles twigc into an executable phar file.
|
||||
* Compile twigc to an executable phar.
|
||||
*
|
||||
* This clas is heavily inspired by Composer's Compiler:
|
||||
*
|
||||
|
@ -39,407 +40,321 @@ use Symfony\Component\Finder\Finder;
|
|||
* THE SOFTWARE.
|
||||
*/
|
||||
class PharCompiler {
|
||||
protected $output;
|
||||
protected $baseDir;
|
||||
protected $finderSort;
|
||||
protected $versionNumber;
|
||||
protected $versionCommit;
|
||||
protected $versionDate;
|
||||
protected $output;
|
||||
protected $baseDir;
|
||||
protected $finderSort;
|
||||
|
||||
/**
|
||||
* Object constructor.
|
||||
*
|
||||
* @param (bool) $verbose (optional) Whether to display verbose output.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function __construct($verbose = false) {
|
||||
$this->output = new ConsoleOutput();
|
||||
$this->baseDir = realpath(\Dana\Twigc\Twigc::BASE_DIR);
|
||||
$this->finderSort = function ($a, $b) {
|
||||
return strcmp(
|
||||
strtr($a->getRealPath(), '\\', '/'),
|
||||
strtr($b->getRealPath(), '\\', '/')
|
||||
);
|
||||
};
|
||||
/**
|
||||
* Object constructor.
|
||||
*
|
||||
* @param OutputInterface $output The output to write to.
|
||||
* @param bool $verbose (optional) Whether to display verbose output.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function __construct(OutputInterface $output, bool $verbose = false) {
|
||||
$this->output = $output;
|
||||
$this->baseDir = realpath(__DIR__ . '/../..');
|
||||
$this->finderSort = function ($a, $b) {
|
||||
return strcmp(
|
||||
strtr($a->getRealPath(), '\\', '/'),
|
||||
strtr($b->getRealPath(), '\\', '/')
|
||||
);
|
||||
};
|
||||
|
||||
if ( $verbose ) {
|
||||
$this->output->setVerbosity(ConsoleOutput::VERBOSITY_VERBOSE);
|
||||
}
|
||||
}
|
||||
if ( $verbose ) {
|
||||
$this->output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles the project into an executable phar file.
|
||||
*
|
||||
* @param string $pharFile
|
||||
* (optional) The path (absolute or relative to the CWD) to write the
|
||||
* resulting phar file to.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function compile($pharFile = 'twigc.phar') {
|
||||
$this->output->writeln('Compiling phar...');
|
||||
/**
|
||||
* Compiles the project into an executable phar file.
|
||||
*
|
||||
* @param string $phar
|
||||
* (optional) The path (absolute or relative to the CWD) to write the
|
||||
* resulting phar file to.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function compile(string $phar) {
|
||||
$this->output->writeln('Compiling phar...');
|
||||
|
||||
$this->output->writeln('', ConsoleOutput::VERBOSITY_VERBOSE);
|
||||
$this->extractVersionInformation();
|
||||
$this->output->writeln('', OutputInterface::VERBOSITY_VERBOSE);
|
||||
|
||||
if ( file_exists($pharFile) ) {
|
||||
unlink($pharFile);
|
||||
}
|
||||
if ( file_exists($phar) ) {
|
||||
unlink($phar);
|
||||
}
|
||||
|
||||
$phar = new \Phar($pharFile, 0, 'twigc.phar');
|
||||
$phar->setSignatureAlgorithm(\Phar::SHA1);
|
||||
$phar->startBuffering();
|
||||
$obj = new \Phar($phar, 0, basename($phar));
|
||||
$obj->setSignatureAlgorithm(\Phar::SHA1);
|
||||
$obj->startBuffering();
|
||||
|
||||
$this->output->writeln('', ConsoleOutput::VERBOSITY_VERBOSE);
|
||||
$this->output->writeln('Adding src files...');
|
||||
$this->addSrc($phar);
|
||||
$this->output->writeln('', OutputInterface::VERBOSITY_VERBOSE);
|
||||
$this->output->writeln('Adding src files...');
|
||||
$this->addSrc($obj);
|
||||
|
||||
$this->output->writeln('', ConsoleOutput::VERBOSITY_VERBOSE);
|
||||
$this->output->writeln('Adding vendor files...');
|
||||
$this->addVendor($phar);
|
||||
$this->output->writeln('', OutputInterface::VERBOSITY_VERBOSE);
|
||||
$this->output->writeln('Adding vendor files...');
|
||||
$this->addVendor($obj);
|
||||
|
||||
$this->output->writeln('', ConsoleOutput::VERBOSITY_VERBOSE);
|
||||
$this->output->writeln('Adding root files...');
|
||||
$this->addRoot($phar);
|
||||
$this->output->writeln('', OutputInterface::VERBOSITY_VERBOSE);
|
||||
$this->output->writeln('Adding root files...');
|
||||
$this->addRoot($obj);
|
||||
|
||||
$this->output->writeln('', ConsoleOutput::VERBOSITY_VERBOSE);
|
||||
$this->output->writeln('Adding bin files...');
|
||||
$this->addBin($phar);
|
||||
$this->output->writeln('', OutputInterface::VERBOSITY_VERBOSE);
|
||||
$this->output->writeln('Adding bin files...');
|
||||
$this->addBin($obj);
|
||||
|
||||
$phar->setStub($this->getStub());
|
||||
$phar->stopBuffering();
|
||||
$obj->setStub($this->getStub());
|
||||
$obj->stopBuffering();
|
||||
unset($obj);
|
||||
|
||||
unset($phar);
|
||||
chmod($phar, 0755);
|
||||
|
||||
chmod($pharFile, 0755);
|
||||
$this->output->writeln('', OutputInterface::VERBOSITY_VERBOSE);
|
||||
$this->output->writeln("Compiled to ${phar}.");
|
||||
}
|
||||
|
||||
$this->output->writeln('', ConsoleOutput::VERBOSITY_VERBOSE);
|
||||
$this->output->writeln("Compiled to ${pharFile}.");
|
||||
/**
|
||||
* Add a file to a phar archive.
|
||||
*
|
||||
* @param \Phar $phar
|
||||
* The phar file to add to.
|
||||
*
|
||||
* @param \SplFileInfo|string $file
|
||||
* The file to add, or its path.
|
||||
*
|
||||
* @param bool|null $strip
|
||||
* (optional) Whether to strip extraneous white space from the file in
|
||||
* order to reduce its size. The default is to auto-detect based on file
|
||||
* extension.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function addFile(\Phar $phar, $file, bool $strip = null) {
|
||||
if ( is_string($file) ) {
|
||||
$file = new \SplFileInfo($file);
|
||||
}
|
||||
|
||||
/*
|
||||
// Re-sign the phar with reproducible time stamps and signature
|
||||
$util = new Timestamps($pharFile);
|
||||
$util->updateTimestamps($this->versionDate);
|
||||
$util->save($pharFile, \Phar::SHA1);
|
||||
*/
|
||||
}
|
||||
// Strip the absolute base directory off the front of the path
|
||||
$prefix = $this->baseDir . DIRECTORY_SEPARATOR;
|
||||
$path = strtr(
|
||||
str_replace($prefix, '', $file->getRealPath()),
|
||||
'\\',
|
||||
'/'
|
||||
);
|
||||
|
||||
/**
|
||||
* Extracts the application version information from the git repository and
|
||||
* sets the associated object properties.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \RuntimeException if `git describe` fails
|
||||
* @throws \RuntimeException if `git log` fails
|
||||
* @throws \RuntimeException if `git log` fails (2)
|
||||
*/
|
||||
protected function extractVersionInformation() {
|
||||
$workDir = escapeshellarg(__DIR__);
|
||||
$this->output->writeln(
|
||||
"Adding file: ${path}",
|
||||
OutputInterface::VERBOSITY_VERBOSE
|
||||
);
|
||||
|
||||
// Get version number
|
||||
$output = [];
|
||||
exec("cd ${workDir} && git describe --tags --match='v*.*.*' --dirty='!'", $output, $ret);
|
||||
$content = file_get_contents($file);
|
||||
|
||||
if ( $ret !== 0 || empty($output) ) {
|
||||
$output = ['0.0.0'];
|
||||
}
|
||||
// Strip interpreter directives
|
||||
if ( strpos($path, 'bin/') === 0 ) {
|
||||
$content = preg_replace('%^#!/usr/bin/env php\s*%', '', $content);
|
||||
|
||||
$tokens = explode('-', trim($output[0]));
|
||||
// Replace build-date place-holder
|
||||
} elseif ( $path === 'src/Twigc/Application.php' ) {
|
||||
$date = new \DateTime('now', new \DateTimeZone('UTC'));
|
||||
|
||||
$this->versionNumber = rtrim(ltrim($tokens[0], 'v'), '!');
|
||||
$content = str_replace(
|
||||
'%BUILD_DATE%',
|
||||
$date->format('D Y-m-d H:i:s T'),
|
||||
$content
|
||||
);
|
||||
}
|
||||
|
||||
// If we're ahead of a tag, add the number of commits
|
||||
if ( count($tokens) > 1 ) {
|
||||
$this->versionNumber .= '-plus' . rtrim($tokens[1], '!');
|
||||
}
|
||||
if ( $strip === null ) {
|
||||
$strip = in_array($file->getExtension(), ['json', 'lock', 'php'], true);
|
||||
}
|
||||
|
||||
// If the index is dirty, add that
|
||||
if ( rtrim(implode('-', $tokens), '!') !== implode('-', $tokens) ) {
|
||||
$this->versionNumber .= '-dirty';
|
||||
}
|
||||
if ( $strip ) {
|
||||
$content = $this->stripWhiteSpace($content);
|
||||
}
|
||||
|
||||
// Get version last commit hash
|
||||
$output = [];
|
||||
exec("cd ${workDir} && git log -1 --pretty='%H' HEAD", $output, $ret);
|
||||
$phar->addFromString($path, $content);
|
||||
}
|
||||
|
||||
if ( $ret !== 0 || empty($output) ) {
|
||||
throw new \RuntimeException(
|
||||
'An error occurred whilst running `git log`'
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Remove extraneous white space from a string whilst preserving PHP line
|
||||
* numbers.
|
||||
*
|
||||
* @param string $source
|
||||
* The PHP or JSON string to strip white space from.
|
||||
*
|
||||
* @param string $type
|
||||
* (optional) The type of file the string represents. Available options
|
||||
* are 'php' and 'json'. The default is 'php'.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function stripWhiteSpace(
|
||||
string $source,
|
||||
string $type = 'php'
|
||||
): string {
|
||||
$output = '';
|
||||
|
||||
$this->versionCommit = trim($output[0]);
|
||||
if ( $type === 'json' ) {
|
||||
$output = json_encode(json_decode($json, true));
|
||||
|
||||
// Get version last commit date
|
||||
$output = [];
|
||||
exec("cd ${workDir} && git log -1 --pretty='%ci' HEAD", $output, $ret);
|
||||
return $output === null ? $source : $output . "\n";
|
||||
}
|
||||
|
||||
if ( $ret !== 0 || empty($output) ) {
|
||||
throw new \RuntimeException(
|
||||
'An error occurred whilst running `git log`'
|
||||
);
|
||||
}
|
||||
if ( ! function_exists('token_get_all') ) {
|
||||
return $source;
|
||||
}
|
||||
|
||||
$this->versionDate = new \DateTime(trim($output[0]));
|
||||
$this->versionDate->setTimezone(new \DateTimeZone('UTC'));
|
||||
foreach ( token_get_all($source) as $token ) {
|
||||
// Arbitrary text, return as-is
|
||||
if ( is_string($token) ) {
|
||||
$output .= $token;
|
||||
// Replace comments by empty lines
|
||||
} elseif ( in_array($token[0], [\T_COMMENT, \T_DOC_COMMENT]) ) {
|
||||
$output .= str_repeat("\n", substr_count($token[1], "\n"));
|
||||
// Collapse and normalise white-space
|
||||
} elseif ($token[0] === \T_WHITESPACE) {
|
||||
// Collapse consecutive spaces
|
||||
$space = preg_replace('/[ \t]+/', ' ', $token[1]);
|
||||
// Normalise new-lines to \n
|
||||
$space = preg_replace('/(?:\r\n|\r|\n)/', "\n", $space);
|
||||
// Trim leading spaces
|
||||
$space = preg_replace('/\n[ ]+/', "\n", $space);
|
||||
$output .= $space;
|
||||
// Anything else, return as-is
|
||||
} else {
|
||||
$output .= $token[1];
|
||||
}
|
||||
}
|
||||
|
||||
if ( $this->output->isVerbose() ) {
|
||||
$this->output->writeln(
|
||||
'Got version number: ' . $this->versionNumber
|
||||
);
|
||||
$this->output->writeln(
|
||||
'Got version commit: ' . $this->versionCommit
|
||||
);
|
||||
$this->output->writeln(
|
||||
'Got version date: ' . $this->versionDate->getTimestamp()
|
||||
);
|
||||
}
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a file to a phar.
|
||||
*
|
||||
* @param \Phar $phar
|
||||
* The phar file to add to.
|
||||
*
|
||||
* @param \SplFileInfo|string $file
|
||||
* The file to add, or its path.
|
||||
*
|
||||
* @param null|bool $strip
|
||||
* (optional) Whether to strip extraneous white space from the file in
|
||||
* order to reduce its size. The default is to auto-detect based on file
|
||||
* extension.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function addFile($phar, $file, $strip = null) {
|
||||
if ( is_string($file) ) {
|
||||
$file = new \SplFileInfo($file);
|
||||
}
|
||||
/**
|
||||
* Add files from src directory to a phar.
|
||||
*
|
||||
* @param \Phar $phar The phar file to add to.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function addSrc(\Phar $phar) {
|
||||
$finder = new Finder();
|
||||
$finder
|
||||
->files()
|
||||
->in($this->baseDir . '/src')
|
||||
->ignoreDotFiles(true)
|
||||
->ignoreVCS(true)
|
||||
->name('*.php')
|
||||
->notName('PharCompiler.php')
|
||||
->sort($this->finderSort)
|
||||
;
|
||||
|
||||
// Strip the absolute base directory off the front of the path
|
||||
$prefix = $this->baseDir . DIRECTORY_SEPARATOR;
|
||||
$path = strtr(
|
||||
str_replace($prefix, '', $file->getRealPath()),
|
||||
'\\',
|
||||
'/'
|
||||
);
|
||||
foreach ( $finder as $file ) {
|
||||
$this->addFile($phar, $file);
|
||||
}
|
||||
}
|
||||
|
||||
$this->output->writeln("Adding file: ${path}", ConsoleOutput::VERBOSITY_VERBOSE);
|
||||
/**
|
||||
* Adds files from vendor directory to a phar.
|
||||
*
|
||||
* @param \Phar $phar The phar file to add to.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function addVendor($phar) {
|
||||
$devPaths = (new ComposerHelper())->getDevPackages();
|
||||
$devPaths = array_map(function ($x) {
|
||||
return $x->name . '/';
|
||||
}, $devPaths);
|
||||
|
||||
$content = file_get_contents($file);
|
||||
$finder = new Finder();
|
||||
$finder
|
||||
->files()
|
||||
->in($this->baseDir . '/vendor')
|
||||
->ignoreDotFiles(true)
|
||||
->ignoreVCS(true)
|
||||
;
|
||||
|
||||
// Strip interpreter directives
|
||||
if ( strpos($path, 'bin/') === 0 ) {
|
||||
$content = preg_replace('%^#!/usr/bin/env php\s*%', '', $content);
|
||||
// Exclude files from dev packages
|
||||
foreach ( $devPaths as $path ) {
|
||||
$finder->notPath($path);
|
||||
}
|
||||
|
||||
// Replace version place-holders
|
||||
} elseif ( $path === 'src/Twigc/Twigc.php' ) {
|
||||
$content = str_replace(
|
||||
[
|
||||
'%version_number%',
|
||||
'%version_commit%',
|
||||
'%version_date%',
|
||||
],
|
||||
[
|
||||
$this->versionNumber,
|
||||
$this->versionCommit,
|
||||
$this->versionDate->format('Y-m-d H:i:s'),
|
||||
],
|
||||
$content
|
||||
);
|
||||
}
|
||||
$finder
|
||||
->exclude('bin')
|
||||
->exclude('doc')
|
||||
->exclude('docs')
|
||||
->exclude('test')
|
||||
->exclude('tests')
|
||||
->notPath('/^[^\/]+\/[^\/]+\/Tests?\//')
|
||||
->notName('*.c')
|
||||
->notName('*.h')
|
||||
->notName('*.m4')
|
||||
->notName('*.w32')
|
||||
->notName('*.xml.dist')
|
||||
->notName('build.xml')
|
||||
->notName('composer.json')
|
||||
->notName('composer.lock')
|
||||
->notName('travis-ci.xml')
|
||||
->notName('phpunit.xml')
|
||||
->notName('ChangeLog*')
|
||||
->notName('CHANGE*')
|
||||
->notName('*CONDUCT*')
|
||||
->notName('CONTRIBUT*')
|
||||
->notName('README*')
|
||||
->sort($this->finderSort)
|
||||
;
|
||||
|
||||
if ( $strip === null ) {
|
||||
$strip = in_array($file->getExtension(), ['json', 'lock', 'php'], true);
|
||||
}
|
||||
foreach ( $finder as $file ) {
|
||||
$this->addFile($phar, $file);
|
||||
}
|
||||
}
|
||||
|
||||
if ( $strip ) {
|
||||
$content = $this->stripWhiteSpace($content);
|
||||
}
|
||||
/**
|
||||
* Adds files from project root directory to a phar.
|
||||
*
|
||||
* @param \Phar $phar The phar file to add to.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function addRoot(\Phar $phar) {
|
||||
$this->addFile($phar, $this->baseDir . '/composer.json');
|
||||
$this->addFile($phar, $this->baseDir . '/composer.lock');
|
||||
}
|
||||
|
||||
$phar->addFromString($path, $content);
|
||||
}
|
||||
/**
|
||||
* Adds files from bin directory to a phar.
|
||||
*
|
||||
* @param \Phar $phar The phar file to add to.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function addBin(\Phar $phar) {
|
||||
$this->addFile($phar, $this->baseDir . '/bin/twigc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes extraneous white space from a string whilst preserving PHP line
|
||||
* numbers.
|
||||
*
|
||||
* @param string $source
|
||||
* The PHP or JSON string to strip white space from.
|
||||
*
|
||||
* @param string $type
|
||||
* (optional) The type of file the string represents. Available options
|
||||
* are 'php' and 'json'. The default is 'php'.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function stripWhiteSpace($source, $type = 'php') {
|
||||
$output = '';
|
||||
/**
|
||||
* Returns the phar stub.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getStub() {
|
||||
$stub = "
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
if ( $type === 'json' ) {
|
||||
$output = json_encode(json_decode($json, true));
|
||||
/**
|
||||
* This file is part of twigc.
|
||||
*
|
||||
* @author dana <dana@dana.is>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
return $output === null ? $source : $output . "\n";
|
||||
}
|
||||
|
||||
if ( ! function_exists('token_get_all') ) {
|
||||
return $source;
|
||||
}
|
||||
|
||||
foreach ( token_get_all($source) as $token ) {
|
||||
// Arbitrary text, return as-is
|
||||
if ( is_string($token) ) {
|
||||
$output .= $token;
|
||||
// Replace comments by empty lines
|
||||
} elseif ( in_array($token[0], [\T_COMMENT, \T_DOC_COMMENT]) ) {
|
||||
$output .= str_repeat("\n", substr_count($token[1], "\n"));
|
||||
// Collapse and normalise white-space
|
||||
} elseif (T_WHITESPACE === $token[0]) {
|
||||
// Collapse consecutive spaces
|
||||
$space = preg_replace('#[ \t]+#', ' ', $token[1]);
|
||||
// Normalise new-lines to \n
|
||||
$space = preg_replace('#(?:\r\n|\r|\n)#', "\n", $space);
|
||||
// Trim leading spaces
|
||||
$space = preg_replace('#\n[ ]+#', "\n", $space);
|
||||
$output .= $space;
|
||||
// Anything else, return as-is
|
||||
} else {
|
||||
$output .= $token[1];
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds files from src directory to a phar.
|
||||
*
|
||||
* @param \Phar $phar The phar file to add to.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function addSrc($phar) {
|
||||
$finder = new Finder();
|
||||
$finder
|
||||
->files()
|
||||
->in($this->baseDir . '/src')
|
||||
->ignoreDotFiles(true)
|
||||
->ignoreVCS(true)
|
||||
->name('*.php')
|
||||
->notName('PharCompiler.php')
|
||||
->sort($this->finderSort)
|
||||
;
|
||||
|
||||
foreach ( $finder as $file ) {
|
||||
$this->addFile($phar, $file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds files from vendor directory to a phar.
|
||||
*
|
||||
* @param \Phar $phar The phar file to add to.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function addVendor($phar) {
|
||||
$devPaths = \Dana\Twigc\Twigc::getComposerDevPackages();
|
||||
$devPaths = array_map(function ($x) {
|
||||
return $x->name . '/';
|
||||
}, $devPaths);
|
||||
|
||||
$finder = new Finder();
|
||||
$finder
|
||||
->files()
|
||||
->in($this->baseDir . '/vendor')
|
||||
->ignoreDotFiles(true)
|
||||
->ignoreVCS(true)
|
||||
;
|
||||
|
||||
// Exclude files from dev packages
|
||||
foreach ( $devPaths as $path ) {
|
||||
$finder->notPath($path);
|
||||
}
|
||||
|
||||
$finder
|
||||
->exclude('bin')
|
||||
->exclude('doc')
|
||||
->exclude('docs')
|
||||
->exclude('test')
|
||||
->exclude('tests')
|
||||
->notPath('/^[^\/]+\/[^\/]+\/Tests?\//')
|
||||
->notName('*.c')
|
||||
->notName('*.h')
|
||||
->notName('*.m4')
|
||||
->notName('*.w32')
|
||||
->notName('*.xml.dist')
|
||||
->notName('build.xml')
|
||||
->notName('composer.json')
|
||||
->notName('composer.lock')
|
||||
->notName('travis-ci.xml')
|
||||
->notName('phpunit.xml')
|
||||
->notName('ChangeLog*')
|
||||
->notName('CHANGE*')
|
||||
->notName('*CONDUCT*')
|
||||
->notName('CONTRIBUT*')
|
||||
->notName('README*')
|
||||
->sort($this->finderSort)
|
||||
;
|
||||
|
||||
foreach ( $finder as $file ) {
|
||||
$this->addFile($phar, $file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds files from project root directory to a phar.
|
||||
*
|
||||
* @param \Phar $phar The phar file to add to.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function addRoot($phar) {
|
||||
$this->addFile($phar, $this->baseDir . '/composer.json');
|
||||
$this->addFile($phar, $this->baseDir . '/composer.lock');
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds files from bin directory to a phar.
|
||||
*
|
||||
* @param \Phar $phar The phar file to add to.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function addBin($phar) {
|
||||
$this->addFile($phar, $this->baseDir . '/bin/twigc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the phar stub.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getStub() {
|
||||
$stub = "
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of twigc.
|
||||
*
|
||||
* @author dana geier <dana@dana.is>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
\Phar::mapPhar('twigc.phar');
|
||||
require 'phar://twigc.phar/bin/twigc';
|
||||
__HALT_COMPILER();
|
||||
";
|
||||
|
||||
return str_replace("\t", '', trim($stub)) . "\n";
|
||||
}
|
||||
\Phar::mapPhar('twigc.phar');
|
||||
require 'phar://twigc.phar/bin/twigc';
|
||||
__HALT_COMPILER();
|
||||
";
|
||||
return preg_replace('/^\s+/m', '', trim($stub)) . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,88 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of twigc.
|
||||
*
|
||||
* @author dana geier <dana@dana.is>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace Dana\Twigc;
|
||||
|
||||
/**
|
||||
* Holds various project-specific constants and methods.
|
||||
*/
|
||||
class Twigc {
|
||||
const BASE_DIR = __DIR__ . '/../..';
|
||||
const VERSION_NUMBER = '%version_number%';
|
||||
const VERSION_COMMIT = '%version_commit%';
|
||||
const VERSION_DATE = '%version_date%';
|
||||
|
||||
/**
|
||||
* Returns an array of data representing the project's Composer lock file.
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws \RuntimeException if composer.lock doesn't exist
|
||||
* @throws \RuntimeException if composer.lock can't be decoded
|
||||
*/
|
||||
private static function parseComposerLock() {
|
||||
$lockFile = static::BASE_DIR . '/composer.lock';
|
||||
|
||||
if ( ! file_exists($lockFile) ) {
|
||||
throw new \RuntimeException('Missing ' . basename($lockFile));
|
||||
}
|
||||
|
||||
$installed = json_decode(file_get_contents($lockFile), true);
|
||||
|
||||
if ( empty($installed) || ! isset($installed['packages']) ) {
|
||||
throw new \RuntimeException('Error decoding ' . basename($lockFile));
|
||||
}
|
||||
|
||||
return $installed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts and object-ifies an array of package data.
|
||||
*
|
||||
* @param array $packages Package data from composer.lock.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private static function massagePackages(array $packages) {
|
||||
usort($packages, function ($a, $b) {
|
||||
return strcasecmp($a['name'], $b['name']);
|
||||
});
|
||||
|
||||
foreach ( $packages as &$package ) {
|
||||
$package = (object) $package;
|
||||
}
|
||||
|
||||
return $packages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of installed non-dev Composer packages based on the
|
||||
* project's Composer lock file.
|
||||
*
|
||||
* @return object[] An array of objects representing Composer packages.
|
||||
*/
|
||||
public static function getComposerPackages() {
|
||||
$packages = static::parseComposerLock()['packages'];
|
||||
|
||||
return static::massagePackages($packages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of installed dev Composer packages based on the
|
||||
* project's lock file.
|
||||
*
|
||||
* @return object[] An array of objects representing Composer packages.
|
||||
*/
|
||||
public static function getComposerDevPackages() {
|
||||
$packages = static::parseComposerLock()['packages-dev'];
|
||||
|
||||
return static::massagePackages($packages);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,62 +3,57 @@
|
|||
/**
|
||||
* This file is part of twigc.
|
||||
*
|
||||
* @author dana geier <dana@dana.is>
|
||||
* @author dana <dana@dana.is>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Helper function for printing an error message.
|
||||
* Print an error message.
|
||||
*
|
||||
* Uses fprintf() to print to STDERR if available; uses echo otherwise.
|
||||
* Uses fprintf() to print to stderr if available; uses echo otherwise.
|
||||
*
|
||||
* @param string $string
|
||||
* (optional) The message to print. Will be passed through rtrim(); if the
|
||||
* result is an empty string, only an empty line will be printed; otherwise,
|
||||
* the text 'twigc: ' will be appended to the beginning.
|
||||
* (optional) The message to print. Is passed through rtrim(); if the result
|
||||
* is an empty string, only an empty line is printed; otherwise, the text
|
||||
* 'twigc: ' is appended to the beginning.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function twigc_puts_error($string = '') {
|
||||
$string = rtrim($string);
|
||||
$string = $string === '' ? '' : "twigc: ${string}";
|
||||
$string = rtrim($string);
|
||||
$string = $string === '' ? '' : "twigc: ${string}";
|
||||
|
||||
// \STDERR only exists when we're using the CLI SAPI
|
||||
if ( defined('\\STDERR') ) {
|
||||
fprintf(\STDERR, "%s\n", $string);
|
||||
} else {
|
||||
echo $string, "\n";
|
||||
}
|
||||
if ( defined('\\STDERR') ) {
|
||||
fprintf(\STDERR, "%s\n", $string);
|
||||
} else {
|
||||
echo $string, "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Find Composer's auto-loader
|
||||
$autoloaders = [
|
||||
// Phar/repo path
|
||||
__DIR__ . '/../vendor/autoload.php',
|
||||
// Composer path
|
||||
__DIR__ . '/../../../autoload.php',
|
||||
];
|
||||
|
||||
foreach ( $autoloaders as $autoloader ) {
|
||||
if ( file_exists($autoloader) ) {
|
||||
define('TWIGC_AUTOLOADER', $autoloader);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
unset($autoloaders, $autoloader);
|
||||
|
||||
// Disallow running from non-CLI SAPIs
|
||||
if ( \PHP_SAPI !== 'cli' ) {
|
||||
twigc_puts_error("This tool must be invoked via PHP's CLI SAPI.");
|
||||
exit(1);
|
||||
twigc_puts_error("This tool must be invoked via PHP's CLI SAPI.");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Give a meaningful error if we don't have the auto-loader
|
||||
if ( ! defined('TWIGC_AUTOLOADER') ) {
|
||||
twigc_puts_error('Auto-loader is missing — try running `composer install`.');
|
||||
exit(1);
|
||||
(function () {
|
||||
$paths = [
|
||||
// Phar/repo path
|
||||
__DIR__ . '/../vendor/autoload.php',
|
||||
// Composer path
|
||||
__DIR__ . '/../../../autoload.php',
|
||||
];
|
||||
|
||||
foreach ( $paths as $path ) {
|
||||
if ( file_exists($path) ) {
|
||||
define('TWIGC_AUTOLOADER', $path);
|
||||
return;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
if ( ! defined('\\TWIGC_AUTOLOADER') ) {
|
||||
twigc_puts_error('Auto-loader is missing — try running `composer install`.');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
require_once TWIGC_AUTOLOADER;
|
||||
|
||||
require_once \TWIGC_AUTOLOADER;
|
||||
|
|
462
tests/ApplicationTest.php
Normal file
462
tests/ApplicationTest.php
Normal file
|
@ -0,0 +1,462 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of twigc.
|
||||
*
|
||||
* @author dana <dana@dana.is>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace Dana\Test\Twigc;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
|
||||
use Dana\Twigc\Application;
|
||||
|
||||
/**
|
||||
* Tests for twigc.
|
||||
*
|
||||
* This doesn't feel very 'DRY'. Would like to improve it somehow.
|
||||
*/
|
||||
class ApplicationTest extends TestCase {
|
||||
protected $output;
|
||||
protected $app;
|
||||
protected $template;
|
||||
protected $tempDir;
|
||||
protected $tempFiles = [];
|
||||
|
||||
/**
|
||||
* Set up before tests.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function setUp() {
|
||||
$this->output = new BufferedOutput();
|
||||
$this->app = new Application();
|
||||
$this->template = $this->makeFile('default', 'testEnv: {{ testEnv }}');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tear down after tests.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function tearDown() {
|
||||
$this->output->fetch();
|
||||
|
||||
// This is slow, but i'm too lazy to handle recursive deletion properly
|
||||
if ( ! empty($this->tempFiles) ) {
|
||||
$dir = escapeshellarg($this->tempDir);
|
||||
exec("rm -rf ${dir}/?* 2> /dev/null");
|
||||
$this->tempFiles = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a temporary file, and optionally populate it with data.
|
||||
*
|
||||
* @param string $name
|
||||
* The temporary file name/suffix. If the name contains an internal slash,
|
||||
* all leading directories are created.
|
||||
*
|
||||
* @param string|null $content
|
||||
* (optional) Any data to populate the file with.
|
||||
*
|
||||
* @return string The name of the created file.
|
||||
*/
|
||||
protected function makeFile(string $name, string $data = null): string {
|
||||
// Create our temp directory if we don't already have it
|
||||
if ( ! $this->tempDir ) {
|
||||
$rand = base64_encode(random_bytes(32));
|
||||
$rand = substr(str_replace(['/', '+', '='], '', $rand), 0, 10);
|
||||
$this->tempDir = sys_get_temp_dir();
|
||||
$this->tempDir .= "/Dana.Test.Twigc.ApplicationTest.${rand}";
|
||||
|
||||
if ( ! is_dir($this->tempDir) ) {
|
||||
mkdir($this->tempDir, 0700);
|
||||
}
|
||||
}
|
||||
|
||||
$dir = $this->tempDir;
|
||||
$name = trim($name, '/.');
|
||||
|
||||
if ( strlen($name) === 0 ) {
|
||||
throw \RuntimeException('Expected file name');
|
||||
}
|
||||
|
||||
if ( strpos($name, '/') !== false ) {
|
||||
$dir .= '/' . dirname($name);
|
||||
|
||||
if ( ! is_dir($dir) ) {
|
||||
mkdir($dir, 0700, true);
|
||||
}
|
||||
|
||||
$name = basename($name);
|
||||
}
|
||||
|
||||
$file = "${dir}/${name}";
|
||||
$this->tempFiles[] = $file;
|
||||
|
||||
if ( $data !== null ) {
|
||||
if ( file_put_contents($file, $data, \LOCK_EX) === false ) {
|
||||
throw \RuntimeException("Write failed: ${file}");
|
||||
}
|
||||
} elseif ( touch($file) === false ) {
|
||||
throw \RuntimeException("Write failed: ${file}");
|
||||
}
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the application and return common test data.
|
||||
*
|
||||
* @param $args
|
||||
* (optional) Zero or more arguments to pass to the application. This should
|
||||
* NOT include argv[0].
|
||||
*
|
||||
* @return array
|
||||
* An array containing the application return status as an integer, the raw
|
||||
* output as a string, and the output as an array of lines.
|
||||
*/
|
||||
protected function runApp(...$args) {
|
||||
$argv = ['', '-e', 'none'];
|
||||
|
||||
if ( is_array($args[0]) ) {
|
||||
$argv = array_merge($argv, $args[0]);
|
||||
} else {
|
||||
$argv = array_merge($argv, $args);
|
||||
}
|
||||
|
||||
$argv = array_filter($argv, function ($v) {
|
||||
return $v !== null;
|
||||
});
|
||||
|
||||
$ret = $this->app->run($this->output, $argv);
|
||||
$buffer = $this->output->fetch();
|
||||
$lines = explode("\n", rtrim($buffer, "\r\n"));
|
||||
|
||||
return [$ret, $buffer, $lines];
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide data for testEscape().
|
||||
*
|
||||
* All tests assume the following input data (value is literal):
|
||||
*
|
||||
* testEnv="<foo$bar>"
|
||||
*
|
||||
* All tests assume the following template:
|
||||
*
|
||||
* testEnv: {{ testEnv }}
|
||||
*
|
||||
* @return array[]
|
||||
*/
|
||||
public function provideTestEscape() {
|
||||
return [
|
||||
// Escape method: none
|
||||
['f', 'testEnv: "<foo$bar>"'],
|
||||
['false', 'testEnv: "<foo$bar>"'],
|
||||
['n', 'testEnv: "<foo$bar>"'],
|
||||
['no', 'testEnv: "<foo$bar>"'],
|
||||
['none', 'testEnv: "<foo$bar>"'],
|
||||
['never', 'testEnv: "<foo$bar>"'],
|
||||
|
||||
// Escape method: html
|
||||
['always', 'testEnv: "<foo$bar>"'],
|
||||
['t', 'testEnv: "<foo$bar>"'],
|
||||
['true', 'testEnv: "<foo$bar>"'],
|
||||
['y', 'testEnv: "<foo$bar>"'],
|
||||
['yes', 'testEnv: "<foo$bar>"'],
|
||||
['html', 'testEnv: "<foo$bar>"'],
|
||||
|
||||
// Escape method: css
|
||||
['css', 'testEnv: \\22 \\3C foo\\24 bar\\3E \\22'],
|
||||
|
||||
// Escape method: html_attr
|
||||
['html_attr', 'testEnv: "<foo$bar>"'],
|
||||
|
||||
// Escape method: js
|
||||
['js', 'testEnv: \\x22\\x3Cfoo\\x24bar\\x3E\\x22'],
|
||||
|
||||
// Escape method: json
|
||||
['json', 'testEnv: "\"<foo$bar>\""'],
|
||||
|
||||
// Escape method: sh
|
||||
['sh', 'testEnv: "\"<foo\$bar>\""'],
|
||||
|
||||
// Escape method: url
|
||||
['url', 'testEnv: %22%3Cfoo%24bar%3E%22'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test `-h` / `--help` function.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testHelp() {
|
||||
foreach ( ['-h', '--help'] as $opt ) {
|
||||
list($ret, $buffer, $lines) = $this->runApp($opt);
|
||||
|
||||
$this->assertSame(0, $ret);
|
||||
$this->assertGreaterThan(3, count($lines));
|
||||
$this->assertContains('--help', $buffer);
|
||||
$this->assertContains('--version', $buffer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test `-V` / `--version` function.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testVersion() {
|
||||
foreach ( ['-V', '--version'] as $opt ) {
|
||||
list($ret, $buffer, $lines) = $this->runApp($opt);
|
||||
|
||||
$this->assertSame(0, $ret);
|
||||
$this->assertSame(1, count($lines));
|
||||
$this->assertContains(' version ', $lines[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test `--credits` function.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testCredits() {
|
||||
list($ret, $buffer, $lines) = $this->runApp('--credits');
|
||||
|
||||
$this->assertSame(0, $ret);
|
||||
$this->assertGreaterThan(1, count($lines));
|
||||
$this->assertContains('licence', $lines[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test `-d` / `--dir` function, as well as default include-directory
|
||||
* functionality.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testDir() {
|
||||
$templateSame = $this->makeFile('dir1/a.twig', '{% include "b.twig" %}');
|
||||
$includeSame = $this->makeFile('dir1/b.twig', 'included: {{ testEnv }}');
|
||||
|
||||
$templateDiff = $this->makeFile('dir2/a.twig', '{% include "b.twig" %}');
|
||||
$includeDiff = $this->makeFile('dir3/b.twig', 'included: {{ testEnv }}');
|
||||
|
||||
// $includeSame's directory should be searched by default
|
||||
list($ret, $buffer, $lines) = $this->runApp(
|
||||
'-p',
|
||||
'testEnv=abc123',
|
||||
$templateSame
|
||||
);
|
||||
$this->assertSame(0, $ret);
|
||||
|
||||
// $includeDiff's directory should have to be specified manually
|
||||
list($ret, $buffer, $lines) = $this->runApp(
|
||||
'-p',
|
||||
'testEnv=abc123',
|
||||
$templateDiff
|
||||
);
|
||||
$this->assertNotSame(0, $ret);
|
||||
|
||||
// Now we confirm that that works
|
||||
foreach ( ['-d', '--dir'] as $opt ) {
|
||||
list($ret, $buffer, $lines) = $this->runApp(
|
||||
$opt,
|
||||
dirname($includeDiff),
|
||||
'-p',
|
||||
'testEnv=abc123',
|
||||
$templateDiff
|
||||
);
|
||||
|
||||
$this->assertSame(0, $ret);
|
||||
$this->assertSame(1, count($lines));
|
||||
$this->assertContains('included: abc123', $lines[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test `-E` / `--env` function.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testEnv() {
|
||||
foreach ( ['-E', '--env'] as $opt ) {
|
||||
// This option can't be set at run time; we'll just test what we have
|
||||
if ( strpos(ini_get('variables_order'), 'E') === false ) {
|
||||
list($ret, $buffer, $lines) = $this->runApp($opt, $this->template);
|
||||
|
||||
$this->assertGreaterThan(0, $ret);
|
||||
$this->assertContains('variables_order', $buffer);
|
||||
return;
|
||||
}
|
||||
|
||||
$_ENV['testEnv'] = 'abc123';
|
||||
list($ret, $buffer, $lines) = $this->runApp($opt, $this->template);
|
||||
|
||||
$this->assertSame(0, $ret);
|
||||
$this->assertSame(1, count($lines));
|
||||
$this->assertContains('testEnv: abc123', $lines[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test `-j` / `--json` function (dictionary string).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testJsonDict() {
|
||||
foreach ( ['-j', '--json'] as $opt ) {
|
||||
list($ret, $buffer, $lines) = $this->runApp(
|
||||
$opt,
|
||||
'{"testEnv": "abc123"}',
|
||||
$this->template
|
||||
);
|
||||
|
||||
$this->assertSame(0, $ret);
|
||||
$this->assertSame(1, count($lines));
|
||||
$this->assertContains('testEnv: abc123', $lines[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test `-j` / `--json` function (file).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testJsonFile() {
|
||||
$jsonFile = $this->makeFile('json', '{"testEnv": "abc123"}');
|
||||
|
||||
foreach ( ['-j', '--json'] as $opt ) {
|
||||
list($ret, $buffer, $lines) = $this->runApp(
|
||||
$opt,
|
||||
$jsonFile,
|
||||
$this->template
|
||||
);
|
||||
|
||||
$this->assertSame(0, $ret);
|
||||
$this->assertSame(1, count($lines));
|
||||
$this->assertContains('testEnv: abc123', $lines[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test `-p` / `--pair` function.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testPair() {
|
||||
foreach ( ['-p', '--pair'] as $opt ) {
|
||||
list($ret, $buffer, $lines) = $this->runApp(
|
||||
$opt,
|
||||
'testEnv=abc123',
|
||||
$this->template
|
||||
);
|
||||
|
||||
$this->assertSame(0, $ret);
|
||||
$this->assertSame(1, count($lines));
|
||||
$this->assertContains('testEnv: abc123', $lines[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test `--query` function.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testQuery() {
|
||||
list($ret, $buffer, $lines) = $this->runApp(
|
||||
'--query',
|
||||
'?testEnv=abc123&testEnv2=x&testEnv3=y',
|
||||
$this->template
|
||||
);
|
||||
|
||||
$this->assertSame(0, $ret);
|
||||
$this->assertSame(1, count($lines));
|
||||
$this->assertContains('testEnv: abc123', $lines[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test input-data precedence.
|
||||
*
|
||||
* Input precedence should be as follows (ascending):
|
||||
*
|
||||
* env -> query -> json -> pair
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testInputDataPrecedence() {
|
||||
list($ret, $buffer, $lines) = $this->runApp(
|
||||
'--pair',
|
||||
'testEnv=aaa',
|
||||
'--query',
|
||||
'?testEnv=bbb',
|
||||
'--json',
|
||||
'{ "testEnv": "ccc" }',
|
||||
$this->template
|
||||
);
|
||||
|
||||
$this->assertSame(0, $ret);
|
||||
$this->assertSame(1, count($lines));
|
||||
$this->assertContains('testEnv: aaa', $lines[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test handling of undefined variable WITHOUT `-s` / `--strict`.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testUndefinedNoStrict() {
|
||||
list($ret, $buffer, $lines) = $this->runApp($this->template);
|
||||
|
||||
$this->assertSame(0, $ret);
|
||||
$this->assertSame(1, count($lines));
|
||||
$this->assertContains('testEnv:', $lines[0]);
|
||||
$this->assertNotContains('abc123', $lines[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test handling of undefined variable WITHOUT `-s` / `--strict`.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testUndefinedStrict() {
|
||||
foreach ( ['-s', '--strict'] as $opt ) {
|
||||
list($ret, $buffer, $lines) = $this->runApp($opt, $this->template);
|
||||
|
||||
$this->assertSame(1, $ret);
|
||||
$this->assertContains('testEnv', $lines[0]);
|
||||
$this->assertNotContains('abc123', $lines[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test various escape methods with `-e` / `--escape`.
|
||||
*
|
||||
* @param string $method The method to test.
|
||||
* @param string $expected The expected output.
|
||||
*
|
||||
* @dataProvider provideTestEscape
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testEcape(string $method, string $expected) {
|
||||
foreach ( ['-e', '--escape'] as $opt ) {
|
||||
list($ret, $buffer, $lines) = $this->runApp(
|
||||
$opt,
|
||||
$method,
|
||||
'-p',
|
||||
'testEnv="<foo$bar>"',
|
||||
$this->template
|
||||
);
|
||||
|
||||
$this->assertSame(0, $ret);
|
||||
$this->assertContains($expected, $lines[0] ?? '');
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue