Skip to content

Commit 46cd78d

Browse files
committed
Initial release
0 parents  commit 46cd78d

File tree

8 files changed

+286
-0
lines changed

8 files changed

+286
-0
lines changed

ChangeLog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Template literals for PHP - ChangeLog
2+
=====================================
3+
4+
## ?.?.? / ????-??-??
5+
6+
* Hello World! First release - @thekid

README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
Template literals for PHP
2+
=========================
3+
4+
[![Build status on GitHub](https://github.com/xp-lang/php-template-literals/workflows/Tests/badge.svg)](https://github.com/xp-lang/php-template-literals/actions)
5+
[![XP Framework Module](https://raw.githubusercontent.com/xp-framework/web/master/static/xp-framework-badge.png)](https://github.com/xp-framework/core)
6+
[![BSD Licence](https://raw.githubusercontent.com/xp-framework/web/master/static/licence-bsd.png)](https://github.com/xp-framework/core/blob/master/LICENCE.md)
7+
[![Requires PHP 7.4+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-7_4plus.svg)](http://php.net/)
8+
[![Supports PHP 8.0+](https://raw.githubusercontent.com/xp-framework/web/master/static/php-8_0plus.svg)](http://php.net/)
9+
[![Latest Stable Version](https://poser.pugx.org/xp-lang/php-template-literals/version.svg)](https://packagpatternt.org/packages/xp-lang/php-template-literals)
10+
11+
Plugin for the [XP Compiler](https://github.com/xp-framework/compiler/) which reimagines [JS template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) for PHP.
12+
13+
Example
14+
-------
15+
The following outputs `https://thekid.de/?a&b` - the ampersand is encoded correctly:
16+
17+
```php
18+
$html= function($strings, ... $arguments) {
19+
$r= '';
20+
foreach ($strings as $i => $string) {
21+
$r.= $string.htmlspecialchars($arguments[$i] ?? '');
22+
}
23+
return $r;
24+
};
25+
26+
$link= 'https://thekid.de/?a&b';
27+
echo $html`This is a <a href="${$link}">link</a>.`;
28+
```
29+
30+
The compiler transforms the syntax with expressions embedded in `${}` into the following:
31+
32+
```php
33+
echo $html(['This is a ', '.'], $link);
34+
```
35+
36+
Installation
37+
------------
38+
After installing the XP Compiler into your project, also include this plugin.
39+
40+
```bash
41+
$ composer require xp-framework/compiler
42+
# ...
43+
44+
$ composer require xp-lang/php-template-literals
45+
# ...
46+
```
47+
48+
No further action is required.

class.pth

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
src/main/php/
2+
src/test/php/

composer.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name" : "xp-lang/php-template-literals",
3+
"type" : "library",
4+
"homepage" : "http://xp-framework.net/",
5+
"license" : "BSD-3-Clause",
6+
"description" : "Template literals for PHP",
7+
"keywords": ["language", "module", "xp"],
8+
"require" : {
9+
"xp-framework/core": "^12.0 | ^11.0 | ^10.0",
10+
"xp-framework/compiler": "^9.0 | ^8.0 | ^7.0",
11+
"php" : ">=7.4.0"
12+
},
13+
"require-dev" : {
14+
"xp-framework/test": "^2.0 | ^1.0"
15+
},
16+
"autoload" : {
17+
"files" : ["src/main/php/autoload.php"]
18+
}
19+
}

src/main/php/autoload.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<?php namespace xp;
2+
3+
\lang\ClassLoader::registerPath(__DIR__);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php namespace lang\ast\syntax\php;
2+
3+
use lang\ast\Node;
4+
5+
class TemplateLiteral extends Node {
6+
public $kind= 'template';
7+
public $resolver, $strings, $arguments;
8+
9+
public function __construct($resolver, $strings, $arguments, $line= -1) {
10+
$this->resolver= $resolver;
11+
$this->strings= $strings;
12+
$this->arguments= $arguments;
13+
$this->line= $line;
14+
}
15+
16+
/** @return iterable */
17+
public function children() { return []; }
18+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php namespace lang\ast\syntax\php;
2+
3+
use lang\ast\nodes\{ArrayLiteral, InvokeExpression, Literal};
4+
use lang\ast\syntax\Extension;
5+
use lang\ast\{Error, Tokens};
6+
7+
class TemplateStrings implements Extension {
8+
9+
public function setup($language, $emitter) {
10+
$language->suffix('(literal)', 100, function($parse, $token, $left) {
11+
static $escape= ['"' => '\\"', '\\`' => '`', '$' => '\\$'];
12+
13+
if ('string' !== $token->kind || '`' !== $token->value[0]) {
14+
throw new Error("Unexpected {$token->kind} {$token->value}", $parse->file, $token->line);
15+
}
16+
17+
$strings= [];
18+
$arguments= [];
19+
for ($o= 1, $l= strlen($token->value) - 1; $o < $l; ) {
20+
$p= strpos($token->value, '${', $o);
21+
22+
// Everything before the placeholder is a string literal
23+
$strings[]= new Literal('"'.($p === $o ? '' : strtr(substr($token->value, $o, ($p ?: $l) - $o), $escape)).'"');
24+
if (false === $p) break;
25+
26+
// Everything following is the placeholder, which may contain `{}`
27+
for ($o= $p+= 2, $b= 1; $b && $o < $l; $o++) {
28+
$o+= strcspn($token->value, '{}', $o);
29+
if ('{' === $token->value[$o]) {
30+
$b++;
31+
} else if ('}' === $token->value[$o]) {
32+
$b--;
33+
}
34+
}
35+
36+
$expr= $this->parse(new Tokens(strtr(substr($token->value, $p, $o - $p - 1), ['\\`' => '`'])), $parse->scope);
37+
$expr->forward();
38+
$arguments[]= $this->expression($expr, 0);
39+
}
40+
41+
return new TemplateLiteral($left, $strings, $arguments, $token->line);
42+
});
43+
44+
$emitter->transform('template', function($codegen, $node) {
45+
$strings= new ArrayLiteral([]);
46+
foreach ($node->strings as $string) {
47+
$strings->values[]= [null, $string];
48+
}
49+
return new InvokeExpression($node->resolver, [$strings, ...$node->arguments]);
50+
});
51+
}
52+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<?php namespace lang\ast\syntax\php\unittest;
2+
3+
use lang\ast\Errors;
4+
use lang\ast\unittest\emit\EmittingTest;
5+
use test\{Assert, Before, Expect, Test};
6+
7+
class TemplateLiteralsTest extends EmittingTest {
8+
private $format;
9+
10+
/** Evaluates a strign template */
11+
private function evaluate(string $template, array $arguments= []) {
12+
return $this->type('class %T { public function run($f, $arguments) { return '.$template.'; } }')
13+
->newInstance()
14+
->run($this->format, $arguments)
15+
;
16+
}
17+
18+
#[Before]
19+
public function format() {
20+
$this->format= function($strings, ... $arguments) {
21+
$r= '';
22+
foreach ($strings as $i => $string) {
23+
$r.= $string.htmlspecialchars($arguments[$i] ?? '');
24+
}
25+
return $r;
26+
};
27+
}
28+
29+
#[Test]
30+
public function without_placeholders() {
31+
Assert::equals('Test', $this->evaluate('$f`Test`'));
32+
}
33+
34+
#[Test]
35+
public function escaped_backtick() {
36+
Assert::equals('Command: `ls -al`', $this->evaluate('$f`Command: \`ls -al\``'));
37+
}
38+
39+
#[Test]
40+
public function support_escape_sequences() {
41+
Assert::equals("A\n", $this->evaluate('$f`A\n\u{20ac}`'));
42+
}
43+
44+
#[Test]
45+
public function does_not_interpolate_variables() {
46+
Assert::equals('Used $literally', $this->evaluate('$f`Used $literally`'));
47+
}
48+
49+
#[Test]
50+
public function placeholder_at_beginning() {
51+
Assert::equals('test', $this->evaluate('$f`${"test"}`'));
52+
}
53+
54+
#[Test]
55+
public function evaluates_arguments() {
56+
Assert::equals('2 + 3 = 5', $this->evaluate('$f`2 + 3 = ${2 + 3}`'));
57+
}
58+
59+
#[Test]
60+
public function dollar_sign() {
61+
Assert::equals('Price is $1.99', $this->evaluate('$f`Price is $${1.99}`'));
62+
}
63+
64+
#[Test]
65+
public function braces() {
66+
Assert::equals('Supported on '.PHP_OS, $this->evaluate('$f`Supported on ${match (true) { default => PHP_OS }}`'));
67+
}
68+
69+
#[Test]
70+
public function evaluates_global_constant() {
71+
Assert::equals('PHP_OS = '.PHP_OS, $this->evaluate('$f`PHP_OS = ${PHP_OS}`'));
72+
}
73+
74+
#[Test]
75+
public function argument_passed() {
76+
Assert::equals(
77+
'This is a <a href="https://example.com/?a&amp;b">link</a>.',
78+
$this->evaluate('$f`This is a <a href="${$arguments[0]}">link</a>.`', ['https://example.com/?a&b'])
79+
);
80+
}
81+
82+
#[Test]
83+
public function quoted_string() {
84+
Assert::equals(
85+
'He said "Test"',
86+
$this->evaluate('$f`He said "Test"`')
87+
);
88+
}
89+
90+
#[Test]
91+
public function ternary_expression() {
92+
Assert::equals(
93+
'You are 17, you can not yet vote.',
94+
$this->evaluate('$f`You are ${$arguments[0]}, you can ${$arguments[0] < 18 ? "not yet vote" : "vote"}.`', [17])
95+
);
96+
}
97+
98+
#[Test]
99+
public function resolve_via_method() {
100+
$t= $this->type('class %T {
101+
private function plain($strings, ... $arguments) {
102+
return implode("", $strings);
103+
}
104+
105+
public function run() {
106+
return $this->plain`Test`;
107+
}
108+
}');
109+
110+
Assert::equals('Test', $t->newInstance()->run());
111+
}
112+
113+
#[Test]
114+
public function can_return_other_types_than_string() {
115+
$t= $this->type('class %T {
116+
private $base= "//example.com";
117+
118+
private function resource($strings, ... $arguments) {
119+
$path= "";
120+
foreach ($strings as $i => $string) {
121+
$path.= $string.rawurlencode($arguments[$i] ?? "");
122+
}
123+
return ["base" => $this->base, "path" => $path];
124+
}
125+
126+
public function run($userId) {
127+
return $this->resource`/users/${$userId}`;
128+
}
129+
}');
130+
131+
Assert::equals(['base' => '//example.com', 'path' => '/users/%40me'], $t->newInstance()->run('@me'));
132+
}
133+
134+
#[Test, Expect(class: Errors::class, message: '/Unexpected string "Test" \[line 1 of .+\]/')]
135+
public function cannot_suffix_other_literals() {
136+
$this->evaluate('$f"Test"');
137+
}
138+
}

0 commit comments

Comments
 (0)