├── VERSION
├── .github
├── FUNDING.yml
├── dependabot.yml
└── workflows
│ └── build.yml
├── .gitignore
├── .editorconfig
├── phpunit.xml.dist
├── composer.json
├── LICENSE
├── tests
├── composer.json
└── CommentTest.php
├── CHANGELOG.md
├── README.md
└── src
└── Comment.php
/VERSION:
--------------------------------------------------------------------------------
1 | 1.2.1
2 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: adhocore
2 | custom: ['https://paypal.me/ji10']
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # standards
2 | /.cache/
3 | /.env
4 | /.idea/
5 | /vendor/
6 | composer.lock
7 | *.cache
8 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: composer
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | time: "22:00"
8 | open-pull-requests-limit: 10
9 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | ; http://editorconfig.org
2 | ;
3 | ; Sublime: https://github.com/sindresorhus/editorconfig-sublime
4 | ; Phpstorm: https://plugins.jetbrains.com/plugin/7294-editorconfig
5 |
6 | root = true
7 |
8 | [*]
9 | indent_style = space
10 | indent_size = 2
11 | end_of_line = lf
12 | charset = utf-8
13 | trim_trailing_whitespace = true
14 | insert_final_newline = true
15 |
16 | [{*.md,*.php,composer.json,composer.lock}]
17 | indent_size = 4
18 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
14 |
15 | ./tests/
16 |
17 |
18 |
19 |
20 | ./src
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "adhocore/json-comment",
3 | "description": "Lightweight JSON comment stripper library for PHP",
4 | "type": "library",
5 | "keywords": [
6 | "json", "comment", "strip-comment"
7 | ],
8 | "license": "MIT",
9 | "authors": [
10 | {
11 | "name": "Jitendra Adhikari",
12 | "email": "jiten.adhikary@gmail.com"
13 | }
14 | ],
15 | "autoload": {
16 | "psr-4": {
17 | "Ahc\\Json\\": "src/"
18 | }
19 | },
20 | "autoload-dev": {
21 | "psr-4": {
22 | "Ahc\\Json\\Test\\": "tests/"
23 | }
24 | },
25 | "require": {
26 | "php": ">=7.0",
27 | "ext-ctype": "*"
28 | },
29 | "require-dev": {
30 | "phpunit/phpunit": "^7.5 || ^8.5"
31 | },
32 | "scripts": {
33 | "test": "phpunit",
34 | "test:cov": "phpunit --coverage-text --coverage-clover coverage.xml --coverage-html vendor/cov"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Jitendra Adhikari
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/tests/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "adhocore/json-comment",
3 | "description": "JSON comment stripper library for PHP.
4 | There is literal line break just above this line but that's okay",
5 | "type":/* This is creepy comment */ "library",
6 | "keywords": [
7 | "json",
8 | "comment",
9 | // Single line comment, Notice the comma below:
10 | "strip-comment",
11 | ],
12 | "license": "MIT",
13 | /*
14 | * This is a multiline comment.
15 | */
16 | "authors": [
17 | {
18 | "name": "Jitendra Adhikari",
19 | "email": "jiten.adhikary@gmail.com",
20 | },
21 | ],
22 | "autoload": {
23 | "psr-4": {
24 | "Ahc\\Json\\": "src/",
25 | },
26 | },
27 | "autoload-dev": {
28 | "psr-4": {
29 | "Ahc\\Json\\Test\\": "tests/",
30 | },
31 | },
32 | "require": {
33 | "php": ">=7.0",
34 | "ext-ctype": "*",
35 | },
36 | "require-dev": {
37 | "phpunit/phpunit": "^6.5 || ^7.5 || ^8.5",
38 | },
39 | "scripts": {
40 | "test": "phpunit",
41 | "echo": "echo '// This is not comment'",
42 | "test:cov": "phpunit --coverage-text",
43 | },
44 | }
45 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | pull_request:
6 |
7 | defaults:
8 | run:
9 | shell: bash
10 |
11 | concurrency:
12 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
13 | cancel-in-progress: true
14 |
15 | permissions:
16 | contents: read
17 |
18 | jobs:
19 |
20 | tests:
21 | name: Tests
22 | env:
23 | extensions: pcov
24 |
25 | strategy:
26 | matrix:
27 | include:
28 | - php: '7.1'
29 | - php: '7.2'
30 | - php: '7.3'
31 | - php: '7.4'
32 | - php: '8.0'
33 | - php: '8.1'
34 | - php: '8.2'
35 | fail-fast: true
36 |
37 | runs-on: ubuntu-20.04
38 |
39 | steps:
40 | - name: Checkout
41 | uses: actions/checkout@v2
42 | with:
43 | fetch-depth: 2
44 |
45 | - name: Setup PHP
46 | uses: shivammathur/setup-php@v2
47 | with:
48 | coverage: "none"
49 | ini-values: date.timezone=Asia/Bangkok,memory_limit=-1,default_socket_timeout=10,session.gc_probability=0,zend.assertions=1
50 | php-version: "${{ matrix.php }}"
51 | extensions: "${{ env.extensions }}"
52 | tools: flex
53 |
54 | - name: Before run
55 | run: |
56 | echo COLUMNS=120 >> $GITHUB_ENV
57 | for P in src tests; do find $P -type f -name '*.php' -exec php -l {} \;; done
58 |
59 | - name: Install dependencies
60 | run: composer install --no-progress --ansi -o
61 |
62 | - name: Run tests
63 | run: composer test:cov
64 |
65 | - name: Codecov
66 | run: bash <(curl -s https://codecov.io/bash)
67 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [1.2.1](https://github.com/adhocore/php-json-comment/releases/tag/1.2.1) (2022-10-02)
2 |
3 | ### Bug Fixes
4 | - Check file existence by @martijnengler (Jitendra Adhikari) [_0c8378d_](https://github.com/adhocore/php-json-comment/commit/0c8378d)
5 |
6 | ### Miscellaneous
7 | - Retire
14 |
15 |
16 | - Lightweight JSON comment stripper library for PHP.
17 | - Makes possible to have comment in any form of JSON data.
18 | - Supported comments: single line `// comment` or multi line `/* comment */`.
19 | - Also strips trailing comma at the end of array or object, eg:
20 | - `[1,2,,]` => `[1,2]`
21 | - `{"x":1,,}` => `{"x":1}`
22 | - Handles literal LF (newline/linefeed) within string notation so that we can have multiline string
23 | - Supports JSON string inside JSON string (see ticket [#15](https://github.com/adhocore/php-json-comment/issues/15) and PR [#16](https://github.com/adhocore/php-json-comment/pull/16))
24 | - Zero dependency (no vendor bloat).
25 |
26 | ## Installation
27 | ```bash
28 | composer require adhocore/json-comment
29 |
30 | # for php5.6
31 | composer require adhocore/json-comment:^0.2
32 | ```
33 |
34 | ## Usage
35 | ```php
36 | use Ahc\Json\Comment;
37 |
38 | // The JSON string!
39 | $someJsonText = '{"a":1,
40 | "b":2,// comment
41 | "c":3 /* inline comment */,
42 | // comment
43 | "d":/* also a comment */"d",
44 | /* creepy comment*/"e":2.3,
45 | /* multi line
46 | comment */
47 | "f":"f1",}';
48 |
49 | // OR
50 | $someJsonText = file_get_contents('...');
51 |
52 | // Strip only!
53 | (new Comment)->strip($someJsonText);
54 |
55 | // Strip and decode!
56 | (new Comment)->decode($someJsonText);
57 |
58 | // You can pass args like in `json_decode`
59 | (new Comment)->decode($someJsonText, $assoc = true, $depth = 512, $options = JSON_BIGINT_AS_STRING);
60 |
61 | // Or you can use static alias of decode:
62 | Comment::parse($json, true);
63 |
64 | # Or use file directly
65 | Comment::parseFromFile('/path/to/file.json', true);
66 | ```
67 |
68 | ### Example
69 |
70 | An example JSON that this library can parse:
71 |
72 | ```json
73 | {
74 | "name": "adhocore/json-comment",
75 | "description": "JSON comment stripper library for PHP.
76 | There is literal line break just above this line but that's okay",
77 | "type":/* This is creepy comment */ "library",
78 | "keywords": [
79 | "json",
80 | "comment",
81 | // Single line comment, Notice the comma below:
82 | "strip-comment",
83 | ],
84 | "license": "MIT",
85 | /*
86 | * This is a multiline
87 | * comment.
88 | */
89 | "authors": [
90 | {
91 | "name": "Jitendra Adhikari",
92 | "email": "jiten.adhikary@gmail.com",
93 | },
94 | ],
95 | "autoload": {
96 | "psr-4": {
97 | "Ahc\\Json\\": "src/",
98 | },
99 | },
100 | "autoload-dev": {
101 | "psr-4": {
102 | "Ahc\\Json\\Test\\": "tests/",
103 | },
104 | },
105 | "require": {
106 | "php": ">=7.0",
107 | "ext-ctype": "*",
108 | },
109 | "require-dev": {
110 | "phpunit/phpunit": "^6.5 || ^7.5 || ^8.5",
111 | },
112 | "scripts": {
113 | "test": "phpunit",
114 | "echo": "echo '// This is not comment'",
115 | "test:cov": "phpunit --coverage-text",
116 | },
117 | }
118 | ```
119 |
--------------------------------------------------------------------------------
/tests/CommentTest.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | *
9 | * Licensed under MIT license.
10 | */
11 |
12 | namespace Ahc\Json\Test;
13 |
14 | use Ahc\Json\Comment;
15 | use PHPUnit\Framework\TestCase;
16 |
17 | class CommentTest extends TestCase
18 | {
19 | /**
20 | * @dataProvider theTests
21 | */
22 | public function testStrip($json, $expect)
23 | {
24 | $this->assertSame($expect, (new Comment)->strip($json));
25 | }
26 |
27 | /**
28 | * @dataProvider theTests
29 | */
30 | public function testDecode($json)
31 | {
32 | $actual = (new Comment)->decode($json, true);
33 |
34 | $this->assertSame(JSON_ERROR_NONE, json_last_error());
35 | $this->assertNotEmpty($actual);
36 | $this->assertArrayHasKey('a', $actual);
37 | $this->assertArrayHasKey('b', $actual);
38 | }
39 |
40 | public function testDecodeThrows()
41 | {
42 | $this->expectException(\RuntimeException::class);
43 | $this->expectExceptionMessage('JSON decode failed');
44 |
45 | (new Comment)->decode('{"a":1, /* comment */, "b":}', true);
46 | }
47 |
48 | public function testParse()
49 | {
50 | $parsed = Comment::parse('{
51 | // comment
52 | "a//b":"/*value*/"
53 | /* also comment */
54 | }', true);
55 |
56 | $this->assertNotEmpty($parsed);
57 | $this->assertTrue(is_array($parsed));
58 | $this->assertArrayHasKey('a//b', $parsed);
59 | $this->assertSame('/*value*/', $parsed['a//b']);
60 | }
61 |
62 | public function testParseFromFile()
63 | {
64 | $parsed = Comment::parseFromFile(__DIR__ . '/composer.json', true);
65 |
66 | $this->assertTrue(is_array($parsed));
67 | $this->assertNotEmpty($parsed);
68 | $this->assertSame('adhocore/json-comment', $parsed['name']);
69 | }
70 |
71 | public function testParseFromFileThrowsNotExists()
72 | {
73 | $file = 'does-not-exist.json';
74 |
75 | $this->expectException(\InvalidArgumentException::class);
76 | $this->expectExceptionMessage($file . ' does not exist or is not a file');
77 |
78 | Comment::parseFromFile($file, true);
79 | }
80 |
81 | public function testParseFromFileThrowsNotFile()
82 | {
83 | $this->expectException(\InvalidArgumentException::class);
84 | $this->expectExceptionMessage(__DIR__ . ' does not exist or is not a file');
85 |
86 | Comment::parseFromFile(__DIR__, true);
87 | }
88 |
89 | public function testSubJson()
90 | {
91 | // https://github.com/adhocore/php-json-comment/issues/15
92 | $parsed = Comment::parse('{
93 | "jo": "{
94 | /* comment */
95 | \"url\": \"http://example.com\"//comment
96 | }",
97 | "x": {
98 | /* comment 1
99 | comment 2 */
100 | "y": {
101 | // comment
102 | "XY\\\": "//no comment/*",
103 | },
104 | }
105 | }', true);
106 |
107 | $this->assertArrayHasKey('jo', $parsed);
108 | $this->assertSame('//no comment/*', $parsed['x']['y']['XY\\']);
109 | $this->assertSame('http://example.com', Comment::parse($parsed['jo'])->url);
110 | }
111 |
112 | public function theTests()
113 | {
114 | return [
115 | 'without comment' => [
116 | 'json' => '{"a":1,"b":2}',
117 | 'expect' => '{"a":1,"b":2}',
118 | ],
119 | 'with trail only' => [
120 | 'json' => '{"a":1,"b":2,,}',
121 | 'expect' => '{"a":1,"b":2}',
122 | ],
123 | 'single line comment' => [
124 | 'json' => '{"a":1,
125 | // comment
126 | "b":2,
127 | // comment
128 | "c":3,,}',
129 | 'expect' => '{"a":1,
130 | "b":2,
131 | "c":3}',
132 | ],
133 | 'single line comment at end' => [
134 | 'json' => '{"a":1,
135 | "b":2,// comment
136 | "c":[1,2,,]}',
137 | 'expect' => '{"a":1,
138 | "b":2,
139 | "c":[1,2]}',
140 | ],
141 | 'real multiline comment' => [
142 | 'json' => '{"a":1,
143 | /*
144 | * comment
145 | */
146 | "b":2, "c":3,}',
147 | 'expect' => '{"a":1,
148 | ' . '
149 | "b":2, "c":3}',
150 | ],
151 | 'inline multiline comment' => [
152 | 'json' => '{"a":1,
153 | /* comment */ "b":2, "c":3}',
154 | 'expect' => '{"a":1,
155 | "b":2, "c":3}',
156 | ],
157 | 'inline multiline comment at end' => [
158 | 'json' => '{"a":1, "b":2, "c":3/* comment */,}',
159 | 'expect' => '{"a":1, "b":2, "c":3}',
160 | ],
161 | 'comment inside string' => [
162 | 'json' => '{"a": "a//b", "b":"a/* not really comment */b"}',
163 | 'expect' => '{"a": "a//b", "b":"a/* not really comment */b"}',
164 | ],
165 | 'escaped string' => [
166 | 'json' => '{"a": "a//b", "b":"a/* \"not really comment\" */b"}',
167 | 'expect' => '{"a": "a//b", "b":"a/* \"not really comment\" */b"}',
168 | ],
169 | 'string inside comment' => [
170 | 'json' => '{"a": "ab", /* also comment */ "b":"a/* not a comment */b" /* "comment string" */ }',
171 | 'expect' => '{"a": "ab", "b":"a/* not a comment */b" }',
172 | ],
173 | 'literal lf' => [
174 | 'json' => '{"a":/*literal linefeed*/"apple' . "\n" . 'ball","b":"","c\\\\":"",}',
175 | 'expect' => '{"a":"apple\nball","b":"","c\\\\":""}',
176 | ],
177 | ];
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/src/Comment.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | *
11 | * Licensed under MIT license.
12 | */
13 |
14 | namespace Ahc\Json;
15 |
16 | /**
17 | * JSON comment and trailing comma stripper.
18 | *
19 | * @author Jitendra Adhikari
20 | */
21 | class Comment
22 | {
23 | /** @var int The current index being scanned */
24 | protected $index = -1;
25 |
26 | /** @var bool If current char is within a string */
27 | protected $inStr = false;
28 |
29 | /** @var int Lines of comments 0 = no comment, 1 = single line, 2 = multi lines */
30 | protected $comment = 0;
31 |
32 | /** @var int Holds the backtace position of a possibly trailing comma */
33 | protected $commaPos = -1;
34 |
35 | /**
36 | * Strip comments from JSON string.
37 | *
38 | * @param string $json
39 | *
40 | * @return string The comment stripped JSON.
41 | */
42 | public function strip(string $json): string
43 | {
44 | if (!\preg_match('%\/(\/|\*)%', $json) && !\preg_match('/,\s*(\}|\])/', $json)) {
45 | return $json;
46 | }
47 |
48 | $this->reset();
49 |
50 | return $this->doStrip($json);
51 | }
52 |
53 | protected function reset()
54 | {
55 | $this->index = -1;
56 | $this->inStr = false;
57 | $this->comment = 0;
58 | }
59 |
60 | protected function doStrip(string $json): string
61 | {
62 | $return = '';
63 | $crlf = ["\n" => '\n', "\r" => '\r'];
64 |
65 | while (isset($json[++$this->index])) {
66 | $oldprev = $prev ?? '';
67 | list($prev, $char, $next) = $this->getSegments($json);
68 |
69 | $return = $this->checkTrail($char, $return);
70 |
71 | if ($this->inStringOrCommentEnd($prev, $char, $char . $next, $oldprev)) {
72 | $return .= $this->inStr && isset($crlf[$char]) ? $crlf[$char] : $char;
73 |
74 | continue;
75 | }
76 |
77 | $wasSingle = 1 === $this->comment;
78 | if ($this->hasCommentEnded($char, $char . $next) && $wasSingle) {
79 | $return = \rtrim($return) . $char;
80 | }
81 |
82 | $this->index += $char . $next === '*/' ? 1 : 0;
83 | }
84 |
85 | return $return;
86 | }
87 |
88 | protected function getSegments(string $json): array
89 | {
90 | return [
91 | $json[$this->index - 1] ?? '',
92 | $json[$this->index],
93 | $json[$this->index + 1] ?? '',
94 | ];
95 | }
96 |
97 | protected function checkTrail(string $char, string $json): string
98 | {
99 | if ($char === ',' || $this->commaPos === -1) {
100 | $this->commaPos = $this->commaPos + ($char === ',' ? 1 : 0);
101 |
102 | return $json;
103 | }
104 |
105 | if (\ctype_digit($char) || \strpbrk($char, '"tfn{[')) {
106 | $this->commaPos = -1;
107 | } elseif ($char === ']' || $char === '}') {
108 | $pos = \strlen($json) - $this->commaPos - 1;
109 | $json = \substr($json, 0, $pos) . \ltrim(\substr($json, $pos), ',');
110 |
111 | $this->commaPos = -1;
112 | } else {
113 | $this->commaPos += 1;
114 | }
115 |
116 | return $json;
117 | }
118 |
119 | protected function inStringOrCommentEnd(string $prev, string $char, string $next, string $oldprev): bool
120 | {
121 | return $this->inString($char, $prev, $next, $oldprev) || $this->inCommentEnd($next);
122 | }
123 |
124 | protected function inString(string $char, string $prev, string $next, string $oldprev): bool
125 | {
126 | if (0 === $this->comment && $char === '"' && $prev !== '\\') {
127 | return $this->inStr = !$this->inStr;
128 | }
129 |
130 | if ($this->inStr && \in_array($next, ['":', '",', '"]', '"}'], true)) {
131 | $this->inStr = "$oldprev$prev" !== '\\\\';
132 | }
133 |
134 | return $this->inStr;
135 | }
136 |
137 | protected function inCommentEnd(string $next): bool
138 | {
139 | if (!$this->inStr && 0 === $this->comment) {
140 | $this->comment = $next === '//' ? 1 : ($next === '/*' ? 2 : 0);
141 | }
142 |
143 | return 0 === $this->comment;
144 | }
145 |
146 | protected function hasCommentEnded(string $char, string $next): bool
147 | {
148 | $singleEnded = $this->comment === 1 && $char == "\n";
149 | $multiEnded = $this->comment === 2 && $next == '*/';
150 |
151 | if ($singleEnded || $multiEnded) {
152 | $this->comment = 0;
153 |
154 | return true;
155 | }
156 |
157 | return false;
158 | }
159 |
160 | /**
161 | * Strip comments and decode JSON string.
162 | *
163 | * @param string $json
164 | * @param bool $assoc
165 | * @param int $depth
166 | * @param int $options
167 | *
168 | * @see http://php.net/json_decode [JSON decode native function]
169 | *
170 | * @throws \RuntimeException When decode fails.
171 | *
172 | * @return mixed
173 | */
174 | public function decode(string $json, bool $assoc = false, int $depth = 512, int $options = 0)
175 | {
176 | $decoded = \json_decode($this->strip($json), $assoc, $depth, $options);
177 |
178 | if (\JSON_ERROR_NONE !== $err = \json_last_error()) {
179 | $msg = 'JSON decode failed';
180 |
181 | if (\function_exists('json_last_error_msg')) {
182 | $msg .= ': ' . \json_last_error_msg();
183 | }
184 |
185 | throw new \RuntimeException($msg, $err);
186 | }
187 |
188 | return $decoded;
189 | }
190 |
191 | /**
192 | * Static alias of decode().
193 | */
194 | public static function parse(string $json, bool $assoc = false, int $depth = 512, int $options = 0)
195 | {
196 | static $parser;
197 |
198 | if (!$parser) {
199 | $parser = new static;
200 | }
201 |
202 | return $parser->decode($json, $assoc, $depth, $options);
203 | }
204 |
205 | public static function parseFromFile(string $file, bool $assoc = false, int $depth = 512, int $options = 0)
206 | {
207 | if (!is_file($file)) {
208 | throw new \InvalidArgumentException($file . ' does not exist or is not a file');
209 | }
210 |
211 | $json = \file_get_contents($file);
212 |
213 | return static::parse(\trim($json), $assoc, $depth, $options);
214 | }
215 | }
216 |
--------------------------------------------------------------------------------