Skip to content

Instantly share code, notes, and snippets.

@inxilpro
Created September 30, 2021 19:15
Show Gist options
  • Save inxilpro/580dcfa1ce6cc65a57358d53912e1b90 to your computer and use it in GitHub Desktop.
Save inxilpro/580dcfa1ce6cc65a57358d53912e1b90 to your computer and use it in GitHub Desktop.
Javascript string implementation for Laravel
<?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;
}
}
<?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'];
}
};
}
}
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