mirror of
https://github.com/okdana/twigc.git
synced 2024-06-14 11:45:12 +02:00
Compare commits
20 commits
Author | SHA1 | Date | |
---|---|---|---|
bb6feca8f4 | |||
05ce587f84 | |||
c7f9eb7514 | |||
c76ce18517 | |||
5de5948cdd | |||
3a89dfaf37 | |||
d855eca3cc | |||
dd16763ae9 | |||
532b078376 | |||
33e945b4bc | |||
aa45391808 | |||
97f8acb902 | |||
9cbe7e75f9 | |||
37528fc72e | |||
497b9d5e54 | |||
f453680b04 | |||
f294101183 | |||
281b50438a | |||
dfcf0349ca | |||
593f69d0c1 |
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -1,17 +1,15 @@
|
||||||
# PHP-related files
|
# PHP-related files
|
||||||
.idea/
|
|
||||||
.phpintel/
|
|
||||||
/vendor/
|
/vendor/
|
||||||
/phpunit.xml
|
/phpunit.xml
|
||||||
/twigc.phar
|
/twigc.phar
|
||||||
|
|
||||||
# OS X-generated files
|
# macOS-generated files
|
||||||
._*
|
._*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.DS_Store?
|
.DS_Store?
|
||||||
.Spotlight-V100
|
.Spotlight-V100
|
||||||
.Trashes
|
.Trashes
|
||||||
Icon\?
|
Icon?
|
||||||
|
|
||||||
# Windows-generated files
|
# Windows-generated files
|
||||||
Desktop.ini
|
Desktop.ini
|
||||||
|
@ -19,7 +17,8 @@ ehthumbs.db
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
# Editor-generated files
|
# Editor-generated files
|
||||||
|
.idea/
|
||||||
|
.phpintel/
|
||||||
*~
|
*~
|
||||||
.*.s[a-w][a-z]
|
.*.s[a-w][a-z]
|
||||||
*.sublime-workspace
|
*.sublime-workspace
|
||||||
|
|
||||||
|
|
3
LICENCE
3
LICENCE
|
@ -1,6 +1,6 @@
|
||||||
The MIT License (MIT)
|
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
|
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
|
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
|
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
|
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.
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
|
45
Makefile
45
Makefile
|
@ -1,21 +1,49 @@
|
||||||
##
|
##
|
||||||
# This file is part of twigc.
|
# This file is part of twigc.
|
||||||
#
|
#
|
||||||
# @author dana geier <dana@dana.is>
|
# @author dana <dana@dana.is>
|
||||||
# @license MIT
|
# @license MIT
|
||||||
|
|
||||||
all: build
|
prefix ?= /usr/local
|
||||||
|
bindir ?= $(prefix)/bin
|
||||||
|
|
||||||
|
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:
|
vendor:
|
||||||
composer install
|
composer install
|
||||||
|
|
||||||
twigc.phar: vendor
|
twigc.phar:
|
||||||
php bin/compile
|
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
|
||||||
cp twigc.phar /usr/local/bin/twigc
|
./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 $(DESTDIR)$(bindir)/twigc
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f twigc.phar
|
rm -f twigc.phar
|
||||||
|
@ -23,5 +51,4 @@ clean:
|
||||||
distclean: clean
|
distclean: clean
|
||||||
rm -rf vendor/
|
rm -rf vendor/
|
||||||
|
|
||||||
.PHONY: all build install clean distclean
|
.PHONY: all build clean distclean help install phar test test-integration test-unit
|
||||||
|
|
||||||
|
|
225
README.md
225
README.md
|
@ -5,98 +5,201 @@
|
||||||
for interacting with Twig through shell scripts and other command-line
|
for interacting with Twig through shell scripts and other command-line
|
||||||
applications.
|
applications.
|
||||||
|
|
||||||
## Usage overview
|
## Usage
|
||||||
|
|
||||||
```
|
|
||||||
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)
|
|
||||||
-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
|
|
||||||
```
|
|
||||||
|
|
||||||
**twigc** can render Twig templates supplied via either standard input or a file
|
**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 }}!'
|
% twigc -p 'name=dana' <<< 'Hello, {{ name }}!'
|
||||||
Hello, dana!
|
Hello, dana!
|
||||||
```
|
```
|
||||||
|
|
||||||
Of course, only simple string values can be provided this way. For more complex
|
Of course, only basic string values can be provided this way. For more complex
|
||||||
data, you can use the JSON option:
|
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!
|
1... 2... 3!
|
||||||
```
|
```
|
||||||
|
|
||||||
JSON data can also be provided by file path or on standard input:
|
JSON data can also be provided by file path or on standard input:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
# JSON from file, template from standard input
|
||||||
% cat numbers.json
|
% cat numbers.json
|
||||||
{ "numbers": [1, 2, 3] }
|
{"numbers": [1, 2, 3]}
|
||||||
% twigc -j numbers.json <<< '{{ numbers|join("... ") }}!'
|
% twigc -j numbers.json <<< '{{ numbers|join("... ") }}!'
|
||||||
1... 2... 3!
|
1... 2... 3!
|
||||||
|
|
||||||
|
# JSON from standard input, template from file
|
||||||
% cat numbers.twig
|
% cat numbers.twig
|
||||||
{{ numbers|join("... ") }}!
|
{{ 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)
|
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
|
during rendering based on the template file extension (or disabled by default if
|
||||||
input), but this is configurable:
|
using standard input), but this is configurable:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
# No auto-escaping by default on standard input
|
||||||
% twigc -p 'html=<p>Hello!</p>' <<< '{{ html }}'
|
% twigc -p 'html=<p>Hello!</p>' <<< '{{ html }}'
|
||||||
<p>Hello!</p>
|
<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>
|
<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
|
\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
|
%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
|
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 }}!'
|
% twigc <<< 'Hello, {{ name }}!'
|
||||||
Hello, !
|
Hello, !
|
||||||
|
|
||||||
% twigc -s <<< 'Hello, {{ name }}!'
|
% twigc -s <<< 'Hello, {{ name }}!'
|
||||||
[Twig_Error_Runtime]
|
twigc: Variable "name" does not exist in "-" at line 1.
|
||||||
Variable "name" does not exist in "-" at line 1
|
|
||||||
```
|
```
|
||||||
|
|
||||||
If a template file name was provided, the file's parent directory is used for
|
Use of this option is recommended for reliability in scripting scenarios.
|
||||||
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
|
### Specifying search directories
|
||||||
explicitly supplied on the command line:
|
|
||||||
|
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
|
% cat include.twig
|
||||||
Hello!
|
Hello!
|
||||||
|
|
||||||
% twigc <<< '{% include "include.twig" %}'
|
% twigc <<< '{% include "include.twig" %}'
|
||||||
[Twig_Error_Loader]
|
twigc: Template "include.twig" is not defined in "-" at line 1.
|
||||||
Template "include.twig" is not defined in "-" at line 1.
|
|
||||||
|
|
||||||
% twigc -d '.' <<< '{% include "include.twig" %}'
|
% twigc -d '.' <<< '{% include "include.twig" %}'
|
||||||
Hello!
|
Hello!
|
||||||
|
@ -107,7 +210,7 @@ Hello!
|
||||||
**twigc** is provided as a self-contained executable archive; to download it,
|
**twigc** is provided as a self-contained executable archive; to download it,
|
||||||
see the [releases](https://github.com/okdana/twigc/releases) page.
|
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
|
% git clone https://github.com/okdana/twigc
|
||||||
|
@ -117,12 +220,40 @@ Of course, you can also build and install it from source:
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
The **twigc** executable is bundled with Twig and all of its other dependencies;
|
The **twigc** executable archive is bundled with Twig and all of its other
|
||||||
the only thing you need to run it is PHP version 5.5 or higher.
|
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
|
## 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`.
|
of its dependencies, run `twigc --credits`.
|
||||||
|
|
||||||
The `\Dana\Twigc\PharCompiler` class used to build the executable archive is
|
The `\Dana\Twigc\PharCompiler` class used to build the executable archive is
|
||||||
|
@ -133,17 +264,21 @@ based on
|
||||||
|
|
||||||
* [twigphp/Twig](https://github.com/twigphp/Twig) —
|
* [twigphp/Twig](https://github.com/twigphp/Twig) —
|
||||||
The Twig project on GitHub.
|
The Twig project on GitHub.
|
||||||
|
|
||||||
* [farazdagi/twig-cli](https://github.com/farazdagi/twig-cli) —
|
* [farazdagi/twig-cli](https://github.com/farazdagi/twig-cli) —
|
||||||
Another project that aims to bring Twig to the command line. It's actually
|
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
|
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) —
|
* [twigjs/twig.js](https://github.com/twigjs/twig.js) —
|
||||||
A pure JavaScript implementation of Twig. It comes with its own command-line
|
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
|
tool, `twigjs`, which can be used to render Twig templates, but it's quite
|
||||||
limited.
|
limited.
|
||||||
|
|
||||||
* [indigojs/twig-cli](https://github.com/indigojs/twig-cli) —
|
* [indigojs/twig-cli](https://github.com/indigojs/twig-cli) —
|
||||||
Another command-line Twig renderer based on Twig.js. Its functionality is very
|
Another command-line Twig renderer based on Twig.js. Its functionality is very
|
||||||
similar to (almost exactly the same as?) `twigjs`.
|
similar to (almost exactly the same as?) `twigjs`.
|
||||||
|
|
||||||
* [mattrobenolt/jinja2-cli](https://github.com/mattrobenolt/jinja2-cli) —
|
* [mattrobenolt/jinja2-cli](https://github.com/mattrobenolt/jinja2-cli) —
|
||||||
A command-line [Jinja2](http://jinja.pocoo.org/) renderer. Very comparable to
|
A command-line [Jinja2](http://jinja.pocoo.org/) renderer. Very comparable to
|
||||||
**twigc** in terms of features.
|
**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.
|
* This file is part of twigc.
|
||||||
*
|
*
|
||||||
* @author dana geier <dana@dana.is>
|
* @author dana <dana@dana.is>
|
||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
require_once __DIR__ . '/../src/bootstrap.php';
|
require_once __DIR__ . '/../src/bootstrap.php';
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||||
|
use Dana\Twigc\PharCompiler;
|
||||||
|
|
||||||
$verbose = false;
|
$verbose = false;
|
||||||
$verboseArgs = ['v', 'vv', 'vvv', 'verbose', 'debug'];
|
$verboseArgs = ['v', 'vv', 'vvv', 'verbose', 'debug'];
|
||||||
|
|
||||||
foreach ( $argv as $arg ) {
|
foreach ( $argv as $arg ) {
|
||||||
if ( in_array(ltrim($arg, '-'), $verboseArgs, true) ) {
|
if ( in_array(ltrim($arg, '-'), $verboseArgs, true) ) {
|
||||||
$verbose = true;
|
$verbose = true;
|
||||||
break;
|
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.
|
* This file is part of twigc.
|
||||||
*
|
*
|
||||||
* @author dana geier <dana@dana.is>
|
* @author dana <dana@dana.is>
|
||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
require_once __DIR__ . '/../src/bootstrap.php';
|
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));
|
||||||
|
|
51
complete/_twigc
Normal file
51
complete/_twigc
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
#compdef twigc
|
||||||
|
|
||||||
|
local ret=1
|
||||||
|
local -a context expl line state state_descr tmp
|
||||||
|
local -A opt_args
|
||||||
|
|
||||||
|
_arguments -s -S : \
|
||||||
|
'(: * -)'{-h,--help}'[display help information]' \
|
||||||
|
'(: * -)'{-V,--version}'[display version information]' \
|
||||||
|
'(: * -)--credits[display third-party dependency information]' \
|
||||||
|
'--cache=[specify cache directory]:cache directory:_directories' \
|
||||||
|
'*'{-d+,--dir=}'[add specified include directory]:include directory:_directories' \
|
||||||
|
'(-e --escape)'{-e+,--escape=}'[specify auto-escaping strategy]: :->strategies' \
|
||||||
|
'(-E --env)'{-E,--env}'[derive input data from environment]' \
|
||||||
|
'*'{-j+,--json=}'[derive input data from specified JSON dict/file]: :->json' \
|
||||||
|
'*'{-p+,--pair=}'[derive input data from specified key=value pair]:key=value pair' \
|
||||||
|
'*--query=[derive input data from specified URL query string]:URL query string' \
|
||||||
|
'(-s --strict)'{-s,--strict}'[throw exception when undefined variable is referenced]' \
|
||||||
|
'*:template file:_files' \
|
||||||
|
&& ret=0
|
||||||
|
|
||||||
|
case $state in
|
||||||
|
json)
|
||||||
|
if [[ $PREFIX$SUFFIX == [[:space:]]#\{* ]]; then
|
||||||
|
_message -e strings 'JSON dictionary string'
|
||||||
|
elif [[ -n $PREFIX$SUFFIX ]]; then
|
||||||
|
_description files expl 'JSON file'
|
||||||
|
_files "${(@)expl}" && ret=0
|
||||||
|
else
|
||||||
|
_alternative \
|
||||||
|
'strings: : _message -e strings "JSON dictionary string"' \
|
||||||
|
'files:JSON file:_files' \
|
||||||
|
&& ret=0
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
strategies)
|
||||||
|
tmp=(
|
||||||
|
'none:no escaping'
|
||||||
|
'css:CSS hex-escaping'
|
||||||
|
'html:HTML ampersand-escaping (for body)'
|
||||||
|
'html_attr:HTML ampersand-escaping (for attribute)'
|
||||||
|
'js:JavaScript hex-escaping'
|
||||||
|
'json:JSON serialization'
|
||||||
|
'sh:shell double-quoting and escaping'
|
||||||
|
'url:URL percent-escaping'
|
||||||
|
)
|
||||||
|
_describe -t strategies 'auto-escaping strategy' tmp && ret=0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
return ret
|
|
@ -1,25 +1,32 @@
|
||||||
{
|
{
|
||||||
"name": "dana/twigc",
|
"_comments": {
|
||||||
"description": "A CLI utility for rendering Twig templates",
|
"symfony/finder": [
|
||||||
"homepage": "https://github.com/okdana/twigc",
|
"This is only required for the Phar build, actually. Can't remember what",
|
||||||
"keywords": ["twig", "compile", "render", "template", "cli"],
|
"the problem was with making it require-dev, but some day i'll fix it..."
|
||||||
"license": "MIT",
|
]
|
||||||
|
},
|
||||||
|
|
||||||
"require": {
|
"name": "dana/twigc",
|
||||||
"php": ">=5.5.0",
|
"description": "CLI tool for rendering Twig templates",
|
||||||
"symfony/console": "^3.1",
|
"homepage": "https://github.com/okdana/twigc",
|
||||||
"twig/twig": "^1.24"
|
"keywords": ["twig", "compile", "render", "template", "cli"],
|
||||||
},
|
"license": "MIT",
|
||||||
|
|
||||||
"require-dev": {
|
"require": {
|
||||||
"phpunit/phpunit": "^5.4",
|
"php": ">=7.2.5",
|
||||||
"symfony/finder": "^3.1"
|
"symfony/console": "^5.0",
|
||||||
},
|
"symfony/finder": "^5.0",
|
||||||
|
"twig/twig": "^3.0",
|
||||||
|
"ulrichsg/getopt-php": "^3.3"
|
||||||
|
},
|
||||||
|
|
||||||
"autoload": {
|
"require-dev": {
|
||||||
"psr-4": {"Dana\\Twigc\\": "src/Twigc"}
|
"phpunit/phpunit": "^6.5"
|
||||||
},
|
},
|
||||||
|
|
||||||
"bin": ["bin/twigc"]
|
"autoload": {
|
||||||
|
"psr-4": {"Dana\\Twigc\\": "src/Twigc"}
|
||||||
|
},
|
||||||
|
|
||||||
|
"bin": ["bin/twigc"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
1314
composer.lock
generated
1314
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,496 @@
|
||||||
/**
|
/**
|
||||||
* This file is part of twigc.
|
* This file is part of twigc.
|
||||||
*
|
*
|
||||||
* @author dana geier <dana@dana.is>
|
* @author dana <dana@dana.is>
|
||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace Dana\Twigc;
|
namespace Dana\Twigc;
|
||||||
|
|
||||||
use Symfony\Component\Console\Application as BaseApplication;
|
use GetOpt\{Argument,GetOpt,Operand,Option};
|
||||||
use Symfony\Component\Console\Input\ArrayInput;
|
use Symfony\Component\Console\Helper\Table;
|
||||||
use Symfony\Component\Console\Input\InputArgument;
|
use Symfony\Component\Console\Output\{ConsoleOutputInterface,OutputInterface};
|
||||||
use Symfony\Component\Console\Input\InputDefinition;
|
use Twig\Environment;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Twig\Extension\EscaperExtension;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
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
|
* To be completely honest, this feels really shitty to me, and i don't like it.
|
||||||
* application work like a more traditional UNIX CLI tool.
|
* 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 {
|
class Application {
|
||||||
/**
|
const NAME = 'twigc';
|
||||||
* {@inheritdoc}
|
const VERSION = '0.4.0';
|
||||||
*/
|
const BUILD_DATE = '%BUILD_DATE%'; // Replaced during build
|
||||||
public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') {
|
|
||||||
parent::__construct('twigc', \Dana\Twigc\Twigc::VERSION_NUMBER);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
protected $name;
|
||||||
* {@inheritdoc}
|
protected $version;
|
||||||
*
|
|
||||||
* 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);
|
|
||||||
|
|
||||||
if ( ! $name ) {
|
/**
|
||||||
$name = $this->defaultCommand;
|
* Construct the object.
|
||||||
$input = new ArrayInput(['command' => $this->defaultCommand]);
|
*
|
||||||
}
|
* @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;
|
try {
|
||||||
$exitCode = $this->doRunCommand($command, $input, $output);
|
return $this->doRun($output, $argv);
|
||||||
$this->runningCommand = null;
|
} 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();
|
||||||
|
|
||||||
/**
|
$getopt->process(array_slice($argv, 1));
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
public function getDefinition() {
|
|
||||||
$definition = parent::getDefinition();
|
|
||||||
$definition->setArguments();
|
|
||||||
return $definition;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if ( $getopt->getOption('help') ) {
|
||||||
* {@inheritdoc}
|
$this->doHelp($output, $getopt);
|
||||||
*
|
return 0;
|
||||||
* Since we're a one-command application, we always use the name of the
|
}
|
||||||
* default command.
|
if ( $getopt->getOption('version') ) {
|
||||||
*/
|
$this->doVersion($output);
|
||||||
protected function getCommandName(InputInterface $input) {
|
return 0;
|
||||||
return $this->getDefaultCommands()[0]->getName();
|
}
|
||||||
}
|
if ( $getopt->getOption('credits') ) {
|
||||||
|
$this->doCredits($output);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
$inputData = [];
|
||||||
* {@inheritdoc}
|
$template = $getopt->getOperand('template');
|
||||||
*
|
$dirs = $getopt->getOption('dir');
|
||||||
* Since we're a one-command application, we always use the definition of
|
$temp = false;
|
||||||
* 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// If we're receiving data on standard input, and we didn't get a template,
|
||||||
* {@inheritdoc}
|
// assume `-` — we'll make sure this doesn't conflict with `-j` below
|
||||||
*
|
if ( ! posix_isatty(\STDIN) ) {
|
||||||
* Since we're a one-command application, we always return just the default
|
$template = $template ?? '-';
|
||||||
* command.
|
}
|
||||||
*/
|
|
||||||
protected function getDefaultCommands() {
|
|
||||||
return [new \Dana\Twigc\DefaultCommand()];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 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(EscaperExtension::class)->setEscaper(
|
||||||
|
'json',
|
||||||
|
function ($twigEnv, $string, $charset) {
|
||||||
|
return json_encode(
|
||||||
|
$string,
|
||||||
|
\JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
$twig->getExtension(EscaperExtension::class)->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()->setVerticalBorderChars('');
|
||||||
|
$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,415 +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(
|
|
||||||
'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;
|
|
||||||
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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$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.
|
* This file is part of twigc.
|
||||||
*
|
*
|
||||||
* @author dana geier <dana@dana.is>
|
* @author dana <dana@dana.is>
|
||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace Dana\Twigc;
|
namespace Dana\Twigc;
|
||||||
|
|
||||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Finder\Finder;
|
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:
|
* This clas is heavily inspired by Composer's Compiler:
|
||||||
*
|
*
|
||||||
|
@ -39,407 +40,321 @@ use Symfony\Component\Finder\Finder;
|
||||||
* THE SOFTWARE.
|
* THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
class PharCompiler {
|
class PharCompiler {
|
||||||
protected $output;
|
protected $output;
|
||||||
protected $baseDir;
|
protected $baseDir;
|
||||||
protected $finderSort;
|
protected $finderSort;
|
||||||
protected $versionNumber;
|
|
||||||
protected $versionCommit;
|
|
||||||
protected $versionDate;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Object constructor.
|
* Object constructor.
|
||||||
*
|
*
|
||||||
* @param (bool) $verbose (optional) Whether to display verbose output.
|
* @param OutputInterface $output The output to write to.
|
||||||
*
|
* @param bool $verbose (optional) Whether to display verbose output.
|
||||||
* @return self
|
*
|
||||||
*/
|
* @return self
|
||||||
public function __construct($verbose = false) {
|
*/
|
||||||
$this->output = new ConsoleOutput();
|
public function __construct(OutputInterface $output, bool $verbose = false) {
|
||||||
$this->baseDir = realpath(\Dana\Twigc\Twigc::BASE_DIR);
|
$this->output = $output;
|
||||||
$this->finderSort = function ($a, $b) {
|
$this->baseDir = realpath(__DIR__ . '/../..');
|
||||||
return strcmp(
|
$this->finderSort = function ($a, $b) {
|
||||||
strtr($a->getRealPath(), '\\', '/'),
|
return strcmp(
|
||||||
strtr($b->getRealPath(), '\\', '/')
|
strtr($a->getRealPath(), '\\', '/'),
|
||||||
);
|
strtr($b->getRealPath(), '\\', '/')
|
||||||
};
|
);
|
||||||
|
};
|
||||||
|
|
||||||
if ( $verbose ) {
|
if ( $verbose ) {
|
||||||
$this->output->setVerbosity(ConsoleOutput::VERBOSITY_VERBOSE);
|
$this->output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compiles the project into an executable phar file.
|
* Compiles the project into an executable phar file.
|
||||||
*
|
*
|
||||||
* @param string $pharFile
|
* @param string $phar
|
||||||
* (optional) The path (absolute or relative to the CWD) to write the
|
* (optional) The path (absolute or relative to the CWD) to write the
|
||||||
* resulting phar file to.
|
* resulting phar file to.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function compile($pharFile = 'twigc.phar') {
|
public function compile(string $phar) {
|
||||||
$this->output->writeln('Compiling phar...');
|
$this->output->writeln('Compiling phar...');
|
||||||
|
|
||||||
$this->output->writeln('', ConsoleOutput::VERBOSITY_VERBOSE);
|
$this->output->writeln('', OutputInterface::VERBOSITY_VERBOSE);
|
||||||
$this->extractVersionInformation();
|
|
||||||
|
|
||||||
if ( file_exists($pharFile) ) {
|
if ( file_exists($phar) ) {
|
||||||
unlink($pharFile);
|
unlink($phar);
|
||||||
}
|
}
|
||||||
|
|
||||||
$phar = new \Phar($pharFile, 0, 'twigc.phar');
|
$obj = new \Phar($phar, 0, basename($phar));
|
||||||
$phar->setSignatureAlgorithm(\Phar::SHA1);
|
$obj->setSignatureAlgorithm(\Phar::SHA1);
|
||||||
$phar->startBuffering();
|
$obj->startBuffering();
|
||||||
|
|
||||||
$this->output->writeln('', ConsoleOutput::VERBOSITY_VERBOSE);
|
$this->output->writeln('', OutputInterface::VERBOSITY_VERBOSE);
|
||||||
$this->output->writeln('Adding src files...');
|
$this->output->writeln('Adding src files...');
|
||||||
$this->addSrc($phar);
|
$this->addSrc($obj);
|
||||||
|
|
||||||
$this->output->writeln('', ConsoleOutput::VERBOSITY_VERBOSE);
|
$this->output->writeln('', OutputInterface::VERBOSITY_VERBOSE);
|
||||||
$this->output->writeln('Adding vendor files...');
|
$this->output->writeln('Adding vendor files...');
|
||||||
$this->addVendor($phar);
|
$this->addVendor($obj);
|
||||||
|
|
||||||
$this->output->writeln('', ConsoleOutput::VERBOSITY_VERBOSE);
|
$this->output->writeln('', OutputInterface::VERBOSITY_VERBOSE);
|
||||||
$this->output->writeln('Adding root files...');
|
$this->output->writeln('Adding root files...');
|
||||||
$this->addRoot($phar);
|
$this->addRoot($obj);
|
||||||
|
|
||||||
$this->output->writeln('', ConsoleOutput::VERBOSITY_VERBOSE);
|
$this->output->writeln('', OutputInterface::VERBOSITY_VERBOSE);
|
||||||
$this->output->writeln('Adding bin files...');
|
$this->output->writeln('Adding bin files...');
|
||||||
$this->addBin($phar);
|
$this->addBin($obj);
|
||||||
|
|
||||||
$phar->setStub($this->getStub());
|
$obj->setStub($this->getStub());
|
||||||
$phar->stopBuffering();
|
$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);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
// Strip the absolute base directory off the front of the path
|
||||||
// Re-sign the phar with reproducible time stamps and signature
|
$prefix = $this->baseDir . DIRECTORY_SEPARATOR;
|
||||||
$util = new Timestamps($pharFile);
|
$path = strtr(
|
||||||
$util->updateTimestamps($this->versionDate);
|
str_replace($prefix, '', $file->getRealPath()),
|
||||||
$util->save($pharFile, \Phar::SHA1);
|
'\\',
|
||||||
*/
|
'/'
|
||||||
}
|
);
|
||||||
|
|
||||||
/**
|
$this->output->writeln(
|
||||||
* Extracts the application version information from the git repository and
|
"Adding file: ${path}",
|
||||||
* sets the associated object properties.
|
OutputInterface::VERBOSITY_VERBOSE
|
||||||
*
|
);
|
||||||
* @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__);
|
|
||||||
|
|
||||||
// Get version number
|
$content = file_get_contents($file);
|
||||||
$output = [];
|
|
||||||
exec("cd ${workDir} && git describe --tags --match='v*.*.*' --dirty='!'", $output, $ret);
|
|
||||||
|
|
||||||
if ( $ret !== 0 || empty($output) ) {
|
// Strip interpreter directives
|
||||||
$output = ['0.0.0'];
|
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 ( $strip === null ) {
|
||||||
if ( count($tokens) > 1 ) {
|
$strip = in_array($file->getExtension(), ['json', 'lock', 'php'], true);
|
||||||
$this->versionNumber .= '-plus' . rtrim($tokens[1], '!');
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// If the index is dirty, add that
|
if ( $strip ) {
|
||||||
if ( rtrim(implode('-', $tokens), '!') !== implode('-', $tokens) ) {
|
$content = $this->stripWhiteSpace($content);
|
||||||
$this->versionNumber .= '-dirty';
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Get version last commit hash
|
$phar->addFromString($path, $content);
|
||||||
$output = [];
|
}
|
||||||
exec("cd ${workDir} && git log -1 --pretty='%H' HEAD", $output, $ret);
|
|
||||||
|
|
||||||
if ( $ret !== 0 || empty($output) ) {
|
/**
|
||||||
throw new \RuntimeException(
|
* Remove extraneous white space from a string whilst preserving PHP line
|
||||||
'An error occurred whilst running `git log`'
|
* 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
|
return $output === null ? $source : $output . "\n";
|
||||||
$output = [];
|
}
|
||||||
exec("cd ${workDir} && git log -1 --pretty='%ci' HEAD", $output, $ret);
|
|
||||||
|
|
||||||
if ( $ret !== 0 || empty($output) ) {
|
if ( ! function_exists('token_get_all') ) {
|
||||||
throw new \RuntimeException(
|
return $source;
|
||||||
'An error occurred whilst running `git log`'
|
}
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->versionDate = new \DateTime(trim($output[0]));
|
foreach ( token_get_all($source) as $token ) {
|
||||||
$this->versionDate->setTimezone(new \DateTimeZone('UTC'));
|
// 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() ) {
|
return $output;
|
||||||
$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()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a file to a phar.
|
* Add files from src directory to a phar.
|
||||||
*
|
*
|
||||||
* @param \Phar $phar
|
* @param \Phar $phar The phar file to add to.
|
||||||
* The phar file to add to.
|
*
|
||||||
*
|
* @return void
|
||||||
* @param \SplFileInfo|string $file
|
*/
|
||||||
* The file to add, or its path.
|
protected function addSrc(\Phar $phar) {
|
||||||
*
|
$finder = new Finder();
|
||||||
* @param null|bool $strip
|
$finder
|
||||||
* (optional) Whether to strip extraneous white space from the file in
|
->files()
|
||||||
* order to reduce its size. The default is to auto-detect based on file
|
->in($this->baseDir . '/src')
|
||||||
* extension.
|
->ignoreDotFiles(true)
|
||||||
*
|
->ignoreVCS(true)
|
||||||
* @return void
|
->name('*.php')
|
||||||
*/
|
->notName('PharCompiler.php')
|
||||||
protected function addFile($phar, $file, $strip = null) {
|
->sort($this->finderSort)
|
||||||
if ( is_string($file) ) {
|
;
|
||||||
$file = new \SplFileInfo($file);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strip the absolute base directory off the front of the path
|
foreach ( $finder as $file ) {
|
||||||
$prefix = $this->baseDir . DIRECTORY_SEPARATOR;
|
$this->addFile($phar, $file);
|
||||||
$path = strtr(
|
}
|
||||||
str_replace($prefix, '', $file->getRealPath()),
|
}
|
||||||
'\\',
|
|
||||||
'/'
|
|
||||||
);
|
|
||||||
|
|
||||||
$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
|
// Exclude files from dev packages
|
||||||
if ( strpos($path, 'bin/') === 0 ) {
|
foreach ( $devPaths as $path ) {
|
||||||
$content = preg_replace('%^#!/usr/bin/env php\s*%', '', $content);
|
$finder->notPath($path);
|
||||||
|
}
|
||||||
|
|
||||||
// Replace version place-holders
|
$finder
|
||||||
} elseif ( $path === 'src/Twigc/Twigc.php' ) {
|
->exclude('bin')
|
||||||
$content = str_replace(
|
->exclude('doc')
|
||||||
[
|
->exclude('docs')
|
||||||
'%version_number%',
|
->exclude('test')
|
||||||
'%version_commit%',
|
->exclude('tests')
|
||||||
'%version_date%',
|
->notPath('/^[^\/]+\/[^\/]+\/Tests?\//')
|
||||||
],
|
->notName('*.c')
|
||||||
[
|
->notName('*.h')
|
||||||
$this->versionNumber,
|
->notName('*.m4')
|
||||||
$this->versionCommit,
|
->notName('*.w32')
|
||||||
$this->versionDate->format('Y-m-d H:i:s'),
|
->notName('*.xml.dist')
|
||||||
],
|
->notName('build.xml')
|
||||||
$content
|
->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 ) {
|
foreach ( $finder as $file ) {
|
||||||
$strip = in_array($file->getExtension(), ['json', 'lock', 'php'], true);
|
$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
|
* Returns the phar stub.
|
||||||
* numbers.
|
*
|
||||||
*
|
* @return string
|
||||||
* @param string $source
|
*/
|
||||||
* The PHP or JSON string to strip white space from.
|
protected function getStub() {
|
||||||
*
|
$stub = "
|
||||||
* @param string $type
|
#!/usr/bin/env php
|
||||||
* (optional) The type of file the string represents. Available options
|
<?php
|
||||||
* are 'php' and 'json'. The default is 'php'.
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
protected function stripWhiteSpace($source, $type = 'php') {
|
|
||||||
$output = '';
|
|
||||||
|
|
||||||
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";
|
\Phar::mapPhar('twigc.phar');
|
||||||
}
|
require 'phar://twigc.phar/bin/twigc';
|
||||||
|
__HALT_COMPILER();
|
||||||
if ( ! function_exists('token_get_all') ) {
|
";
|
||||||
return $source;
|
return preg_replace('/^\s+/m', '', trim($stub)) . "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
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";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.
|
* This file is part of twigc.
|
||||||
*
|
*
|
||||||
* @author dana geier <dana@dana.is>
|
* @author dana <dana@dana.is>
|
||||||
* @license MIT
|
* @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
|
* @param string $string
|
||||||
* (optional) The message to print. Will be passed through rtrim(); if the
|
* (optional) The message to print. Is passed through rtrim(); if the result
|
||||||
* result is an empty string, only an empty line will be printed; otherwise,
|
* is an empty string, only an empty line is printed; otherwise, the text
|
||||||
* the text 'twigc: ' will be appended to the beginning.
|
* 'twigc: ' is appended to the beginning.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
function twigc_puts_error($string = '') {
|
function twigc_puts_error($string = '') {
|
||||||
$string = rtrim($string);
|
$string = rtrim($string);
|
||||||
$string = $string === '' ? '' : "twigc: ${string}";
|
$string = $string === '' ? '' : "twigc: ${string}";
|
||||||
|
|
||||||
// \STDERR only exists when we're using the CLI SAPI
|
if ( defined('\\STDERR') ) {
|
||||||
if ( defined('\\STDERR') ) {
|
fprintf(\STDERR, "%s\n", $string);
|
||||||
fprintf(\STDERR, "%s\n", $string);
|
} else {
|
||||||
} else {
|
echo $string, "\n";
|
||||||
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' ) {
|
if ( \PHP_SAPI !== 'cli' ) {
|
||||||
twigc_puts_error("This tool must be invoked via PHP's CLI SAPI.");
|
twigc_puts_error("This tool must be invoked via PHP's CLI SAPI.");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Give a meaningful error if we don't have the auto-loader
|
(function () {
|
||||||
if ( ! defined('TWIGC_AUTOLOADER') ) {
|
$paths = [
|
||||||
twigc_puts_error('Auto-loader is missing — try running `composer install`.');
|
// Phar/repo path
|
||||||
exit(1);
|
__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: \\u0022\\u003Cfoo\\u0024bar\\u003E\\u0022'],
|
||||||
|
|
||||||
|
// 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