mirror of
https://github.com/okdana/twigc.git
synced 2024-06-08 00:42:32 +02:00
Significantly rework application: 0.3.0
- Get rid of Symfony Console application/command/input components These were causing me serious problems — not least of all was the fact that the Console argument parser chokes on command lines lke `twigc -j - foo.twig`. I am not happy with the way i've structured this, but i just needed to get it *done*, so here we are. If someone has any advice on to how to make this nicer (maybe break it up into different classes, &c.), i would be appreciative - Add GetOpt.php for argument parsing and usage-help printing The usage help is really ugly, tbh. But whatever - Support multiple input data sources at once - Add `-E` short option for `--env` - Display an error when `-E` is used without the appropriate `variables_order` configuration in place - Add shell (`sh`) auto-escape method - Simplify/eliminate a lot of code (handling of version numbers, validation, &c.) - Change a lot of white space and formatting stuff (RIP `blame`)
This commit is contained in:
parent
37528fc72e
commit
9cbe7e75f9
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));
|
||||||
|
|
|
@ -3,97 +3,495 @@
|
||||||
/**
|
/**
|
||||||
* 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\Loader\{ArrayLoader,FilesystemLoader};
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
|
||||||
|
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.3.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('Twig_Extension_Core')->setEscaper(
|
||||||
|
'json',
|
||||||
|
function($twigEnv, $string, $charset) {
|
||||||
|
return json_encode(
|
||||||
|
$string,
|
||||||
|
\JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
$twig->getExtension('Twig_Extension_Core')->setEscaper(
|
||||||
|
'sh',
|
||||||
|
function($twigEnv, $string, $charset) {
|
||||||
|
return '"' . addcslashes($string, '$`\\"') . '"';
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
$output->writeln(
|
||||||
|
rtrim($twig->render(basename($template), $inputData), "\r\n")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
} finally {
|
||||||
|
if ( $temp ) {
|
||||||
|
unlink($template);
|
||||||
|
rmdir(dirname($template));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the application's usage help.
|
||||||
|
*
|
||||||
|
* @param OutputInterface $output
|
||||||
|
* The output to write to.
|
||||||
|
*
|
||||||
|
* @param GetOpt $getopt
|
||||||
|
* The GetOpt instance from which to derive the usage help.
|
||||||
|
*
|
||||||
|
* @param string|null $message
|
||||||
|
* (optional) An additional message to print above the usage help. This is
|
||||||
|
* intended primarily for error messages.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function doHelp(
|
||||||
|
OutputInterface $output,
|
||||||
|
GetOpt $getopt,
|
||||||
|
string $message = null
|
||||||
|
): int {
|
||||||
|
if ( $message !== null && $message !== '' ) {
|
||||||
|
$output->writeln("{$this->name}: " . rtrim($message, "\r\n") . "\n");
|
||||||
|
}
|
||||||
|
$output->writeln(rtrim($getopt->getHelpText(), "\r\n"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the application's version information.
|
||||||
|
*
|
||||||
|
* @param OutputInterface $output The output to write to.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function doVersion(OutputInterface $output): int {
|
||||||
|
$version = sprintf('%s version %s', $this->name, $this->version);
|
||||||
|
|
||||||
|
if ( strpos(static::BUILD_DATE, '%') === false ) {
|
||||||
|
$version .= sprintf(' (built %s)', static::BUILD_DATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
$output->writeln($version);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the application's dependency information.
|
||||||
|
*
|
||||||
|
* @param OutputInterface $output The output to write to.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function doCredits(OutputInterface $output): int {
|
||||||
|
$packages = (new ComposerHelper())->getPackages();
|
||||||
|
|
||||||
|
$table = new Table($output);
|
||||||
|
$table->setStyle('compact');
|
||||||
|
$table->getStyle()->setVerticalBorderChar('');
|
||||||
|
$table->getStyle()->setCellRowContentFormat('%s ');
|
||||||
|
$table->setHeaders(['name', 'version', 'licence']);
|
||||||
|
|
||||||
|
foreach ( $packages as $package ) {
|
||||||
|
$table->addRow([
|
||||||
|
$package->name,
|
||||||
|
ltrim($package->version, 'v'),
|
||||||
|
implode(', ', $package->license) ?: '?',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
$table->render();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an instance of GetOpt configured for this application.
|
||||||
|
*
|
||||||
|
* @return GetOpt
|
||||||
|
*/
|
||||||
|
public function getGetOpt(): GetOpt {
|
||||||
|
$getopt = new GetOpt(null, [
|
||||||
|
GetOpt::SETTING_SCRIPT_NAME => $this->name,
|
||||||
|
GetOpt::SETTING_STRICT_OPERANDS => true,
|
||||||
|
]);
|
||||||
|
$getopt->addOptions([
|
||||||
|
Option::create('h', 'help', GetOpt::NO_ARGUMENT)
|
||||||
|
->setDescription('Display this usage help and exit')
|
||||||
|
,
|
||||||
|
Option::create('V', 'version', GetOpt::NO_ARGUMENT)
|
||||||
|
->setDescription('Display version information and exit')
|
||||||
|
,
|
||||||
|
Option::create(null, 'credits', GetOpt::NO_ARGUMENT)
|
||||||
|
->setDescription('Display dependency information and exit')
|
||||||
|
,
|
||||||
|
Option::create(null, 'cache', GetOpt::REQUIRED_ARGUMENT)
|
||||||
|
->setDescription('Enable caching to specified directory')
|
||||||
|
->setArgumentName('dir')
|
||||||
|
->setValidation('is_dir')
|
||||||
|
,
|
||||||
|
Option::create('d', 'dir', GetOpt::MULTIPLE_ARGUMENT)
|
||||||
|
->setDescription('Add specified search directory to loader')
|
||||||
|
->setArgumentName('dir')
|
||||||
|
->setValidation('is_dir')
|
||||||
|
,
|
||||||
|
Option::create('e', 'escape', GetOpt::REQUIRED_ARGUMENT)
|
||||||
|
->setArgumentName('strategy')
|
||||||
|
->setDescription('Specify default auto-escaping strategy')
|
||||||
|
,
|
||||||
|
Option::create('E', 'env', GetOpt::NO_ARGUMENT)
|
||||||
|
->setDescription('Derive input data from environment')
|
||||||
|
,
|
||||||
|
Option::create('j', 'json', GetOpt::MULTIPLE_ARGUMENT)
|
||||||
|
->setArgumentName('dict/file')
|
||||||
|
->setDescription('Derive input data from specified JSON file or dictionary string')
|
||||||
|
,
|
||||||
|
Option::create('p', 'pair', GetOpt::MULTIPLE_ARGUMENT)
|
||||||
|
->setArgumentName('input')
|
||||||
|
->setDescription('Derive input data from specified key=value pair')
|
||||||
|
,
|
||||||
|
Option::create(null, 'query', GetOpt::MULTIPLE_ARGUMENT)
|
||||||
|
->setArgumentName('input')
|
||||||
|
->setDescription('Derive input data from specified URL query string')
|
||||||
|
,
|
||||||
|
Option::create('s', 'strict', GetOpt::NO_ARGUMENT)
|
||||||
|
->setDescription('Throw exception when undefined variable is referenced')
|
||||||
|
,
|
||||||
|
]);
|
||||||
|
$getopt->addOperands([
|
||||||
|
Operand::create('template', Operand::OPTIONAL),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $getopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the correct Twig escape method given the provided options.
|
||||||
|
*
|
||||||
|
* @param string|null $escape
|
||||||
|
* The user-provided escape option, or null if not provided.
|
||||||
|
*
|
||||||
|
* @param string|null $template
|
||||||
|
* The name/path of the template file, or null if not provided. This is only
|
||||||
|
* used when $escape is null or 'auto'.
|
||||||
|
*
|
||||||
|
* @return string|false
|
||||||
|
*/
|
||||||
|
public function getEscaper($escape, string $template = null) {
|
||||||
|
$escape = $escape === null ? $escape : strtolower($escape);
|
||||||
|
$template = $template ?? '';
|
||||||
|
|
||||||
|
if ( $escape === null || $escape === 'auto' ) {
|
||||||
|
if (
|
||||||
|
substr($template, -5) === '.twig'
|
||||||
|
&&
|
||||||
|
strpos(substr($template, 0, -5), '.')
|
||||||
|
) {
|
||||||
|
$ext = pathinfo(substr($template, 0, -5), \PATHINFO_EXTENSION);
|
||||||
|
} else {
|
||||||
|
$ext = pathinfo($template, \PATHINFO_EXTENSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ( strtolower($ext) ) {
|
||||||
|
case 'htm':
|
||||||
|
case 'html':
|
||||||
|
case 'phtml':
|
||||||
|
case 'thtml':
|
||||||
|
case 'xhtml':
|
||||||
|
case 'template':
|
||||||
|
case 'tmpl':
|
||||||
|
case 'tpl':
|
||||||
|
return 'html';
|
||||||
|
case 'css':
|
||||||
|
case 'scss':
|
||||||
|
return 'css';
|
||||||
|
case 'js':
|
||||||
|
return 'js';
|
||||||
|
case 'json':
|
||||||
|
return 'json';
|
||||||
|
case 'bash':
|
||||||
|
case 'ksh':
|
||||||
|
case 'sh':
|
||||||
|
case 'zsh':
|
||||||
|
return 'sh';
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, try to parse the supplied method
|
||||||
|
switch ( $escape ) {
|
||||||
|
case 'f':
|
||||||
|
case 'n':
|
||||||
|
case 'none':
|
||||||
|
case 'never':
|
||||||
|
$escape = 'false';
|
||||||
|
break;
|
||||||
|
case 't':
|
||||||
|
case 'y':
|
||||||
|
case 'always':
|
||||||
|
$escape = 'true';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bool = filter_var(
|
||||||
|
$escape,
|
||||||
|
\FILTER_VALIDATE_BOOLEAN,
|
||||||
|
\FILTER_NULL_ON_FAILURE
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( $bool !== null ) {
|
||||||
|
$escape = $bool ? 'html' : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $escape;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
88
src/Twigc/ComposerHelper.php
Normal file
88
src/Twigc/ComposerHelper.php
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of twigc.
|
||||||
|
*
|
||||||
|
* @author dana <dana@dana.is>
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Dana\Twigc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class with various functions for interacting with Composer's lock
|
||||||
|
* file.
|
||||||
|
*/
|
||||||
|
class ComposerHelper {
|
||||||
|
/**
|
||||||
|
* Get an array of the installed non-dev packages listed in a Composer lock
|
||||||
|
* file.
|
||||||
|
*
|
||||||
|
* @param string|null (optional) The path to the lock file to parse.
|
||||||
|
*
|
||||||
|
* @return object[]
|
||||||
|
*/
|
||||||
|
public function getPackages(string $lockFile = null) {
|
||||||
|
$packages = $this->parseLockFile($lockFile)['packages'];
|
||||||
|
return $this->massagePackages($packages);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an array of the installed dev packages listed in a Composer lock file.
|
||||||
|
*
|
||||||
|
* @param string|null (optional) The path to the lock file to parse.
|
||||||
|
*
|
||||||
|
* @return object[]
|
||||||
|
*/
|
||||||
|
public function getDevPackages(string $lockFile = null) {
|
||||||
|
$packages = $this->parseLockFile($lockFile)['packages-dev'];
|
||||||
|
return $this->massagePackages($packages);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an array of data representing a Composer lock file.
|
||||||
|
*
|
||||||
|
* @param string $path
|
||||||
|
* (optional) The path to the lock file to parse. The default is the lock
|
||||||
|
* file associated with the current project.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*
|
||||||
|
* @throws \RuntimeException if composer.lock doesn't exist
|
||||||
|
* @throws \RuntimeException if composer.lock can't be decoded
|
||||||
|
*/
|
||||||
|
public function parseLockFile(string $lockFile = null): array {
|
||||||
|
$lockFile = $lockFile ?? __DIR__ . '/../../composer.lock';
|
||||||
|
|
||||||
|
if ( ! file_exists($lockFile) ) {
|
||||||
|
throw new \RuntimeException('Missing ' . basename($lockFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
$lock = json_decode(file_get_contents($lockFile), true);
|
||||||
|
|
||||||
|
if ( empty($lock) || ! isset($lock['packages']) ) {
|
||||||
|
throw new \RuntimeException('Error decoding ' . basename($lockFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $lock;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort and object-ify an array of package data.
|
||||||
|
*
|
||||||
|
* @param array $packages Package data from composer.lock.
|
||||||
|
*
|
||||||
|
* @return object[]
|
||||||
|
*/
|
||||||
|
public function massagePackages(array $packages): array {
|
||||||
|
usort($packages, function ($a, $b) {
|
||||||
|
return strcasecmp($a['name'], $b['name']);
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach ( $packages as &$package ) {
|
||||||
|
$package = (object) $package;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $packages;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,438 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This file is part of twigc.
|
|
||||||
*
|
|
||||||
* @author dana geier <dana@dana.is>
|
|
||||||
* @license MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Dana\Twigc;
|
|
||||||
|
|
||||||
use Symfony\Component\Console\Command\Command;
|
|
||||||
use Symfony\Component\Console\Helper\DescriptorHelper;
|
|
||||||
use Symfony\Component\Console\Helper\Table;
|
|
||||||
use Symfony\Component\Console\Input\InputArgument;
|
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
|
||||||
use Symfony\Component\Console\Input\InputOption;
|
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default twigc command.
|
|
||||||
*/
|
|
||||||
class DefaultCommand extends Command {
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
protected function configure() {
|
|
||||||
$this
|
|
||||||
->setName('twigc')
|
|
||||||
->setDescription('Compile a Twig template')
|
|
||||||
->addArgument(
|
|
||||||
'template',
|
|
||||||
InputArgument::OPTIONAL,
|
|
||||||
'Twig template file to render (use `-` for STDIN)'
|
|
||||||
)
|
|
||||||
->addOption(
|
|
||||||
'help',
|
|
||||||
'h',
|
|
||||||
InputOption::VALUE_NONE,
|
|
||||||
'Display this usage help'
|
|
||||||
)
|
|
||||||
->addOption(
|
|
||||||
'version',
|
|
||||||
'V',
|
|
||||||
InputOption::VALUE_NONE,
|
|
||||||
'Display version information'
|
|
||||||
)
|
|
||||||
->addOption(
|
|
||||||
'credits',
|
|
||||||
null,
|
|
||||||
InputOption::VALUE_NONE,
|
|
||||||
'Display dependency credits (including Twig version)'
|
|
||||||
)
|
|
||||||
->addOption(
|
|
||||||
'cache',
|
|
||||||
null,
|
|
||||||
InputOption::VALUE_REQUIRED,
|
|
||||||
'Enable caching to specified directory'
|
|
||||||
)
|
|
||||||
->addOption(
|
|
||||||
'dir',
|
|
||||||
'd',
|
|
||||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
|
||||||
'Add search directory to loader'
|
|
||||||
)
|
|
||||||
->addOption(
|
|
||||||
'env',
|
|
||||||
null,
|
|
||||||
InputOption::VALUE_NONE,
|
|
||||||
'Treat environment variables as input data'
|
|
||||||
)
|
|
||||||
->addOption(
|
|
||||||
'escape',
|
|
||||||
'e',
|
|
||||||
InputOption::VALUE_REQUIRED,
|
|
||||||
'Set autoescape environment option'
|
|
||||||
)
|
|
||||||
->addOption(
|
|
||||||
'json',
|
|
||||||
'j',
|
|
||||||
InputOption::VALUE_REQUIRED,
|
|
||||||
'Pass variables as JSON (dictionary string or file path)'
|
|
||||||
)
|
|
||||||
->addOption(
|
|
||||||
'pair',
|
|
||||||
'p',
|
|
||||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
|
||||||
'Pass variable as key=value pair'
|
|
||||||
)
|
|
||||||
->addOption(
|
|
||||||
'query',
|
|
||||||
null,
|
|
||||||
InputOption::VALUE_REQUIRED,
|
|
||||||
'Pass variables as URL query string'
|
|
||||||
)
|
|
||||||
->addOption(
|
|
||||||
'strict',
|
|
||||||
's',
|
|
||||||
InputOption::VALUE_NONE,
|
|
||||||
'Enable strict_variables environment option'
|
|
||||||
)
|
|
||||||
;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output) {
|
|
||||||
switch ( true ) {
|
|
||||||
// Display usage help
|
|
||||||
case $input->getOption('help'):
|
|
||||||
return $this->doHelp($input, $output);
|
|
||||||
// Display version information
|
|
||||||
case $input->getOption('version'):
|
|
||||||
return $this->doVersion($input, $output);
|
|
||||||
// Display package credits
|
|
||||||
case $input->getOption('credits'):
|
|
||||||
return $this->doCredits($input, $output);
|
|
||||||
}
|
|
||||||
// Render Twig template
|
|
||||||
return $this->doRender($input, $output);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*
|
|
||||||
* Overriding this prevents TextDescriptor from displaying the Help section.
|
|
||||||
*/
|
|
||||||
public function getProcessedHelp() {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays usage help.
|
|
||||||
*
|
|
||||||
* @param InputInterface $input
|
|
||||||
* @param OutputInterface $output
|
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function doHelp(InputInterface $input, OutputInterface $output) {
|
|
||||||
(new DescriptorHelper())->describe($output, $this);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays version information.
|
|
||||||
*
|
|
||||||
* @param InputInterface $input
|
|
||||||
* @param OutputInterface $output
|
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function doVersion(InputInterface $input, OutputInterface $output) {
|
|
||||||
$nameFmt = '<info>twigc</info>';
|
|
||||||
$versionFmt = '<comment>%s</comment> (<comment>%s</comment> @ <comment>%s</comment>)';
|
|
||||||
|
|
||||||
$output->writeln(sprintf(
|
|
||||||
"${nameFmt} version ${versionFmt}",
|
|
||||||
\Dana\Twigc\Twigc::VERSION_NUMBER,
|
|
||||||
\Dana\Twigc\Twigc::VERSION_COMMIT,
|
|
||||||
\Dana\Twigc\Twigc::VERSION_DATE
|
|
||||||
));
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays package credits.
|
|
||||||
*
|
|
||||||
* @param InputInterface $input
|
|
||||||
* @param OutputInterface $output
|
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function doCredits(InputInterface $input, OutputInterface $output) {
|
|
||||||
$installed = \Dana\Twigc\Twigc::getComposerPackages();
|
|
||||||
|
|
||||||
$table = new Table($output);
|
|
||||||
$table->setStyle('compact');
|
|
||||||
$table->getStyle()->setVerticalBorderChar('');
|
|
||||||
$table->getStyle()->setCellRowContentFormat('%s ');
|
|
||||||
$table->setHeaders(['name', 'version', 'licence']);
|
|
||||||
|
|
||||||
foreach ( $installed as $package ) {
|
|
||||||
$table->addRow([
|
|
||||||
$package->name,
|
|
||||||
ltrim($package->version, 'v'),
|
|
||||||
implode(', ', $package->license) ?: '?',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
$table->render();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders a Twig template.
|
|
||||||
*
|
|
||||||
* @param InputInterface $input
|
|
||||||
* @param OutputInterface $output
|
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function doRender(InputInterface $input, OutputInterface $output) {
|
|
||||||
$inputData = [];
|
|
||||||
$template = $input->getArgument('template');
|
|
||||||
$template = $template === null ? '-' : $template;
|
|
||||||
$cache = $input->getOption('cache');
|
|
||||||
$cache = $cache === null ? false : $cache;
|
|
||||||
$dirs = $template === '-' ? [] : [dirname($template)];
|
|
||||||
$dirs = array_merge($dirs, $input->getOption('dir'));
|
|
||||||
$temp = false;
|
|
||||||
$strict = (bool) $input->getOption('strict');
|
|
||||||
$escape = $input->getOption('escape');
|
|
||||||
$inputs = [
|
|
||||||
'json' => (int) ($input->getOption('json') !== null),
|
|
||||||
'pair' => (int) (! empty($input->getOption('pair'))),
|
|
||||||
'query' => (int) ($input->getOption('query') !== null),
|
|
||||||
];
|
|
||||||
|
|
||||||
// If we're reading from STDIN, but STDIN is a TTY, print help and die
|
|
||||||
if ( $template === '-' && posix_isatty(\STDIN) ) {
|
|
||||||
$this->doHelp($input, $output);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate search directories
|
|
||||||
foreach ( $dirs as $dir ) {
|
|
||||||
if ( ! is_dir($dir) ) {
|
|
||||||
throw new \InvalidArgumentException(
|
|
||||||
"Illegal search directory: ${dir}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no escape option was supplied, try to auto-detect
|
|
||||||
// (we could do this with Twig's 'filename' method, but i have some
|
|
||||||
// control over this)
|
|
||||||
if ( $escape === null ) {
|
|
||||||
if ( substr($template, -5) === '.twig' ) {
|
|
||||||
$ext = pathinfo(substr($template, -5), \PATHINFO_EXTENSION);
|
|
||||||
} else {
|
|
||||||
$ext = pathinfo($template, \PATHINFO_EXTENSION);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ( strtolower($ext) ) {
|
|
||||||
case 'template':
|
|
||||||
case 'tmpl':
|
|
||||||
case 'tpl':
|
|
||||||
case 'htm':
|
|
||||||
case 'html':
|
|
||||||
case 'phtml':
|
|
||||||
case 'thtml':
|
|
||||||
case 'xhtml':
|
|
||||||
$escape = 'html';
|
|
||||||
break;
|
|
||||||
case 'css':
|
|
||||||
case 'scss':
|
|
||||||
$escape = 'css';
|
|
||||||
break;
|
|
||||||
case 'js':
|
|
||||||
$escape = 'js';
|
|
||||||
break;
|
|
||||||
case 'json':
|
|
||||||
$escape = 'json';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$escape = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, try to parse the supplied method
|
|
||||||
} else {
|
|
||||||
// Normalise some boolean values
|
|
||||||
$escape = strtolower($escape);
|
|
||||||
$escape = $escape === 'f' ? 'false' : $escape;
|
|
||||||
$escape = $escape === 'n' ? 'false' : $escape;
|
|
||||||
$escape = $escape === 'none' ? 'false' : $escape;
|
|
||||||
$escape = $escape === 'never' ? 'false' : $escape;
|
|
||||||
$escape = $escape === 't' ? 'true' : $escape;
|
|
||||||
$escape = $escape === 'y' ? 'true' : $escape;
|
|
||||||
$escape = $escape === 'always' ? 'true' : $escape;
|
|
||||||
|
|
||||||
$bool = filter_var($escape, \FILTER_VALIDATE_BOOLEAN, \FILTER_NULL_ON_FAILURE);
|
|
||||||
|
|
||||||
if ( $bool !== null ) {
|
|
||||||
$escape = $bool ? 'html' : false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Because Console doesn't allow us to see the order of options supplied
|
|
||||||
// at the command line, there's no good way to sort out the precedence
|
|
||||||
// amongst the different input methods... so let's just say we can only
|
|
||||||
// use one of them at a time
|
|
||||||
if ( array_sum($inputs) > 1 ) {
|
|
||||||
throw new \InvalidArgumentException(
|
|
||||||
'-j, -p, and --query options are mutually exclusive'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Input data supplied via query string
|
|
||||||
if ( ($query = $input->getOption('query')) !== null ) {
|
|
||||||
if ( $query && $query[0] === '?' ) {
|
|
||||||
$query = substr($query, 1);
|
|
||||||
}
|
|
||||||
parse_str($query, $inputData);
|
|
||||||
|
|
||||||
// Input data supplied via JSON
|
|
||||||
} elseif ( ($json = $input->getOption('json')) !== null ) {
|
|
||||||
$json = trim($json);
|
|
||||||
|
|
||||||
// JSON supplied via STDIN
|
|
||||||
if ( $json === '-' ) {
|
|
||||||
if ( $template === '-' ) {
|
|
||||||
throw new \InvalidArgumentException(
|
|
||||||
'Can not read both template and JSON input from STDIN'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if ( posix_isatty(\STDIN) ) {
|
|
||||||
throw new \InvalidArgumentException(
|
|
||||||
'Expected JSON input on STDIN'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
$json = file_get_contents('php://stdin');
|
|
||||||
|
|
||||||
// JSON supplied via file
|
|
||||||
} elseif ( $json && $json[0] !== '{' ) {
|
|
||||||
if ( ! file_exists($json) || is_dir($json) ) {
|
|
||||||
throw new \InvalidArgumentException(
|
|
||||||
"Missing or illegal JSON file name: ${json}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
$json = file_get_contents($json);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This check is here to prevent errors if the input is just empty
|
|
||||||
if ( trim($json) !== '' ) {
|
|
||||||
$inputData = json_decode($json, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( ! is_array($inputData) ) {
|
|
||||||
throw new \InvalidArgumentException(
|
|
||||||
'JSON input must be a dictionary'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Input data supplied via key=value pair
|
|
||||||
} elseif ( count($input->getOption('pair')) ) {
|
|
||||||
foreach ( $input->getOption('pair') as $pair ) {
|
|
||||||
$kv = explode('=', $pair, 2);
|
|
||||||
|
|
||||||
if ( count($kv) !== 2 ) {
|
|
||||||
throw new \InvalidArgumentException(
|
|
||||||
"Illegal key=value pair: ${pair}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$inputData[$kv[0]] = $kv[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( $input->getOption('env') ) {
|
|
||||||
$inputData = array_merge($_ENV, $inputData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate key names now
|
|
||||||
foreach ( $inputData as $key => $value ) {
|
|
||||||
if ( ! preg_match('#^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$#', $key) ) {
|
|
||||||
throw new \InvalidArgumentException(
|
|
||||||
"Illegal variable name: ${key}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Template supplied via STDIN
|
|
||||||
if ( $template === '-' ) {
|
|
||||||
// If we've been supplied one or more search directories, we'll need
|
|
||||||
// to write the template out to a temp directory so we can use the
|
|
||||||
// file-system loader
|
|
||||||
if ( $dirs ) {
|
|
||||||
$temp = true;
|
|
||||||
$template = implode('/', [
|
|
||||||
sys_get_temp_dir(),
|
|
||||||
implode('.', ['twigc', getmypid(), md5(time())]),
|
|
||||||
'-',
|
|
||||||
]);
|
|
||||||
|
|
||||||
mkdir(dirname($template));
|
|
||||||
file_put_contents($template, file_get_contents('php://stdin'), LOCK_EX);
|
|
||||||
|
|
||||||
$dirs = array_merge([dirname($template)], $dirs);
|
|
||||||
|
|
||||||
$loader = new \Twig_Loader_Filesystem($dirs);
|
|
||||||
|
|
||||||
// Otherwise, we can just use the array loader, which is a little
|
|
||||||
// faster and cleaner
|
|
||||||
} else {
|
|
||||||
$loader = new \Twig_Loader_Array([
|
|
||||||
$template => file_get_contents('php://stdin'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Template supplied via file path
|
|
||||||
} else {
|
|
||||||
$loader = new \Twig_Loader_Filesystem($dirs);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$twig = new \Twig_Environment($loader, [
|
|
||||||
'cache' => $cache,
|
|
||||||
'debug' => false,
|
|
||||||
'strict_variables' => $strict,
|
|
||||||
'autoescape' => $escape,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$twig->getExtension('core')->setEscaper(
|
|
||||||
'json',
|
|
||||||
function($twigEnv, $string, $charset) {
|
|
||||||
return json_encode(
|
|
||||||
$string,
|
|
||||||
\JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
$output->writeln(
|
|
||||||
rtrim($twig->render(basename($template), $inputData), "\r\n")
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
if ( $temp ) {
|
|
||||||
unlink($template);
|
|
||||||
rmdir(dirname($template));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,18 +3,19 @@
|
||||||
/**
|
/**
|
||||||
* This file is part of twigc.
|
* 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;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue