Skip to content

Commit 519365e

Browse files
committed
Merge branch 'jsstring' into 8.x
2 parents 1c834e4 + bbf47d5 commit 519365e

File tree

3 files changed

+251
-28
lines changed

3 files changed

+251
-28
lines changed

src/Illuminate/Support/Js.php

Lines changed: 127 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,144 @@
22

33
namespace Illuminate\Support;
44

5-
class Js
5+
use Illuminate\Contracts\Support\Arrayable;
6+
use Illuminate\Contracts\Support\Htmlable;
7+
use Illuminate\Contracts\Support\Jsonable;
8+
use JsonSerializable;
9+
10+
class Js implements Htmlable
611
{
712
/**
8-
* Convert an expression into a valid JavaScript object, JSON string, or string.
13+
* The JavaScript string.
14+
*
15+
* @var string
16+
*/
17+
protected $js;
18+
19+
/**
20+
* Flags that should be used when encoding to JSON.
21+
*
22+
* @var int
23+
*/
24+
protected const REQUIRED_FLAGS = JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_THROW_ON_ERROR;
25+
26+
/**
27+
* Create a new class instance.
28+
*
29+
* @param mixed $data
30+
* @param int|null $flags
31+
* @param int $depth
32+
* @return void
33+
*
34+
* @throws \JsonException
35+
*/
36+
public function __construct($data, $flags = 0, $depth = 512)
37+
{
38+
$this->js = $this->convertDataToJavaScriptExpression($data, $flags, $depth);
39+
}
40+
41+
/**
42+
* Create a new JavaScript string from the given data.
943
*
10-
* @param mixed $expression
11-
* @param int|null $options
44+
* @param mixed $data
45+
* @param int $flags
46+
* @param int $depth
47+
* @return static
48+
*
49+
* @throws \JsonException
50+
*/
51+
public static function from($data, $flags = 0, $depth = 512)
52+
{
53+
return new static($data, $flags, $depth);
54+
}
55+
56+
/**
57+
* Convert the given data to a JavaScript expression.
58+
*
59+
* @param mixed $data
60+
* @param int $flags
1261
* @param int $depth
1362
* @return string
63+
*
64+
* @throws \JsonException
1465
*/
15-
public static function from($expression, $options = null, $depth = 512)
66+
protected function convertDataToJavaScriptExpression($data, $flags = 0, $depth = 512)
1667
{
17-
if (is_object($expression) || is_array($expression)) {
18-
$base64 = base64_encode(json_encode($expression, $options, $depth));
68+
if ($data instanceof self) {
69+
return $data->toHtml();
70+
}
1971

20-
return "JSON.parse(atob('{$base64}'))";
72+
$json = $this->jsonEncode($data, $flags, $depth);
73+
74+
if (is_string($data)) {
75+
return "'".substr($json, 1, -1)."'";
2176
}
2277

23-
if (is_string($expression)) {
24-
return "'".str_replace("'", "\'", str_replace('\\', '\\\\', $expression))."'";
78+
return $this->convertJsonToJavaScriptExpression($json, $flags);
79+
}
80+
81+
/**
82+
* Encode the given data as JSON.
83+
*
84+
* @param mixed $data
85+
* @param int $flags
86+
* @param int $depth
87+
* @return string
88+
*
89+
* @throws \JsonException
90+
*/
91+
protected function jsonEncode($data, $flags = 0, $depth = 512)
92+
{
93+
if ($data instanceof Jsonable) {
94+
return $data->toJson($flags | static::REQUIRED_FLAGS);
95+
}
96+
97+
if ($data instanceof Arrayable && ! ($data instanceof JsonSerializable)) {
98+
$data = $data->toArray();
99+
}
100+
101+
return json_encode($data, $flags | static::REQUIRED_FLAGS, $depth);
102+
}
103+
104+
/**
105+
* Convert the given JSON to a JavaScript expression.
106+
*
107+
* @param string $json
108+
* @param int $flags
109+
* @return string
110+
*
111+
* @throws \JsonException
112+
*/
113+
protected function convertJsonToJavaScriptExpression($json, $flags = 0)
114+
{
115+
if ('[]' === $json || '{}' === $json) {
116+
return $json;
25117
}
26118

27-
return json_encode($expression, $options, $depth);
119+
if (Str::startsWith($json, ['"', '{', '['])) {
120+
return "JSON.parse('".substr(json_encode($json, $flags | static::REQUIRED_FLAGS), 1, -1)."')";
121+
}
122+
123+
return $json;
124+
}
125+
126+
/**
127+
* Get the string representation of the data for use in HTML.
128+
*
129+
* @return string
130+
*/
131+
public function toHtml()
132+
{
133+
return $this->js;
134+
}
135+
136+
/**
137+
* Get the string representation of the data for use in HTML.
138+
*
139+
* @return string
140+
*/
141+
public function __toString()
142+
{
143+
return $this->toHtml();
28144
}
29145
}

tests/Support/SupportJsStringTest.php

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Support;
4+
5+
use Illuminate\Contracts\Support\Arrayable;
6+
use Illuminate\Contracts\Support\Jsonable;
7+
use Illuminate\Support\Js;
8+
use JsonSerializable;
9+
use PHPUnit\Framework\TestCase;
10+
11+
class SupportJsStringTest extends TestCase
12+
{
13+
public function testScalars()
14+
{
15+
$this->assertEquals('false', (string) Js::from(false));
16+
$this->assertEquals('true', (string) Js::from(true));
17+
$this->assertEquals('1', (string) Js::from(1));
18+
$this->assertEquals('1.1', (string) Js::from(1.1));
19+
$this->assertEquals(
20+
"'\\u003Cdiv class=\\u0022foo\\u0022\\u003E\\u0027quoted html\\u0027\\u003C\\/div\\u003E'",
21+
(string) Js::from('<div class="foo">\'quoted html\'</div>')
22+
);
23+
}
24+
25+
public function testArrays()
26+
{
27+
$this->assertEquals(
28+
"JSON.parse('[\\u0022hello\\u0022,\\u0022world\\u0022]')",
29+
(string) Js::from(['hello', 'world'])
30+
);
31+
32+
$this->assertEquals(
33+
"JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}')",
34+
(string) Js::from(['foo' => 'hello', 'bar' => 'world'])
35+
);
36+
}
37+
38+
public function testObjects()
39+
{
40+
$this->assertEquals(
41+
"JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}')",
42+
(string) Js::from((object) ['foo' => 'hello', 'bar' => 'world'])
43+
);
44+
}
45+
46+
public function testJsonSerializable()
47+
{
48+
// JsonSerializable should take precedence over Arrayable, so we'll
49+
// implement both and make sure the correct data is used.
50+
$data = new class() implements JsonSerializable, Arrayable
51+
{
52+
public $foo = 'not hello';
53+
54+
public $bar = 'not world';
55+
56+
public function jsonSerialize()
57+
{
58+
return ['foo' => 'hello', 'bar' => 'world'];
59+
}
60+
61+
public function toArray()
62+
{
63+
return ['foo' => 'not hello', 'bar' => 'not world'];
64+
}
65+
};
66+
67+
$this->assertEquals(
68+
"JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}')",
69+
(string) Js::from($data)
70+
);
71+
}
72+
73+
public function testJsonable()
74+
{
75+
// Jsonable should take precedence over JsonSerializable and Arrayable, so we'll
76+
// implement all three and make sure the correct data is used.
77+
$data = new class() implements Jsonable, JsonSerializable, Arrayable
78+
{
79+
public $foo = 'not hello';
80+
81+
public $bar = 'not world';
82+
83+
public function toJson($options = 0)
84+
{
85+
return json_encode(['foo' => 'hello', 'bar' => 'world'], $options);
86+
}
87+
88+
public function jsonSerialize()
89+
{
90+
return ['foo' => 'not hello', 'bar' => 'not world'];
91+
}
92+
93+
public function toArray()
94+
{
95+
return ['foo' => 'not hello', 'bar' => 'not world'];
96+
}
97+
};
98+
99+
$this->assertEquals(
100+
"JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}')",
101+
(string) Js::from($data)
102+
);
103+
}
104+
105+
public function testArrayable()
106+
{
107+
$data = new class() implements Arrayable
108+
{
109+
public $foo = 'not hello';
110+
111+
public $bar = 'not world';
112+
113+
public function toArray()
114+
{
115+
return ['foo' => 'hello', 'bar' => 'world'];
116+
}
117+
};
118+
119+
$this->assertEquals(
120+
"JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}')",
121+
(string) Js::from($data)
122+
);
123+
}
124+
}

tests/Support/SupportJsTest.php

Lines changed: 0 additions & 17 deletions
This file was deleted.

0 commit comments

Comments
 (0)