Created
September 30, 2021 19:15
-
-
Save inxilpro/580dcfa1ce6cc65a57358d53912e1b90 to your computer and use it in GitHub Desktop.
Javascript string implementation for Laravel
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Support; | |
use Illuminate\Contracts\Support\Arrayable; | |
use Illuminate\Contracts\Support\Htmlable; | |
use Illuminate\Contracts\Support\Jsonable; | |
use Illuminate\Support\Str; | |
class JsString implements Htmlable | |
{ | |
protected const REQUIRED_FLAGS = JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_THROW_ON_ERROR; | |
public string $js; | |
protected int $flags = 0; | |
public static function of($data, int $flags = 0): self | |
{ | |
return new static($data, $flags); | |
} | |
public function __construct($data, int $flags = 0) | |
{ | |
$this->flags = $flags; | |
$this->js = $this->toJs($data); | |
} | |
public function toHtml(): string | |
{ | |
return $this->js; | |
} | |
public function __toString(): string | |
{ | |
return $this->toHtml(); | |
} | |
protected function toJs($data): string | |
{ | |
if ($data instanceof Arrayable) { | |
$data = $data->toArray(); | |
} | |
if (is_string($data)) { | |
return "'".substr($this->jsonEncode($data), 1, '-1')."'"; | |
} | |
return $this->jsonToJs($this->jsonEncode($data)); | |
} | |
protected function jsonEncode($data): string | |
{ | |
if ($data instanceof Jsonable) { | |
return $data->toJson($this->flags()); | |
} | |
return json_encode($data, $this->flags()); | |
} | |
protected function jsonToJs(string $json): string | |
{ | |
if ('[]' === $json || '{}' === $json) { | |
return $json; | |
} | |
if (Str::startsWith($json, ['"', '{', '['])) { | |
$json = json_encode($json, $this->flags()); | |
return "JSON.parse('".substr($json, 1, -1)."')"; | |
} | |
return $json; | |
} | |
protected function flags(): int | |
{ | |
return $this->flags | static::REQUIRED_FLAGS; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Tests\Feature\Support; | |
use App\Support\JsString; | |
use Illuminate\Contracts\Support\Arrayable; | |
use Illuminate\Contracts\Support\Jsonable; | |
use JsonSerializable; | |
use Tests\TestCase; | |
class JsStringTest extends TestCase | |
{ | |
public function test_scalars(): void | |
{ | |
$this->assertEquals('false', (string) JsString::of(false)); | |
$this->assertEquals('true', (string) JsString::of(true)); | |
$this->assertEquals('1', (string) JsString::of(1)); | |
$this->assertEquals('1.1', (string) JsString::of(1.1)); | |
$this->assertEquals( | |
"'\\u003Cdiv class=\\u0022foo\\u0022\\u003E\\u0027quoted html\\u0027\\u003C\\/div\\u003E'", | |
(string) JsString::of('<div class="foo">\'quoted html\'</div>') | |
); | |
} | |
public function test_arrays(): void | |
{ | |
$this->assertEquals( | |
"JSON.parse('[\\u0022hello\\u0022,\\u0022world\\u0022]')", | |
(string) JsString::of(['hello', 'world']) | |
); | |
$this->assertEquals( | |
"JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}')", | |
(string) JsString::of(['foo' => 'hello', 'bar' => 'world']) | |
); | |
} | |
public function test_objects(): void | |
{ | |
$this->assertEquals( | |
"JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}')", | |
(string) JsString::of((object) ['foo' => 'hello', 'bar' => 'world']) | |
); | |
} | |
public function test_json_serializable(): void | |
{ | |
$this->assertEquals( | |
"JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}')", | |
(string) JsString::of($this->jsonSerializable()) | |
); | |
} | |
public function test_jsonable(): void | |
{ | |
$this->assertEquals( | |
"JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}')", | |
(string) JsString::of($this->jsonable()) | |
); | |
} | |
public function test_arrayable(): void | |
{ | |
$this->assertEquals( | |
"JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}')", | |
(string) JsString::of($this->arrayable()) | |
); | |
} | |
public function test_in_browser(): void | |
{ | |
$tests = [ | |
'typeof '.(string) JsString::of(true).' === "boolean"', | |
(string) JsString::of(false).' === false', | |
(string) JsString::of(true).' === true', | |
'typeof '.(string) JsString::of(1).' === "number"', | |
(string) JsString::of(-1).' === -1', | |
(string) JsString::of(0).' === 0', | |
(string) JsString::of(1).' === 1', | |
'Number.isInteger('.(string) JsString::of(PHP_INT_MAX).')', | |
'Number.isInteger('.(string) JsString::of(PHP_INT_MIN).')', | |
(string) JsString::of(-1.5).' === -1.5', | |
(string) JsString::of(1.5).' === 1.5', | |
(string) JsString::of('<div class="foo">\'quoted html\'</div>').' === "<div class=\\"foo\\">\'quoted html\'</div>"', | |
(string) JsString::of('hello').'.length === 5', | |
(string) JsString::of('').'.length === 0', | |
'typeof '.(string) JsString::of('hello').' === "string"', | |
'Array.isArray('.(string) JsString::of([]).')', | |
'Array.isArray('.(string) JsString::of(['foo']).')', | |
(string) JsString::of([]).'.length === 0', | |
(string) JsString::of([false]).'.length === 1', | |
(string) JsString::of(['hello', 'world']).'[0] === "hello"', | |
(string) JsString::of(['hello', 'world']).'[1] === "world"', | |
'typeof '.(string) JsString::of(['foo' => 'hello', 'bar' => 'world']).' === "object"', | |
(string) JsString::of(['foo' => 'hello', 'bar' => 'world']).' instanceof Object', | |
(string) JsString::of(['foo' => 'hello', 'bar' => 'world']).'.foo === "hello"', | |
(string) JsString::of(['foo' => 'hello', 'bar' => 'world']).'.bar === "world"', | |
(string) JsString::of((object) ['foo' => 'hello', 'bar' => 'world']).'.bar === "world"', | |
(string) JsString::of($this->jsonSerializable()).'.bar === "world"', | |
(string) JsString::of($this->jsonable()).'.bar === "world"', | |
(string) JsString::of($this->arrayable()).'.bar === "world"', | |
]; | |
foreach ($tests as $test) { | |
echo "if (!({$test})) { throw new Error('Test failed: ' +".json_encode($test)."); }\n"; | |
} | |
echo "console.log('%cAll tests passed!', 'color:green');"; | |
$this->markTestIncomplete('Test need to be run in browser. See output.'); | |
} | |
protected function jsonSerializable(): JsonSerializable | |
{ | |
return new class() implements JsonSerializable { | |
public string $foo = 'not hello'; | |
public string $bar = 'not world'; | |
public function jsonSerialize() | |
{ | |
return ['foo' => 'hello', 'bar' => 'world']; | |
} | |
}; | |
} | |
protected function jsonable(): Jsonable | |
{ | |
return new class() implements Jsonable { | |
public string $foo = 'not hello'; | |
public string $bar = 'not world'; | |
public function toJson($options = 0) | |
{ | |
return json_encode(['foo' => 'hello', 'bar' => 'world'], $options); | |
} | |
}; | |
} | |
protected function arrayable(): Arrayable | |
{ | |
return new class() implements Arrayable { | |
public string $foo = 'not hello'; | |
public string $bar = 'not world'; | |
public function toArray() | |
{ | |
return ['foo' => 'hello', 'bar' => 'world']; | |
} | |
}; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
if (!(typeof true === "boolean")) { | |
throw new Error('Test failed: ' + "typeof true === \"boolean\""); | |
} | |
if (!(false === false)) { | |
throw new Error('Test failed: ' + "false === false"); | |
} | |
if (!(true === true)) { | |
throw new Error('Test failed: ' + "true === true"); | |
} | |
if (!(typeof 1 === "number")) { | |
throw new Error('Test failed: ' + "typeof 1 === \"number\""); | |
} | |
if (!(-1 === -1)) { | |
throw new Error('Test failed: ' + "-1 === -1"); | |
} | |
if (!(0 === 0)) { | |
throw new Error('Test failed: ' + "0 === 0"); | |
} | |
if (!(1 === 1)) { | |
throw new Error('Test failed: ' + "1 === 1"); | |
} | |
if (!(Number.isInteger(9223372036854775807))) { | |
throw new Error('Test failed: ' + "Number.isInteger(9223372036854775807)"); | |
} | |
if (!(Number.isInteger(-9223372036854775808))) { | |
throw new Error('Test failed: ' + "Number.isInteger(-9223372036854775808)"); | |
} | |
if (!(-1.5 === -1.5)) { | |
throw new Error('Test failed: ' + "-1.5 === -1.5"); | |
} | |
if (!(1.5 === 1.5)) { | |
throw new Error('Test failed: ' + "1.5 === 1.5"); | |
} | |
if (!('\u003Cdiv class=\u0022foo\u0022\u003E\u0027quoted html\u0027\u003C\/div\u003E' === "<div class=\"foo\">'quoted html'</div>")) { | |
throw new Error('Test failed: ' + "'\\u003Cdiv class=\\u0022foo\\u0022\\u003E\\u0027quoted html\\u0027\\u003C\\\/div\\u003E' === \"<div class=\\\"foo\\\">'quoted html'<\/div>\""); | |
} | |
if (!('hello'.length === 5)) { | |
throw new Error('Test failed: ' + "'hello'.length === 5"); | |
} | |
if (!(''.length === 0)) { | |
throw new Error('Test failed: ' + "''.length === 0"); | |
} | |
if (!(typeof 'hello' === "string")) { | |
throw new Error('Test failed: ' + "typeof 'hello' === \"string\""); | |
} | |
if (!(Array.isArray([]))) { | |
throw new Error('Test failed: ' + "Array.isArray([])"); | |
} | |
if (!(Array.isArray(JSON.parse('[\u0022foo\u0022]')))) { | |
throw new Error('Test failed: ' + "Array.isArray(JSON.parse('[\\u0022foo\\u0022]'))"); | |
} | |
if (!([].length === 0)) { | |
throw new Error('Test failed: ' + "[].length === 0"); | |
} | |
if (!(JSON.parse('[false]').length === 1)) { | |
throw new Error('Test failed: ' + "JSON.parse('[false]').length === 1"); | |
} | |
if (!(JSON.parse('[\u0022hello\u0022,\u0022world\u0022]')[0] === "hello")) { | |
throw new Error('Test failed: ' + "JSON.parse('[\\u0022hello\\u0022,\\u0022world\\u0022]')[0] === \"hello\""); | |
} | |
if (!(JSON.parse('[\u0022hello\u0022,\u0022world\u0022]')[1] === "world")) { | |
throw new Error('Test failed: ' + "JSON.parse('[\\u0022hello\\u0022,\\u0022world\\u0022]')[1] === \"world\""); | |
} | |
if (!(typeof JSON.parse('{\u0022foo\u0022:\u0022hello\u0022,\u0022bar\u0022:\u0022world\u0022}') === "object")) { | |
throw new Error('Test failed: ' + "typeof JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}') === \"object\""); | |
} | |
if (!(JSON.parse('{\u0022foo\u0022:\u0022hello\u0022,\u0022bar\u0022:\u0022world\u0022}') instanceof Object)) { | |
throw new Error('Test failed: ' + "JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}') instanceof Object"); | |
} | |
if (!(JSON.parse('{\u0022foo\u0022:\u0022hello\u0022,\u0022bar\u0022:\u0022world\u0022}').foo === "hello")) { | |
throw new Error('Test failed: ' + "JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}').foo === \"hello\""); | |
} | |
if (!(JSON.parse('{\u0022foo\u0022:\u0022hello\u0022,\u0022bar\u0022:\u0022world\u0022}').bar === "world")) { | |
throw new Error('Test failed: ' + "JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}').bar === \"world\""); | |
} | |
if (!(JSON.parse('{\u0022foo\u0022:\u0022hello\u0022,\u0022bar\u0022:\u0022world\u0022}').bar === "world")) { | |
throw new Error('Test failed: ' + "JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}').bar === \"world\""); | |
} | |
if (!(JSON.parse('{\u0022foo\u0022:\u0022hello\u0022,\u0022bar\u0022:\u0022world\u0022}').bar === "world")) { | |
throw new Error('Test failed: ' + "JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}').bar === \"world\""); | |
} | |
if (!(JSON.parse('{\u0022foo\u0022:\u0022hello\u0022,\u0022bar\u0022:\u0022world\u0022}').bar === "world")) { | |
throw new Error('Test failed: ' + "JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}').bar === \"world\""); | |
} | |
if (!(JSON.parse('{\u0022foo\u0022:\u0022hello\u0022,\u0022bar\u0022:\u0022world\u0022}').bar === "world")) { | |
throw new Error('Test failed: ' + "JSON.parse('{\\u0022foo\\u0022:\\u0022hello\\u0022,\\u0022bar\\u0022:\\u0022world\\u0022}').bar === \"world\""); | |
} | |
console.log('%cAll tests passed!', 'color:green'); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment