Skip to content

Commit 3545a00

Browse files
committed
Add support for single-quoted strings and fix stringifying whitespace
Fixes #18
1 parent 97a35e9 commit 3545a00

15 files changed

Lines changed: 155 additions & 13 deletions

src/HJSON/HJSONParser.php

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public function __construct()
1515
{
1616
$this->escapee = [
1717
'"' => '"',
18+
'\'' => '\'',
1819
"\\" => "\\",
1920
'/' => '/',
2021
'b' => chr(8),
@@ -99,23 +100,27 @@ private function value()
99100
case '[':
100101
return $this->_array();
101102
case '"':
102-
return $this->string();
103+
return $this->string('"');
104+
case '\'':
105+
if ($this->peek(0) !== '\'' || $this->peek(1) !== '\'') {
106+
return $this->string('\'');
107+
}
103108
default:
104109
return $this->tfnns();
105110
}
106111
}
107112

108-
private function string()
113+
private function string($quote)
109114
{
110115
// Parse a string value.
111116
$hex;
112117
$string = '';
113118
$uffff;
114119

115120
// When parsing for string values, we must look for " and \ characters.
116-
if ($this->ch === '"') {
121+
if ($this->ch === $quote) {
117122
while ($this->next() !== null) {
118-
if ($this->ch === '"') {
123+
if ($this->ch === $quote) {
119124
$this->next();
120125
return $string;
121126
}
@@ -410,7 +415,9 @@ private function keyname()
410415
// unless they include {}[],: or whitespace.
411416

412417
if ($this->ch === '"') {
413-
return $this->string();
418+
return $this->string('"');
419+
} else if ($this->ch === '\'') {
420+
return $this->string('\'');
414421
}
415422

416423
$name = "";

src/HJSON/HJSONStringifier.php

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22

33
namespace HJSON;
44

5+
/**
6+
* NOTE: this may return an empty string at the end of the array when the input
7+
* string ends with a newline character
8+
*/
59
function mb_str_split($string)
610
{
7-
return preg_split('/(?<!^)(?!$)/u', $string);
11+
return preg_split('/(?<!^)/u', $string);
812
}
913

1014
class HJSONStringifier
@@ -13,11 +17,12 @@ class HJSONStringifier
1317
// needsEscape tests if the string can be written without escapes
1418
private $needsEscape = '/[\\\"\x00-\x1f\x7f-\x9f\x{00ad}\x{0600}-\x{0604}\x{070f}\x{17b4}\x{17b5}\x{200c}-\x{200f}\x{2028}-\x{202f}\x{2060}-\x{206f}\x{feff}\x{fff0}-\x{ffff}\x]/u';
1519
// needsQuotes tests if the string can be written as a quoteless string (includes needsEscape but without \\ and \")
16-
private $needsQuotes = '/^\\s|^"|^\'\'\'|^#|^\\/\\*|^\\/\\/|^\\{|^\\}|^\\[|^\\]|^:|^,|\\s$|[\x00-\x1f\x7f-\x9f\x{00ad}\x{0600}-\x{0604}\x{070f}\x{17b4}\x{17b5}\x{200c}-\x{200f}\x{2028}-\x{202f}\x{2060}-\x{206f}\x{feff}\x{fff0}-\x{ffff}\x]/u';
20+
private $needsQuotes = '/^\\s|^"|^\'|^\'\'\'|^#|^\\/\\*|^\\/\\/|^\\{|^\\}|^\\[|^\\]|^:|^,|\\s$|[\x00-\x1f\x7f-\x9f\x{00ad}\x{0600}-\x{0604}\x{070f}\x{17b4}\x{17b5}\x{200c}-\x{200f}\x{2028}-\x{202f}\x{2060}-\x{206f}\x{feff}\x{fff0}-\x{ffff}\x]/u';
1721
// needsEscapeML tests if the string can be written as a multiline string (includes needsEscape but without \n, \r, \\ and \")
18-
private $needsEscapeML = '/\'\'\'|[\x00-\x09\x0b\x0c\x0e-\x1f\x7f-\x9f\x{00ad}\x{0600}-\x{0604}\x{070f}\x{17b4}\x{17b5}\x{200c}-\x{200f}\x{2028}-\x{202f}\x{2060}-\x{206f}\x{feff}\x{fff0}-\x{ffff}\x]/u';
22+
private $needsEscapeML = '/\'\'\'|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f\x{00ad}\x{0600}-\x{0604}\x{070f}\x{17b4}\x{17b5}\x{200c}-\x{200f}\x{2028}-\x{202f}\x{2060}-\x{206f}\x{feff}\x{fff0}-\x{ffff}\x]/u';
23+
private $onlyWhitespace = '/^\\s+$/';
1924
private $startsWithKeyword = '/^(true|false|null)\s*((,|\]|\}|#|\/\/|\/\*).*)?$/';
20-
private $needsEscapeName = '/[,\{\[\}\]\s:#"]|\/\/|\/\*|\'\'\'/';
25+
private $needsEscapeName = '/[,\{\[\}\]\s:#"\']|\/\/|\/\*|\'\'\'/';
2126
private $gap = '';
2227
private $indent = ' ';
2328

@@ -38,6 +43,7 @@ public function __construct()
3843
"\n" => "\\n",
3944
"\r" => "\\r",
4045
'"' => '\\"',
46+
'\'' => '\\\'',
4147
'\\' => "\\\\"
4248
];
4349
$this->meta[chr(8)] = '\\b';
@@ -136,7 +142,11 @@ private function quote($string = null, $gap = null, $hasComment = null, $isRootO
136142

137143
if (!preg_match($this->needsEscape, $string)) {
138144
return '"' . $string . '"';
139-
} elseif (!preg_match($this->needsEscapeML, $string) && !$isRootObject) {
145+
} elseif (
146+
!preg_match($this->needsEscapeML, $string) &&
147+
!preg_match($this->onlyWhitespace, $string) &&
148+
!$isRootObject
149+
) {
140150
return $this->mlString($string, $gap);
141151
} else {
142152
return '"' . $this->quoteReplace($string) . '"';

tests/assets/failJSON24_test.json

Lines changed: 0 additions & 1 deletion
This file was deleted.

tests/assets/failKey5_test.hjson

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
# invalid name
3+
'''foo''': 0
4+
}

tests/assets/failStr8a_test.hjson

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
# invalid ml-string
3+
foo : ""'text'''
4+
}

tests/assets/keys_result.hjson

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
"foo\"bar": test
1919
"'''": test
2020
"foo'''bar": test
21+
"'": test
22+
"'foo": test
23+
"foo'bar": test
2124
":": test
2225
"foo:bar": test
2326
"{": test

tests/assets/keys_result.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
"foo\"bar": "test",
1919
"'''": "test",
2020
"foo'''bar": "test",
21+
"'": "test",
22+
"'foo": "test",
23+
"foo'bar": "test",
2124
":": "test",
2225
"foo:bar": "test",
2326
"{": "test",

tests/assets/keys_test.hjson

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
"foo\"bar": test
2323
"'''": test
2424
"foo'''bar": test
25+
"'": test
26+
"'foo": test
27+
"foo'bar": test
2528
# control char in key name
2629
":": test
2730
"foo:bar": test

tests/assets/strings2_result.hjson

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
key1: a key in single quotes
3+
"key 2": a key in single quotes
4+
"key \"": a key in single quotes
5+
text:
6+
[
7+
single quoted string
8+
'''You need quotes for escapes'''
9+
" untrimmed "
10+
"untrimmed "
11+
containing " double quotes
12+
containing " double quotes
13+
containing " double quotes
14+
'''"containing more " double quotes"'''
15+
containing ' single quotes
16+
containing ' single quotes
17+
containing ' single quotes
18+
"'containing more ' single quotes'"
19+
"'containing more ' single quotes'"
20+
"\n"
21+
" \n"
22+
"\n \n \n \n"
23+
"\t\n"
24+
]
25+
foo3a: asdf'''
26+
foo3b: "'''asdf"
27+
foo4a: "asdf'''\nasdf"
28+
foo4b: "asdf\n'''asdf"
29+
}

tests/assets/strings2_result.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"key1": "a key in single quotes",
3+
"key 2": "a key in single quotes",
4+
"key \"": "a key in single quotes",
5+
"text": [
6+
"single quoted string",
7+
"You need quotes\tfor escapes",
8+
" untrimmed ",
9+
"untrimmed ",
10+
"containing \" double quotes",
11+
"containing \" double quotes",
12+
"containing \" double quotes",
13+
"\"containing more \" double quotes\"",
14+
"containing ' single quotes",
15+
"containing ' single quotes",
16+
"containing ' single quotes",
17+
"'containing more ' single quotes'",
18+
"'containing more ' single quotes'",
19+
"\n",
20+
" \n",
21+
"\n \n \n \n",
22+
"\t\n"
23+
],
24+
"foo3a": "asdf'''",
25+
"foo3b": "'''asdf",
26+
"foo4a": "asdf'''\nasdf",
27+
"foo4b": "asdf\n'''asdf"
28+
}

0 commit comments

Comments
 (0)