├── .gitignore
├── lua
└── .gitignore
├── taplo.toml
├── install.ps1
├── src
├── lib.rs
├── val.rs
├── json5.pest
└── parser.rs
├── .cargo
└── config.toml
├── Cargo.toml
├── install.sh
├── GRAMMAR_LICENSE
├── LICENSE
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | Cargo.lock
3 |
--------------------------------------------------------------------------------
/lua/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/taplo.toml:
--------------------------------------------------------------------------------
1 | formatting.align_entries = true
2 |
--------------------------------------------------------------------------------
/install.ps1:
--------------------------------------------------------------------------------
1 | $TARGET_DIR = if ($env:CARGO_TARGET_DIR) { $env:CARGO_TARGET_DIR } else { "target" }
2 |
3 | cargo build --features luajit --release --target-dir "$TARGET_DIR"
4 | Move-Item -Path "$TARGET_DIR\release\lua_json5.dll" -Destination lua\json5.dll
5 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | use mlua::{Lua, Result, Table};
2 | pub mod parser;
3 | pub mod val;
4 |
5 | #[mlua::lua_module]
6 | fn json5(lua: &Lua) -> Result
{
7 | let exports = lua.create_table()?;
8 | exports.set("parse", lua.create_function(parser::parse)?)?;
9 | Ok(exports)
10 | }
11 |
--------------------------------------------------------------------------------
/.cargo/config.toml:
--------------------------------------------------------------------------------
1 | [target.x86_64-apple-darwin]
2 | rustflags = [
3 | "-C", "link-arg=-undefined",
4 | "-C", "link-arg=dynamic_lookup",
5 | ]
6 |
7 | [target.aarch64-apple-darwin]
8 | rustflags = [
9 | "-C", "link-arg=-undefined",
10 | "-C", "link-arg=dynamic_lookup",
11 | ]
12 |
13 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "lua-json5"
3 | version = "0.1.0"
4 | edition = "2024"
5 |
6 | [lib]
7 | crate-type = ["cdylib"]
8 |
9 | [features]
10 | lua54 = ["mlua/lua54"]
11 | luajit = ["mlua/luajit"]
12 | default = ["luajit"]
13 |
14 | [dependencies]
15 | pest = "2.1"
16 | pest_derive = "2.1"
17 | mlua = { version = "0.10", features = ["module", "macros"] }
18 |
--------------------------------------------------------------------------------
/install.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | TARGET_DIR=${CARGO_TARGET_DIR:-target}
4 |
5 | cargo build --features luajit --release --target-dir "$TARGET_DIR"
6 |
7 | case $OSTYPE in
8 | "linux-gnu"*)
9 | mv "$TARGET_DIR"/release/liblua_json5.so lua/json5.so
10 | strip lua/json5.so
11 | ;;
12 | "darwin"*)
13 | # Provide both just in case
14 | cp "$TARGET_DIR"/release/liblua_json5.dylib lua/json5.dylib
15 | cp lua/json5.dylib lua/json5.so
16 | ;;
17 | esac
18 |
--------------------------------------------------------------------------------
/src/val.rs:
--------------------------------------------------------------------------------
1 | use mlua::{IntoLua, Lua, Nil, Result, Value as LuaValue};
2 | use std::collections::HashMap;
3 |
4 | pub enum Value {
5 | Null,
6 | Array(Vec),
7 | Object(HashMap),
8 | String(String),
9 | Number(f64),
10 | Boolean(bool),
11 | }
12 |
13 | impl IntoLua for Value {
14 | fn into_lua(self, lua: &Lua) -> Result {
15 | match self {
16 | Self::Null => Ok(Nil),
17 | Self::Array(a) => a.into_lua(lua),
18 | Self::String(s) => s.into_lua(lua),
19 | Self::Number(n) => n.into_lua(lua),
20 | Self::Boolean(b) => b.into_lua(lua),
21 | Self::Object(o) => o.into_lua(lua),
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/GRAMMAR_LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2018 Callum Oakley
2 |
3 | Permission to use, copy, modify, and/or distribute this software for any
4 | purpose with or without fee is hereby granted, provided that the above
5 | copyright notice and this permission notice appear in all copies.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
11 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
12 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
13 | PERFORMANCE OF THIS SOFTWARE.
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Joaquín Andrés León Ulloa
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Json5 parser for luajit
2 |
3 | This crate provides json5 deserialization for luajit.
4 |
5 | Inspired and adapted from [json5-rs](https://github.com/callum-oakley/json5-rs)
6 |
7 | Also, if you haven't already, add ';?.dylib' to your `package.cpath` so it will
8 | be recognized by the interpreter.
9 |
10 | ## Usage
11 |
12 | You can simply require the module in your scripts and parse a string using the
13 | `parse` method:
14 |
15 | ```lua
16 | local parse = require'json5'.parse
17 | local data = [[
18 | {
19 | /* This is a comment */
20 | ecma_identifier: 'works like a charm',
21 | "string keys": [1,2,3], // trailing comma
22 | }
23 | ]]
24 | local parsed_data = parse(data)
25 | ```
26 |
27 | ## Use with neovim
28 |
29 | You must have `cargo` installed and in your `$PATH`
30 |
31 | Using [packer.nvim](https://github.com/wbthomason/packer.nvim):
32 |
33 | ```lua
34 | use {
35 | 'Joakker/lua-json5',
36 | -- if you're on windows
37 | -- run = 'powershell ./install.ps1'
38 | run = './install.sh'
39 | }
40 | ```
41 |
42 | Using [lazy.nvim](https://github.com/folke/lazy.nvim.git)
43 |
44 | ```lua
45 | {
46 | 'Joakker/lua-json5',
47 | build = './install.sh',
48 | }
49 | ```
50 |
51 | ## Lua 5.4
52 |
53 | You can also build the library for lua 5.4 using the following command:
54 |
55 | ```sh
56 | cargo build --no-default-features --features lua54 --release
57 | ```
58 |
59 | ## Performance
60 |
61 | Tested on neovim using the following script:
62 |
63 | ```lua
64 | local data = [[ {"hello":"world"} ]]
65 | local json5 = require('json5').parse
66 | local json_decode = vim.fn.json_decode
67 |
68 | local time_json5, time_json_decode = 0, 0
69 |
70 | local aux
71 |
72 | for _ = 1, 1000 do
73 | aux = os.clock()
74 | json5(data)
75 | time_json5 = time_json5 + (os.clock() - aux)
76 | end
77 |
78 | for _ = 1, 1000 do
79 | aux = os.clock()
80 | json_decode(data)
81 | time_json_decode = time_json_decode + (os.clock() - aux)
82 | end
83 |
84 | print(('json5: %.3fms'):format(time_json5))
85 | print(('json_decode: %.3fms'):format(time_json_decode))
86 | ```
87 |
88 | On average:
89 | ```
90 | json5: 0.023ms
91 | json_decode: 0.010ms
92 | ```
93 |
94 | ## So, why should I use this instead of the builtin `json_decode`?
95 |
96 | If performance is your concern, I think you're better off using the builtin
97 | function `json_decode`. The advantage this package has over regular json,
98 | however, is that you get json5 features, such as comments, trailing commas and
99 | more flexible string literals.
100 |
--------------------------------------------------------------------------------
/src/json5.pest:
--------------------------------------------------------------------------------
1 | // Adapted from https://github.com/callum-oakley/json5-rs/blob/master/src/json5.pest
2 |
3 | // see https://spec.json5.org/#syntactic-grammar and
4 | // https://spec.json5.org/#lexical-grammar
5 |
6 | COMMENT = _{ "/*" ~ (!"*/" ~ ANY)* ~ "*/" | "//" ~ (!line_terminator ~ ANY)* }
7 |
8 | WHITESPACE = _{
9 | "\u{0009}" |
10 | "\u{000B}" |
11 | "\u{000C}" |
12 | "\u{0020}" |
13 | "\u{00A0}" |
14 | "\u{FEFF}" |
15 | SPACE_SEPARATOR |
16 | line_terminator
17 | }
18 |
19 | array = { "[" ~ "]" | "[" ~ value ~ ("," ~ value)* ~ ","? ~ "]" }
20 |
21 | boolean = @{ "true" | "false" }
22 |
23 | char_escape_sequence = @{ single_escape_char | non_escape_char }
24 |
25 | char_literal = @{ !("\\" | line_terminator) ~ ANY }
26 |
27 | decimal_integer_literal = _{ "0" | ASCII_NONZERO_DIGIT ~ ASCII_DIGIT* }
28 |
29 | decimal_literal = _{
30 | decimal_integer_literal ~ "." ~ ASCII_DIGIT* ~ exponent_part? |
31 | "." ~ ASCII_DIGIT+~ exponent_part? |
32 | decimal_integer_literal ~ exponent_part?
33 | }
34 |
35 | double_quote_char = _{
36 | "\\" ~ escape_sequence |
37 | line_continuation |
38 | !"\"" ~ char_literal
39 | }
40 |
41 | escape_char = _{ single_escape_char | ASCII_DIGIT | "x" | "u" }
42 |
43 | escape_sequence = _{
44 | char_escape_sequence |
45 | nul_escape_sequence |
46 | "x" ~ hex_escape_sequence |
47 | "u" ~ unicode_escape_sequence
48 | }
49 |
50 | exponent_part = _{ ^"e" ~ ("+" | "-")? ~ ASCII_DIGIT+ }
51 |
52 | hex_escape_sequence = @{ ASCII_HEX_DIGIT{2} }
53 |
54 | hex_integer_literal = _{ ^"0x" ~ ASCII_HEX_DIGIT+ }
55 |
56 | identifier = ${ identifier_start ~ identifier_part* }
57 |
58 | identifier_part = _{
59 | identifier_start |
60 | &(
61 | NONSPACING_MARK |
62 | DIACRITIC | // not sure about this, spec says "Combining spacing mark (Mc)"
63 | DECIMAL_NUMBER |
64 | CONNECTOR_PUNCTUATION |
65 | "\u{200C}" |
66 | "\u{200D}"
67 | ) ~ char_literal
68 | }
69 |
70 | identifier_start = _{
71 | &(unicode_letter | "$" | "_") ~ char_literal |
72 | "\\u" ~ unicode_escape_sequence
73 | }
74 |
75 | key = _{ identifier | string }
76 |
77 | line_continuation = _{ "\\" ~ line_terminator_sequence }
78 |
79 | line_terminator = _{ "\u{000A}" | "\u{000D}" | "\u{2028}" | "\u{2029}" }
80 |
81 | line_terminator_sequence = _{ "\u{000D}" ~ "\u{000A}" | line_terminator }
82 |
83 | non_escape_char = _{ !(escape_char | line_terminator) ~ ANY }
84 |
85 | nul_escape_sequence = @{ "0" }
86 |
87 | null = @{ "null" }
88 |
89 | number = @{ ("+" | "-")? ~ numeric_literal }
90 |
91 | numeric_literal = _{
92 | hex_integer_literal |
93 | decimal_literal |
94 | "Infinity" |
95 | "NaN"
96 | }
97 |
98 | object = { "{" ~ "}" | "{" ~ pair ~ ("," ~ pair)* ~ ","? ~ "}" }
99 |
100 | pair = { key ~ ":" ~ value }
101 |
102 | single_escape_char = _{ "'" | "\"" | "\\" | "b" | "f" | "n" | "r" | "t" | "v" }
103 |
104 | single_quote_char = _{
105 | "\\" ~ escape_sequence |
106 | line_continuation |
107 | !"'" ~ char_literal
108 | }
109 |
110 | string = ${ "\"" ~ double_quote_char* ~ "\"" | "'" ~ single_quote_char* ~ "'" }
111 |
112 | text = _{ SOI ~ value ~ EOI }
113 |
114 | unicode_escape_sequence = @{ ASCII_HEX_DIGIT{4} }
115 |
116 | unicode_letter = _{
117 | UPPERCASE_LETTER |
118 | LOWERCASE_LETTER |
119 | TITLECASE_LETTER |
120 | MODIFIER_LETTER |
121 | OTHER_LETTER |
122 | LETTER_NUMBER
123 | }
124 |
125 | value = _{ null | boolean | string | number | object | array }
126 |
--------------------------------------------------------------------------------
/src/parser.rs:
--------------------------------------------------------------------------------
1 | use mlua::{Error::ExternalError, IntoLua, Lua, Result, Value as LuaValue};
2 | use pest::Parser;
3 | use pest::iterators::Pair;
4 | use std::collections::HashMap;
5 | use std::sync::Arc;
6 |
7 | use crate::val::Value;
8 |
9 | #[derive(pest_derive::Parser)]
10 | #[grammar = "json5.pest"]
11 | struct Json5Parser;
12 |
13 | fn parse_str(pair: Pair) -> String {
14 | let mut buf = Vec::::with_capacity(pair.as_str().len());
15 | for p in pair.into_inner() {
16 | match p.as_rule() {
17 | Rule::char_literal => buf.extend(p.as_str().encode_utf16()),
18 | Rule::nul_escape_sequence => buf.push(0),
19 | Rule::char_escape_sequence => match p.as_str() {
20 | "n" => buf.push(0xA),
21 | "r" => buf.push(0xD),
22 | "t" => buf.push(0x9),
23 | "b" => buf.push(0x8),
24 | "v" => buf.push(0xB),
25 | "f" => buf.push(0xC),
26 | k => buf.extend(k.encode_utf16()),
27 | },
28 | Rule::hex_escape_sequence => {
29 | let s = p.as_str();
30 | let hex = u8::from_str_radix(s, 16).unwrap_or(0);
31 | buf.push(hex as u16);
32 | }
33 | Rule::unicode_escape_sequence => {
34 | if let Ok(v) = u16::from_str_radix(p.as_str(), 16) {
35 | buf.push(v)
36 | }
37 | }
38 | _ => unreachable!(),
39 | }
40 | }
41 | String::from_utf16_lossy(&buf)
42 | }
43 |
44 | #[test]
45 | fn test_char_espace_sequence() {
46 | let mut pairs = Json5Parser::parse(Rule::string, r#""\t""#).unwrap();
47 | let s = parse_str(pairs.next().unwrap());
48 | assert_eq!(s, "\t")
49 | }
50 |
51 | #[test]
52 | fn test_hex_espace_sequence() {
53 | let mut pairs = Json5Parser::parse(Rule::string, r#""\x0A""#).unwrap();
54 | let s = parse_str(pairs.next().unwrap());
55 | assert_eq!(s, "\n")
56 | }
57 |
58 | #[test]
59 | fn test_unicode_espace_sequence_surrogate() {
60 | let mut pairs = Json5Parser::parse(Rule::string, r#""\ud834\udd1e""#).unwrap();
61 | let s = parse_str(pairs.next().unwrap());
62 | assert_eq!(s, "𝄞")
63 | }
64 |
65 | #[test]
66 | fn test_unicode_espace_sequence() {
67 | let mut pairs = Json5Parser::parse(Rule::string, r#""\u000a""#).unwrap();
68 | let s = parse_str(pairs.next().unwrap());
69 | assert_eq!(s, "\n")
70 | }
71 |
72 | fn parse_pair(pair: Pair) -> Value {
73 | match pair.as_rule() {
74 | Rule::array => Value::Array(pair.into_inner().map(parse_pair).collect()),
75 | Rule::null => Value::Null,
76 | Rule::string => Value::String(parse_str(pair)),
77 | Rule::number => Value::Number(pair.as_str().parse().unwrap()),
78 | Rule::boolean => Value::Boolean(pair.as_str().parse().unwrap()),
79 | Rule::object => {
80 | let pairs = pair.into_inner().map(|pair| {
81 | let mut inner_rule = pair.into_inner();
82 | let name = {
83 | let pair = inner_rule.next().unwrap();
84 | match pair.as_rule() {
85 | Rule::identifier => pair.as_str().to_string(),
86 | Rule::string => parse_str(pair),
87 | _ => unreachable!(),
88 | }
89 | };
90 | let value = parse_pair(inner_rule.next().unwrap());
91 | (name, value)
92 | });
93 | let mut m = HashMap::with_capacity(pairs.len());
94 | for (k, v) in pairs {
95 | m.insert(k, v);
96 | }
97 | Value::Object(m)
98 | }
99 | _ => unreachable!(),
100 | }
101 | }
102 |
103 | pub fn parse(lua: &Lua, data: String) -> Result {
104 | let data = match Json5Parser::parse(Rule::text, data.as_str()) {
105 | Ok(mut data) => data.next().unwrap(),
106 | Err(err) => return Err(ExternalError(Arc::new(err))),
107 | };
108 | parse_pair(data).into_lua(lua)
109 | }
110 |
--------------------------------------------------------------------------------