├── .gitignore ├── CMakeLists.txt ├── Dockerfile.alpine ├── Dockerfile.debian ├── LICENSE ├── README.md ├── clang-format.bash ├── ext ├── argparse │ └── argparse.hpp ├── catch2 │ └── catch.hpp └── termcolor │ └── termcolor.hpp ├── img ├── 01.png ├── 02.png ├── 03.png ├── 04.png └── logo.png ├── include └── jsonlint │ ├── errors.hpp │ ├── lexer.hpp │ ├── parser.hpp │ ├── string.hpp │ └── utf8.hpp ├── src ├── errors.cpp ├── lexer.cpp ├── main.cpp ├── parser.cpp └── string.cpp └── test ├── lexer ├── keywords.cpp ├── number.cpp ├── punctuation.cpp ├── string.cpp └── whitespace.cpp ├── main.cpp └── parser ├── array.cpp ├── number.cpp ├── object.cpp └── string.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | .vscode/ 28 | # Uncomment if you have tasks that create the project's static files in wwwroot 29 | #wwwroot/ 30 | 31 | # MSTest test Results 32 | [Tt]est[Rr]esult*/ 33 | [Bb]uild[Ll]og.* 34 | 35 | # NUNIT 36 | *.VisualState.xml 37 | TestResult.xml 38 | 39 | # Build Results of an ATL Project 40 | [Dd]ebugPS/ 41 | [Rr]eleasePS/ 42 | dlldata.c 43 | 44 | # DNX 45 | project.lock.json 46 | project.fragment.lock.json 47 | artifacts/ 48 | 49 | *_i.c 50 | *_p.c 51 | *_i.h 52 | *.ilk 53 | *.meta 54 | *.obj 55 | *.pch 56 | *.pdb 57 | *.pgc 58 | *.pgd 59 | *.rsp 60 | *.sbr 61 | *.tlb 62 | *.tli 63 | *.tlh 64 | *.tmp 65 | *.tmp_proj 66 | *.log 67 | *.vspscc 68 | *.vssscc 69 | .builds 70 | *.pidb 71 | *.svclog 72 | *.scc 73 | 74 | # Chutzpah Test files 75 | _Chutzpah* 76 | 77 | # Visual C++ cache files 78 | ipch/ 79 | *.aps 80 | *.ncb 81 | *.opendb 82 | *.opensdf 83 | *.sdf 84 | *.cachefile 85 | *.VC.db 86 | *.VC.VC.opendb 87 | 88 | # Visual Studio profiler 89 | *.psess 90 | *.vsp 91 | *.vspx 92 | *.sap 93 | 94 | # TFS 2012 Local Workspace 95 | $tf/ 96 | 97 | # Guidance Automation Toolkit 98 | *.gpState 99 | 100 | # ReSharper is a .NET coding add-in 101 | _ReSharper*/ 102 | *.[Rr]e[Ss]harper 103 | *.DotSettings.user 104 | 105 | # JustCode is a .NET coding add-in 106 | .JustCode 107 | 108 | # TeamCity is a build add-in 109 | _TeamCity* 110 | 111 | # DotCover is a Code Coverage Tool 112 | *.dotCover 113 | 114 | # NCrunch 115 | _NCrunch_* 116 | .*crunch*.local.xml 117 | nCrunchTemp_* 118 | 119 | # MightyMoose 120 | *.mm.* 121 | AutoTest.Net/ 122 | 123 | # Web workbench (sass) 124 | .sass-cache/ 125 | 126 | # Installshield output folder 127 | [Ee]xpress/ 128 | 129 | # DocProject is a documentation generator add-in 130 | DocProject/buildhelp/ 131 | DocProject/Help/*.HxT 132 | DocProject/Help/*.HxC 133 | DocProject/Help/*.hhc 134 | DocProject/Help/*.hhk 135 | DocProject/Help/*.hhp 136 | DocProject/Help/Html2 137 | DocProject/Help/html 138 | 139 | # Click-Once directory 140 | publish/ 141 | 142 | # Publish Web Output 143 | *.[Pp]ublish.xml 144 | *.azurePubxml 145 | # TODO: Comment the next line if you want to checkin your web deploy settings 146 | # but database connection strings (with potential passwords) will be unencrypted 147 | #*.pubxml 148 | *.publishproj 149 | 150 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 151 | # checkin your Azure Web App publish settings, but sensitive information contained 152 | # in these scripts will be unencrypted 153 | PublishScripts/ 154 | 155 | # NuGet Packages 156 | *.nupkg 157 | # The packages folder can be ignored because of Package Restore 158 | **/packages/* 159 | # except build/, which is used as an MSBuild target. 160 | !**/packages/build/ 161 | # Uncomment if necessary however generally it will be regenerated when needed 162 | #!**/packages/repositories.config 163 | # NuGet v3's project.json files produces more ignoreable files 164 | *.nuget.props 165 | *.nuget.targets 166 | 167 | # Microsoft Azure Build Output 168 | csx/ 169 | *.build.csdef 170 | 171 | # Microsoft Azure Emulator 172 | ecf/ 173 | rcf/ 174 | 175 | # Windows Store app package directories and files 176 | AppPackages/ 177 | BundleArtifacts/ 178 | Package.StoreAssociation.xml 179 | _pkginfo.txt 180 | 181 | # Visual Studio cache files 182 | # files ending in .cache can be ignored 183 | *.[Cc]ache 184 | # but keep track of directories ending in .cache 185 | !*.[Cc]ache/ 186 | 187 | # Others 188 | ClientBin/ 189 | ~$* 190 | *~ 191 | *.dbmdl 192 | *.dbproj.schemaview 193 | *.jfm 194 | *.pfx 195 | *.publishsettings 196 | node_modules/ 197 | orleans.codegen.cs 198 | 199 | # Since there are multiple workflows, uncomment next line to ignore bower_components 200 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 201 | #bower_components/ 202 | 203 | # RIA/Silverlight projects 204 | Generated_Code/ 205 | 206 | # Backup & report files from converting an old project file 207 | # to a newer Visual Studio version. Backup files are not needed, 208 | # because we have git ;-) 209 | _UpgradeReport_Files/ 210 | Backup*/ 211 | UpgradeLog*.XML 212 | UpgradeLog*.htm 213 | 214 | # SQL Server files 215 | *.mdf 216 | *.ldf 217 | 218 | # Business Intelligence projects 219 | *.rdl.data 220 | *.bim.layout 221 | *.bim_*.settings 222 | 223 | # Microsoft Fakes 224 | FakesAssemblies/ 225 | 226 | # GhostDoc plugin setting file 227 | *.GhostDoc.xml 228 | 229 | # Node.js Tools for Visual Studio 230 | .ntvs_analysis.dat 231 | 232 | # Visual Studio 6 build log 233 | *.plg 234 | 235 | # Visual Studio 6 workspace options file 236 | *.opt 237 | 238 | # Visual Studio LightSwitch build output 239 | **/*.HTMLClient/GeneratedArtifacts 240 | **/*.DesktopClient/GeneratedArtifacts 241 | **/*.DesktopClient/ModelManifest.xml 242 | **/*.Server/GeneratedArtifacts 243 | **/*.Server/ModelManifest.xml 244 | _Pvt_Extensions 245 | 246 | # Paket dependency manager 247 | .paket/paket.exe 248 | paket-files/ 249 | 250 | # FAKE - F# Make 251 | .fake/ 252 | 253 | # JetBrains Rider 254 | .idea/ 255 | *.sln.iml 256 | 257 | # CodeRush 258 | .cr/ 259 | 260 | # Python Tools for Visual Studio (PTVS) 261 | __pycache__/ 262 | *.pyc 263 | 264 | # CMake build directory 265 | build 266 | 267 | # Cppcheck build directory 268 | analysis-cppcheck-build-dir 269 | 270 | # Ideas directory 271 | ideas 272 | 273 | desktop.iniimages/ -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6) 2 | project(JSONLINT) 3 | 4 | if(NOT CMAKE_BUILD_TYPE) 5 | set(CMAKE_BUILD_TYPE Release) 6 | endif() 7 | 8 | # Disable deprecation for windows 9 | if (WIN32) 10 | add_compile_definitions(_CRT_SECURE_NO_WARNINGS) 11 | endif() 12 | 13 | # UTILS library 14 | file(GLOB UTILS_SOURCES 15 | "src/string.cpp" 16 | "src/errors.cpp" 17 | ) 18 | ADD_LIBRARY(UTILS STATIC ${UTILS_SOURCES}) 19 | INCLUDE_DIRECTORIES("include") 20 | set_target_properties(UTILS PROPERTIES OUTPUT_NAME utils) 21 | TARGET_LINK_LIBRARIES(UTILS) 22 | set_property(TARGET UTILS PROPERTY CXX_STANDARD 17) 23 | 24 | # LEXER library 25 | file(GLOB LEXER_SOURCES 26 | "src/lexer.cpp" 27 | ) 28 | ADD_LIBRARY(LEXER STATIC ${LEXER_SOURCES}) 29 | INCLUDE_DIRECTORIES("include") 30 | set_target_properties(LEXER PROPERTIES OUTPUT_NAME lexer) 31 | TARGET_LINK_LIBRARIES(LEXER UTILS) 32 | set_property(TARGET LEXER PROPERTY CXX_STANDARD 17) 33 | 34 | # PARSER library 35 | file(GLOB PARSER_SOURCES 36 | "src/parser.cpp" 37 | ) 38 | ADD_LIBRARY(PARSER STATIC ${PARSER_SOURCES}) 39 | INCLUDE_DIRECTORIES("include" "ext") 40 | set_target_properties(PARSER PROPERTIES OUTPUT_NAME parser) 41 | TARGET_LINK_LIBRARIES(PARSER LEXER) 42 | set_property(TARGET PARSER PROPERTY CXX_STANDARD 17) 43 | 44 | # JSONLINT CLI 45 | file(GLOB JSONLINT_SOURCES 46 | "src/main.cpp" 47 | ) 48 | ADD_EXECUTABLE(JSONLINT ${JSONLINT_SOURCES}) 49 | INCLUDE_DIRECTORIES("include" "ext") 50 | set_target_properties(JSONLINT PROPERTIES OUTPUT_NAME jsonlint) 51 | TARGET_LINK_LIBRARIES(JSONLINT PARSER) 52 | set_property(TARGET JSONLINT PROPERTY CXX_STANDARD 17) 53 | 54 | # Tests 55 | file(GLOB TEST_SOURCES 56 | "test/*.cpp" 57 | "test/lexer/*.cpp" 58 | "test/parser/*.cpp" 59 | ) 60 | ADD_EXECUTABLE(TESTS ${TEST_SOURCES}) 61 | INCLUDE_DIRECTORIES("ext") 62 | set_target_properties(TESTS PROPERTIES OUTPUT_NAME tests) 63 | TARGET_LINK_LIBRARIES(TESTS PARSER) 64 | set_property(TARGET TESTS PROPERTY CXX_STANDARD 17) 65 | 66 | # make check - runs unit tests 67 | if (UNIX) 68 | add_custom_target(check COMMAND ./tests) 69 | endif(UNIX) 70 | -------------------------------------------------------------------------------- /Dockerfile.alpine: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=alpine:latest 2 | 3 | FROM ${BASE_IMAGE} AS builder 4 | 5 | RUN apk update \ 6 | && apk add git cmake make binutils musl-dev g++ 7 | 8 | WORKDIR /build 9 | 10 | RUN git clone https://github.com/p-ranav/jsonlint.git 11 | 12 | WORKDIR /build/jsonlint/build 13 | 14 | RUN git rev-parse > git-revision 15 | 16 | RUN cmake .. \ 17 | && make \ 18 | && make check 19 | 20 | FROM ${BASE_IMAGE} 21 | 22 | RUN apk update \ 23 | && apk add libgcc libstdc++ 24 | 25 | WORKDIR /jsonlint 26 | 27 | COPY --from=builder /build/jsonlint/build/git-revision . 28 | COPY --from=builder /build/jsonlint/build/jsonlint . 29 | -------------------------------------------------------------------------------- /Dockerfile.debian: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=debian:sid-slim 2 | 3 | FROM ${BASE_IMAGE} AS builder 4 | 5 | RUN apt-get update \ 6 | && apt-get install -y --no-install-recommends ca-certificates git cmake make clang 7 | 8 | WORKDIR /build 9 | 10 | RUN git clone https://github.com/p-ranav/jsonlint.git 11 | 12 | WORKDIR /build/jsonlint/build 13 | 14 | RUN git rev-parse > git-revision 15 | 16 | RUN cmake .. \ 17 | && make \ 18 | && make check 19 | 20 | FROM ${BASE_IMAGE} 21 | 22 | WORKDIR /jsonlint 23 | 24 | COPY --from=builder /build/jsonlint/build/git-revision . 25 | COPY --from=builder /build/jsonlint/build/jsonlint . 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Pranav Srinivas Kumar 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 |

2 | jsonlint 3 |

4 | 5 | `jsonlint` is a lightweight command-line tool for validating JSON. 6 | 7 | ## Building `jsonlint` 8 | 9 | ```bash 10 | $ git clone https://github.com/p-ranav/jsonlint.git 11 | $ cd jsonlint 12 | $ mkdir build && cd build 13 | $ cmake .. && make && make check 14 | ``` 15 | 16 | ## Usage 17 | 18 | ```bash 19 | $ ./jsonlint --help 20 | Usage: jsonlint [options] file 21 | 22 | Positional arguments: 23 | file json file to validate 24 | 25 | Optional arguments: 26 | -h --help show this help message and exit 27 | ``` 28 | 29 | ## Example Scenarios 30 | 31 | ### Unterminated String 32 | 33 | unterminated string 34 | 35 | ### Trailing comma in array 36 | 37 | trailing comma 38 | 39 | ### Duplicate key in object 40 | 41 | duplicate key 42 | 43 | ### Typo in keyword 44 | 45 | typo in keyword 46 | -------------------------------------------------------------------------------- /clang-format.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | find ./include ./src ./test/ -type f \( -iname \*.cpp -o -iname \*.hpp \) | xargs clang-format -style="{ColumnLimit : 100}" -i 3 | -------------------------------------------------------------------------------- /ext/argparse/argparse.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | __ _ _ __ __ _ _ __ __ _ _ __ ___ ___ 3 | / _` | '__/ _` | '_ \ / _` | '__/ __|/ _ \ Argument Parser for Modern C++ 4 | | (_| | | | (_| | |_) | (_| | | \__ \ __/ http://github.com/p-ranav/argparse 5 | \__,_|_| \__, | .__/ \__,_|_| |___/\___| 6 | |___/|_| 7 | 8 | Licensed under the MIT License . 9 | SPDX-License-Identifier: MIT 10 | Copyright (c) 2019 Pranav Srinivas Kumar . 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining a copy 13 | of this software and associated documentation files (the "Software"), to deal 14 | in the Software without restriction, including without limitation the rights 15 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | copies of the Software, and to permit persons to whom the Software is 17 | furnished to do so, subject to the following conditions: 18 | 19 | The above copyright notice and this permission notice shall be included in all 20 | copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 28 | SOFTWARE. 29 | */ 30 | #pragma once 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | #include 44 | #include 45 | #include 46 | #include 47 | 48 | namespace argparse { 49 | 50 | namespace details { // namespace for helper methods 51 | 52 | template struct is_container_helper {}; 53 | 54 | template 55 | struct is_container : std::false_type {}; 56 | 57 | template <> struct is_container : std::false_type {}; 58 | 59 | template 60 | struct is_container< 61 | T, 62 | std::conditional_t().begin()), 65 | decltype(std::declval().end()), 66 | decltype(std::declval().size())>, 67 | void>> : public std::true_type {}; 68 | 69 | template 70 | static constexpr bool is_container_v = is_container::value; 71 | 72 | template 73 | using enable_if_container = std::enable_if_t, T>; 74 | 75 | template 76 | using enable_if_not_container = std::enable_if_t, T>; 77 | 78 | template 79 | constexpr decltype(auto) apply_plus_one_impl(F &&f, Tuple &&t, Extra &&x, 80 | std::index_sequence) { 81 | return std::invoke(std::forward(f), std::get(std::forward(t))..., 82 | std::forward(x)); 83 | } 84 | 85 | template 86 | constexpr decltype(auto) apply_plus_one(F &&f, Tuple &&t, Extra &&x) { 87 | return details::apply_plus_one_impl( 88 | std::forward(f), std::forward(t), std::forward(x), 89 | std::make_index_sequence< 90 | std::tuple_size_v>>{}); 91 | } 92 | 93 | } // namespace details 94 | 95 | class ArgumentParser; 96 | 97 | class Argument { 98 | friend class ArgumentParser; 99 | friend auto operator<<(std::ostream &, ArgumentParser const &) 100 | -> std::ostream &; 101 | 102 | public: 103 | Argument() = default; 104 | 105 | template 106 | explicit Argument(Args... args) 107 | : mNames({std::move(args)...}), mIsOptional((is_optional(args) || ...)) { 108 | std::sort( 109 | mNames.begin(), mNames.end(), [](const auto &lhs, const auto &rhs) { 110 | return lhs.size() == rhs.size() ? lhs < rhs : lhs.size() < rhs.size(); 111 | }); 112 | } 113 | 114 | Argument &help(std::string aHelp) { 115 | mHelp = std::move(aHelp); 116 | return *this; 117 | } 118 | 119 | Argument &default_value(std::any aDefaultValue) { 120 | mDefaultValue = std::move(aDefaultValue); 121 | return *this; 122 | } 123 | 124 | Argument &required() { 125 | mIsRequired = true; 126 | return *this; 127 | } 128 | 129 | Argument &implicit_value(std::any aImplicitValue) { 130 | mImplicitValue = std::move(aImplicitValue); 131 | mNumArgs = 0; 132 | return *this; 133 | } 134 | 135 | template 136 | auto action(F &&aAction, Args &&... aBound) 137 | -> std::enable_if_t, 138 | Argument &> { 139 | using action_type = std::conditional_t< 140 | std::is_void_v>, 141 | void_action, valued_action>; 142 | if constexpr (sizeof...(Args) == 0) 143 | mAction.emplace(std::forward(aAction)); 144 | else 145 | mAction.emplace( 146 | [f = std::forward(aAction), 147 | tup = std::make_tuple(std::forward(aBound)...)]( 148 | std::string const &opt) mutable { 149 | return details::apply_plus_one(f, tup, opt); 150 | }); 151 | return *this; 152 | } 153 | 154 | Argument &nargs(size_t aNumArgs) { 155 | mNumArgs = aNumArgs; 156 | return *this; 157 | } 158 | 159 | template 160 | Iterator consume(Iterator start, Iterator end, std::string usedName = {}) { 161 | if (mIsUsed) { 162 | throw std::runtime_error("Duplicate argument"); 163 | } 164 | mIsUsed = true; 165 | mUsedName = std::move(usedName); 166 | if (mNumArgs == 0) { 167 | mValues.emplace_back(mImplicitValue); 168 | return start; 169 | } else if (mNumArgs <= static_cast(std::distance(start, end))) { 170 | end = std::next(start, mNumArgs); 171 | if (std::any_of(start, end, Argument::is_optional)) { 172 | throw std::runtime_error("optional argument in parameter sequence"); 173 | } 174 | struct action_apply { 175 | void operator()(valued_action &f) { 176 | std::transform(start, end, std::back_inserter(self.mValues), f); 177 | } 178 | 179 | void operator()(void_action &f) { 180 | std::for_each(start, end, f); 181 | if (!self.mDefaultValue.has_value()) 182 | self.mValues.resize(self.mNumArgs); 183 | } 184 | 185 | Iterator start, end; 186 | Argument &self; 187 | }; 188 | std::visit(action_apply{start, end, *this}, mAction); 189 | return end; 190 | } else if (mDefaultValue.has_value()) { 191 | return start; 192 | } else { 193 | throw std::runtime_error("Too few arguments"); 194 | } 195 | } 196 | 197 | /* 198 | * @throws std::runtime_error if argument values are not valid 199 | */ 200 | void validate() const { 201 | if (mIsOptional) { 202 | if (mIsUsed && mValues.size() != mNumArgs && !mDefaultValue.has_value()) { 203 | std::stringstream stream; 204 | stream << mUsedName << ": expected " << mNumArgs 205 | << " argument(s). " << mValues.size() << " provided."; 206 | throw std::runtime_error(stream.str()); 207 | } else { 208 | // TODO: check if an implicit value was programmed for this argument 209 | if (!mIsUsed && !mDefaultValue.has_value() && mIsRequired) { 210 | std::stringstream stream; 211 | stream << mNames[0] << ": required."; 212 | throw std::runtime_error(stream.str()); 213 | } 214 | if (mIsUsed && mIsRequired && mValues.size() == 0) { 215 | std::stringstream stream; 216 | stream << mUsedName << ": no value provided."; 217 | throw std::runtime_error(stream.str()); 218 | } 219 | } 220 | } else { 221 | if (mValues.size() != mNumArgs && !mDefaultValue.has_value()) { 222 | std::stringstream stream; 223 | stream << mUsedName << ": expected " << mNumArgs 224 | << " argument(s). " << mValues.size() << " provided."; 225 | throw std::runtime_error(stream.str()); 226 | } 227 | } 228 | } 229 | 230 | size_t get_arguments_length() const { 231 | return std::accumulate(std::begin(mNames), std::end(mNames), size_t(0), 232 | [](const auto &sum, const auto &s) { 233 | return sum + s.size() + 234 | 1; // +1 for space between names 235 | }); 236 | } 237 | 238 | friend std::ostream &operator<<(std::ostream &stream, 239 | const Argument &argument) { 240 | std::stringstream nameStream; 241 | std::copy(std::begin(argument.mNames), std::end(argument.mNames), 242 | std::ostream_iterator(nameStream, " ")); 243 | stream << nameStream.str() << "\t" << argument.mHelp; 244 | if (argument.mIsRequired) 245 | stream << "[Required]"; 246 | stream << "\n"; 247 | return stream; 248 | } 249 | 250 | template bool operator!=(const T &aRhs) const { 251 | return !(*this == aRhs); 252 | } 253 | 254 | /* 255 | * Entry point for template non-container types 256 | * @throws std::logic_error in case of incompatible types 257 | */ 258 | template 259 | std::enable_if_t, bool> operator==(const T &aRhs) const { 260 | return get() == aRhs; 261 | } 262 | 263 | /* 264 | * Template specialization for containers 265 | * @throws std::logic_error in case of incompatible types 266 | */ 267 | template 268 | std::enable_if_t, bool> operator==(const T &aRhs) const { 269 | using ValueType = typename T::value_type; 270 | auto tLhs = get(); 271 | if (tLhs.size() != aRhs.size()) 272 | return false; 273 | else { 274 | return std::equal(std::begin(tLhs), std::end(tLhs), std::begin(aRhs), 275 | [](const auto &lhs, const auto &rhs) { 276 | return std::any_cast(lhs) == rhs; 277 | }); 278 | } 279 | } 280 | 281 | private: 282 | static bool is_integer(const std::string &aValue) { 283 | if (aValue.empty() || 284 | ((!isdigit(aValue[0])) && (aValue[0] != '-') && (aValue[0] != '+'))) 285 | return false; 286 | char *tPtr; 287 | strtol(aValue.c_str(), &tPtr, 10); 288 | return (*tPtr == 0); 289 | } 290 | 291 | static bool is_float(const std::string &aValue) { 292 | std::istringstream tStream(aValue); 293 | float tFloat; 294 | // noskipws considers leading whitespace invalid 295 | tStream >> std::noskipws >> tFloat; 296 | // Check the entire string was consumed 297 | // and if either failbit or badbit is set 298 | return tStream.eof() && !tStream.fail(); 299 | } 300 | 301 | // If an argument starts with "-" or "--", then it's optional 302 | static bool is_optional(const std::string &aName) { 303 | return (!aName.empty() && aName[0] == '-' && !is_integer(aName) && 304 | !is_float(aName)); 305 | } 306 | 307 | static bool is_positional(const std::string &aName) { 308 | return !is_optional(aName); 309 | } 310 | 311 | /* 312 | * Getter for template non-container types 313 | * @throws std::logic_error in case of incompatible types 314 | */ 315 | template details::enable_if_not_container get() const { 316 | if (!mValues.empty()) { 317 | return std::any_cast(mValues.front()); 318 | } 319 | if (mDefaultValue.has_value()) { 320 | return std::any_cast(mDefaultValue); 321 | } 322 | throw std::logic_error("No value provided"); 323 | } 324 | 325 | /* 326 | * Getter for container types 327 | * @throws std::logic_error in case of incompatible types 328 | */ 329 | template details::enable_if_container get() const { 330 | using ValueType = typename CONTAINER::value_type; 331 | CONTAINER tResult; 332 | if (!mValues.empty()) { 333 | std::transform( 334 | std::begin(mValues), std::end(mValues), std::back_inserter(tResult), 335 | [](const auto &value) { return std::any_cast(value); }); 336 | return tResult; 337 | } 338 | if (mDefaultValue.has_value()) { 339 | const auto &tDefaultValues = 340 | std::any_cast(mDefaultValue); 341 | std::transform(std::begin(tDefaultValues), std::end(tDefaultValues), 342 | std::back_inserter(tResult), [](const auto &value) { 343 | return std::any_cast(value); 344 | }); 345 | return tResult; 346 | } 347 | throw std::logic_error("No value provided"); 348 | } 349 | 350 | std::vector mNames; 351 | std::string mUsedName; 352 | std::string mHelp; 353 | std::any mDefaultValue; 354 | std::any mImplicitValue; 355 | using valued_action = std::function; 356 | using void_action = std::function; 357 | std::variant mAction{ 358 | std::in_place_type, 359 | [](const std::string &aValue) { return aValue; }}; 360 | std::vector mValues; 361 | std::vector mRawValues; 362 | size_t mNumArgs = 1; 363 | bool mIsOptional = false; 364 | bool mIsRequired = false; 365 | bool mIsUsed = false; // relevant for optional arguments. True if used by user 366 | 367 | public: 368 | static constexpr auto mHelpOption = "-h"; 369 | static constexpr auto mHelpOptionLong = "--help"; 370 | }; 371 | 372 | class ArgumentParser { 373 | public: 374 | explicit ArgumentParser(std::string aProgramName = {}) 375 | : mProgramName(std::move(aProgramName)) { 376 | add_argument(Argument::mHelpOption, Argument::mHelpOptionLong) 377 | .help("show this help message and exit") 378 | .nargs(0) 379 | .default_value(false) 380 | .implicit_value(true); 381 | } 382 | 383 | // Parameter packing 384 | // Call add_argument with variadic number of string arguments 385 | template Argument &add_argument(Targs... Fargs) { 386 | std::shared_ptr tArgument = 387 | std::make_shared(std::move(Fargs)...); 388 | 389 | if (tArgument->mIsOptional) 390 | mOptionalArguments.emplace_back(tArgument); 391 | else 392 | mPositionalArguments.emplace_back(tArgument); 393 | 394 | for (const auto &mName : tArgument->mNames) { 395 | mArgumentMap.insert_or_assign(mName, tArgument); 396 | } 397 | return *tArgument; 398 | } 399 | 400 | // Parameter packed add_parents method 401 | // Accepts a variadic number of ArgumentParser objects 402 | template void add_parents(Targs... Fargs) { 403 | const auto tNewParentParsers = {Fargs...}; 404 | for (const auto &tParentParser : tNewParentParsers) { 405 | const auto &tPositionalArguments = tParentParser.mPositionalArguments; 406 | std::copy(std::begin(tPositionalArguments), 407 | std::end(tPositionalArguments), 408 | std::back_inserter(mPositionalArguments)); 409 | 410 | const auto &tOptionalArguments = tParentParser.mOptionalArguments; 411 | std::copy(std::begin(tOptionalArguments), std::end(tOptionalArguments), 412 | std::back_inserter(mOptionalArguments)); 413 | 414 | const auto &tArgumentMap = tParentParser.mArgumentMap; 415 | for (const auto &[tKey, tValue] : tArgumentMap) { 416 | mArgumentMap.insert_or_assign(tKey, tValue); 417 | } 418 | } 419 | std::move(std::begin(tNewParentParsers), std::end(tNewParentParsers), 420 | std::back_inserter(mParentParsers)); 421 | } 422 | 423 | /* Call parse_args_internal - which does all the work 424 | * Then, validate the parsed arguments 425 | * This variant is used mainly for testing 426 | * @throws std::runtime_error in case of any invalid argument 427 | */ 428 | void parse_args(const std::vector &aArguments) { 429 | parse_args_internal(aArguments); 430 | parse_args_validate(); 431 | } 432 | 433 | /* Main entry point for parsing command-line arguments using this 434 | * ArgumentParser 435 | * @throws std::runtime_error in case of any invalid argument 436 | */ 437 | void parse_args(int argc, const char *const argv[]) { 438 | std::vector arguments; 439 | std::copy(argv, argv + argc, std::back_inserter(arguments)); 440 | parse_args(arguments); 441 | } 442 | 443 | /* Getter enabled for all template types other than std::vector and std::list 444 | * @throws std::logic_error in case of an invalid argument name 445 | * @throws std::logic_error in case of incompatible types 446 | */ 447 | template T get(const std::string &aArgumentName) { 448 | auto tIterator = mArgumentMap.find(aArgumentName); 449 | if (tIterator != mArgumentMap.end()) { 450 | return tIterator->second->get(); 451 | } 452 | throw std::logic_error("No such argument"); 453 | } 454 | 455 | /* Indexing operator. Return a reference to an Argument object 456 | * Used in conjuction with Argument.operator== e.g., parser["foo"] == true 457 | * @throws std::logic_error in case of an invalid argument name 458 | */ 459 | Argument &operator[](const std::string &aArgumentName) { 460 | auto tIterator = mArgumentMap.find(aArgumentName); 461 | if (tIterator != mArgumentMap.end()) { 462 | return *(tIterator->second); 463 | } 464 | throw std::logic_error("No such argument"); 465 | } 466 | 467 | // Print help message 468 | friend auto operator<<(std::ostream &stream, const ArgumentParser &parser) 469 | -> std::ostream & { 470 | if (auto sen = std::ostream::sentry(stream)) { 471 | stream.setf(std::ios_base::left); 472 | stream << "Usage: " << parser.mProgramName << " [options] "; 473 | size_t tLongestArgumentLength = parser.get_length_of_longest_argument(); 474 | 475 | for (const auto &argument : parser.mPositionalArguments) { 476 | stream << argument->mNames.front() << " "; 477 | } 478 | stream << "\n\n"; 479 | 480 | if (!parser.mPositionalArguments.empty()) 481 | stream << "Positional arguments:\n"; 482 | 483 | for (const auto &mPositionalArgument : parser.mPositionalArguments) { 484 | stream.width(tLongestArgumentLength); 485 | stream << *mPositionalArgument; 486 | } 487 | 488 | if (!parser.mOptionalArguments.empty()) 489 | stream << (parser.mPositionalArguments.empty() ? "" : "\n") 490 | << "Optional arguments:\n"; 491 | 492 | for (const auto &mOptionalArgument : parser.mOptionalArguments) { 493 | stream.width(tLongestArgumentLength); 494 | stream << *mOptionalArgument; 495 | } 496 | } 497 | 498 | return stream; 499 | } 500 | 501 | // Format help message 502 | auto help() const -> std::ostringstream { 503 | std::ostringstream out; 504 | out << *this; 505 | return out; 506 | } 507 | 508 | // Printing the one and only help message 509 | // I've stuck with a simple message format, nothing fancy. 510 | [[deprecated("Use cout << program; instead. See also help().")]] std::string 511 | print_help() { 512 | auto out = help(); 513 | std::cout << out.rdbuf(); 514 | return out.str(); 515 | } 516 | 517 | private: 518 | /* 519 | * @throws std::runtime_error in case of any invalid argument 520 | */ 521 | void parse_args_internal(const std::vector &aArguments) { 522 | if (mProgramName.empty() && !aArguments.empty()) { 523 | mProgramName = aArguments.front(); 524 | } 525 | auto end = std::end(aArguments); 526 | auto positionalArgumentIt = std::begin(mPositionalArguments); 527 | for (auto it = std::next(std::begin(aArguments)); it != end;) { 528 | const auto &tCurrentArgument = *it; 529 | if (tCurrentArgument == Argument::mHelpOption || 530 | tCurrentArgument == Argument::mHelpOptionLong) { 531 | throw std::runtime_error("help called"); 532 | } 533 | if (Argument::is_positional(tCurrentArgument)) { 534 | if (positionalArgumentIt == std::end(mPositionalArguments)) { 535 | throw std::runtime_error( 536 | "Maximum number of positional arguments exceeded"); 537 | } 538 | auto tArgument = *(positionalArgumentIt++); 539 | it = tArgument->consume(it, end); 540 | } else if (auto tIterator = mArgumentMap.find(tCurrentArgument); 541 | tIterator != mArgumentMap.end()) { 542 | auto tArgument = tIterator->second; 543 | it = tArgument->consume(std::next(it), end, tCurrentArgument); 544 | } else if (const auto &tCompoundArgument = tCurrentArgument; 545 | tCompoundArgument.size() > 1 && tCompoundArgument[0] == '-' && 546 | tCompoundArgument[1] != '-') { 547 | ++it; 548 | for (size_t j = 1; j < tCompoundArgument.size(); j++) { 549 | auto tCurrentArgument = std::string{'-', tCompoundArgument[j]}; 550 | if (auto tIterator = mArgumentMap.find(tCurrentArgument); 551 | tIterator != mArgumentMap.end()) { 552 | auto tArgument = tIterator->second; 553 | it = tArgument->consume(it, end, tCurrentArgument); 554 | } else { 555 | throw std::runtime_error("Unknown argument"); 556 | } 557 | } 558 | } else { 559 | throw std::runtime_error("Unknown argument"); 560 | } 561 | } 562 | } 563 | 564 | /* 565 | * @throws std::runtime_error in case of any invalid argument 566 | */ 567 | void parse_args_validate() { 568 | // Check if all arguments are parsed 569 | std::for_each(std::begin(mArgumentMap), std::end(mArgumentMap), 570 | [](const auto &argPair) { 571 | const auto &tArgument = argPair.second; 572 | tArgument->validate(); 573 | }); 574 | } 575 | 576 | // Used by print_help. 577 | size_t get_length_of_longest_argument() const { 578 | if (mArgumentMap.empty()) 579 | return 0; 580 | std::vector argumentLengths(mArgumentMap.size()); 581 | std::transform(std::begin(mArgumentMap), std::end(mArgumentMap), 582 | std::begin(argumentLengths), [](const auto &argPair) { 583 | const auto &tArgument = argPair.second; 584 | return tArgument->get_arguments_length(); 585 | }); 586 | return *std::max_element(std::begin(argumentLengths), 587 | std::end(argumentLengths)); 588 | } 589 | 590 | std::string mProgramName; 591 | std::vector mParentParsers; 592 | std::vector> mPositionalArguments; 593 | std::vector> mOptionalArguments; 594 | std::map> mArgumentMap; 595 | }; 596 | 597 | } // namespace argparse 598 | -------------------------------------------------------------------------------- /ext/termcolor/termcolor.hpp: -------------------------------------------------------------------------------- 1 | //! 2 | //! termcolor 3 | //! ~~~~~~~~~ 4 | //! 5 | //! termcolor is a header-only c++ library for printing colored messages 6 | //! to the terminal. Written just for fun with a help of the Force. 7 | //! 8 | //! :copyright: (c) 2013 by Ihor Kalnytskyi 9 | //! :license: BSD, see LICENSE for details 10 | //! 11 | 12 | #ifndef TERMCOLOR_HPP_ 13 | #define TERMCOLOR_HPP_ 14 | 15 | // the following snippet of code detects the current OS and 16 | // defines the appropriate macro that is used to wrap some 17 | // platform specific things 18 | #if defined(_WIN32) || defined(_WIN64) 19 | # define TERMCOLOR_OS_WINDOWS 20 | #elif defined(__APPLE__) 21 | # define TERMCOLOR_OS_MACOS 22 | #elif defined(__unix__) || defined(__unix) 23 | # define TERMCOLOR_OS_LINUX 24 | #else 25 | # error unsupported platform 26 | #endif 27 | 28 | 29 | // This headers provides the `isatty()`/`fileno()` functions, 30 | // which are used for testing whether a standart stream refers 31 | // to the terminal. As for Windows, we also need WinApi funcs 32 | // for changing colors attributes of the terminal. 33 | #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) 34 | # include 35 | #elif defined(TERMCOLOR_OS_WINDOWS) 36 | # include 37 | # include 38 | #endif 39 | 40 | 41 | #include 42 | #include 43 | 44 | 45 | 46 | namespace termcolor 47 | { 48 | // Forward declaration of the `_internal` namespace. 49 | // All comments are below. 50 | namespace _internal 51 | { 52 | // An index to be used to access a private storage of I/O streams. See 53 | // colorize / nocolorize I/O manipulators for details. 54 | static int colorize_index = std::ios_base::xalloc(); 55 | 56 | inline FILE* get_standard_stream(const std::ostream& stream); 57 | inline bool is_colorized(std::ostream& stream); 58 | inline bool is_atty(const std::ostream& stream); 59 | 60 | #if defined(TERMCOLOR_OS_WINDOWS) 61 | inline void win_change_attributes(std::ostream& stream, int foreground, int background=-1); 62 | #endif 63 | } 64 | 65 | inline 66 | std::ostream& colorize(std::ostream& stream) 67 | { 68 | stream.iword(_internal::colorize_index) = 1L; 69 | return stream; 70 | } 71 | 72 | inline 73 | std::ostream& nocolorize(std::ostream& stream) 74 | { 75 | stream.iword(_internal::colorize_index) = 0L; 76 | return stream; 77 | } 78 | 79 | inline 80 | std::ostream& reset(std::ostream& stream) 81 | { 82 | if (_internal::is_colorized(stream)) 83 | { 84 | #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) 85 | stream << "\033[00m"; 86 | #elif defined(TERMCOLOR_OS_WINDOWS) 87 | _internal::win_change_attributes(stream, -1, -1); 88 | #endif 89 | } 90 | return stream; 91 | } 92 | 93 | 94 | inline 95 | std::ostream& bold(std::ostream& stream) 96 | { 97 | if (_internal::is_colorized(stream)) 98 | { 99 | #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) 100 | stream << "\033[1m"; 101 | #elif defined(TERMCOLOR_OS_WINDOWS) 102 | #endif 103 | } 104 | return stream; 105 | } 106 | 107 | 108 | inline 109 | std::ostream& dark(std::ostream& stream) 110 | { 111 | if (_internal::is_colorized(stream)) 112 | { 113 | #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) 114 | stream << "\033[2m"; 115 | #elif defined(TERMCOLOR_OS_WINDOWS) 116 | #endif 117 | } 118 | return stream; 119 | } 120 | 121 | 122 | inline 123 | std::ostream& italic(std::ostream& stream) 124 | { 125 | if (_internal::is_colorized(stream)) 126 | { 127 | #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) 128 | stream << "\033[3m"; 129 | #elif defined(TERMCOLOR_OS_WINDOWS) 130 | #endif 131 | } 132 | return stream; 133 | } 134 | 135 | 136 | inline 137 | std::ostream& underline(std::ostream& stream) 138 | { 139 | if (_internal::is_colorized(stream)) 140 | { 141 | #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) 142 | stream << "\033[4m"; 143 | #elif defined(TERMCOLOR_OS_WINDOWS) 144 | #endif 145 | } 146 | return stream; 147 | } 148 | 149 | 150 | inline 151 | std::ostream& blink(std::ostream& stream) 152 | { 153 | if (_internal::is_colorized(stream)) 154 | { 155 | #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) 156 | stream << "\033[5m"; 157 | #elif defined(TERMCOLOR_OS_WINDOWS) 158 | #endif 159 | } 160 | return stream; 161 | } 162 | 163 | 164 | inline 165 | std::ostream& reverse(std::ostream& stream) 166 | { 167 | if (_internal::is_colorized(stream)) 168 | { 169 | #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) 170 | stream << "\033[7m"; 171 | #elif defined(TERMCOLOR_OS_WINDOWS) 172 | #endif 173 | } 174 | return stream; 175 | } 176 | 177 | 178 | inline 179 | std::ostream& concealed(std::ostream& stream) 180 | { 181 | if (_internal::is_colorized(stream)) 182 | { 183 | #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) 184 | stream << "\033[8m"; 185 | #elif defined(TERMCOLOR_OS_WINDOWS) 186 | #endif 187 | } 188 | return stream; 189 | } 190 | 191 | 192 | inline 193 | std::ostream& crossed(std::ostream& stream) 194 | { 195 | if (_internal::is_colorized(stream)) 196 | { 197 | #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) 198 | stream << "\033[9m"; 199 | #elif defined(TERMCOLOR_OS_WINDOWS) 200 | #endif 201 | } 202 | return stream; 203 | } 204 | 205 | 206 | inline 207 | std::ostream& grey(std::ostream& stream) 208 | { 209 | if (_internal::is_colorized(stream)) 210 | { 211 | #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) 212 | stream << "\033[30m"; 213 | #elif defined(TERMCOLOR_OS_WINDOWS) 214 | _internal::win_change_attributes(stream, 215 | 0 // grey (black) 216 | ); 217 | #endif 218 | } 219 | return stream; 220 | } 221 | 222 | inline 223 | std::ostream& red(std::ostream& stream) 224 | { 225 | if (_internal::is_colorized(stream)) 226 | { 227 | #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) 228 | stream << "\033[31m"; 229 | #elif defined(TERMCOLOR_OS_WINDOWS) 230 | _internal::win_change_attributes(stream, 231 | FOREGROUND_RED 232 | ); 233 | #endif 234 | } 235 | return stream; 236 | } 237 | 238 | inline 239 | std::ostream& green(std::ostream& stream) 240 | { 241 | if (_internal::is_colorized(stream)) 242 | { 243 | #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) 244 | stream << "\033[32m"; 245 | #elif defined(TERMCOLOR_OS_WINDOWS) 246 | _internal::win_change_attributes(stream, 247 | FOREGROUND_GREEN 248 | ); 249 | #endif 250 | } 251 | return stream; 252 | } 253 | 254 | inline 255 | std::ostream& yellow(std::ostream& stream) 256 | { 257 | if (_internal::is_colorized(stream)) 258 | { 259 | #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) 260 | stream << "\033[33m"; 261 | #elif defined(TERMCOLOR_OS_WINDOWS) 262 | _internal::win_change_attributes(stream, 263 | FOREGROUND_GREEN | FOREGROUND_RED 264 | ); 265 | #endif 266 | } 267 | return stream; 268 | } 269 | 270 | inline 271 | std::ostream& blue(std::ostream& stream) 272 | { 273 | if (_internal::is_colorized(stream)) 274 | { 275 | #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) 276 | stream << "\033[34m"; 277 | #elif defined(TERMCOLOR_OS_WINDOWS) 278 | _internal::win_change_attributes(stream, 279 | FOREGROUND_BLUE 280 | ); 281 | #endif 282 | } 283 | return stream; 284 | } 285 | 286 | inline 287 | std::ostream& magenta(std::ostream& stream) 288 | { 289 | if (_internal::is_colorized(stream)) 290 | { 291 | #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) 292 | stream << "\033[35m"; 293 | #elif defined(TERMCOLOR_OS_WINDOWS) 294 | _internal::win_change_attributes(stream, 295 | FOREGROUND_BLUE | FOREGROUND_RED 296 | ); 297 | #endif 298 | } 299 | return stream; 300 | } 301 | 302 | inline 303 | std::ostream& cyan(std::ostream& stream) 304 | { 305 | if (_internal::is_colorized(stream)) 306 | { 307 | #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) 308 | stream << "\033[36m"; 309 | #elif defined(TERMCOLOR_OS_WINDOWS) 310 | _internal::win_change_attributes(stream, 311 | FOREGROUND_BLUE | FOREGROUND_GREEN 312 | ); 313 | #endif 314 | } 315 | return stream; 316 | } 317 | 318 | inline 319 | std::ostream& white(std::ostream& stream) 320 | { 321 | if (_internal::is_colorized(stream)) 322 | { 323 | #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) 324 | stream << "\033[37m"; 325 | #elif defined(TERMCOLOR_OS_WINDOWS) 326 | _internal::win_change_attributes(stream, 327 | FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED 328 | ); 329 | #endif 330 | } 331 | return stream; 332 | } 333 | 334 | 335 | 336 | inline 337 | std::ostream& on_grey(std::ostream& stream) 338 | { 339 | if (_internal::is_colorized(stream)) 340 | { 341 | #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) 342 | stream << "\033[40m"; 343 | #elif defined(TERMCOLOR_OS_WINDOWS) 344 | _internal::win_change_attributes(stream, -1, 345 | 0 // grey (black) 346 | ); 347 | #endif 348 | } 349 | return stream; 350 | } 351 | 352 | inline 353 | std::ostream& on_red(std::ostream& stream) 354 | { 355 | if (_internal::is_colorized(stream)) 356 | { 357 | #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) 358 | stream << "\033[41m"; 359 | #elif defined(TERMCOLOR_OS_WINDOWS) 360 | _internal::win_change_attributes(stream, -1, 361 | BACKGROUND_RED 362 | ); 363 | #endif 364 | } 365 | return stream; 366 | } 367 | 368 | inline 369 | std::ostream& on_green(std::ostream& stream) 370 | { 371 | if (_internal::is_colorized(stream)) 372 | { 373 | #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) 374 | stream << "\033[42m"; 375 | #elif defined(TERMCOLOR_OS_WINDOWS) 376 | _internal::win_change_attributes(stream, -1, 377 | BACKGROUND_GREEN 378 | ); 379 | #endif 380 | } 381 | return stream; 382 | } 383 | 384 | inline 385 | std::ostream& on_yellow(std::ostream& stream) 386 | { 387 | if (_internal::is_colorized(stream)) 388 | { 389 | #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) 390 | stream << "\033[43m"; 391 | #elif defined(TERMCOLOR_OS_WINDOWS) 392 | _internal::win_change_attributes(stream, -1, 393 | BACKGROUND_GREEN | BACKGROUND_RED 394 | ); 395 | #endif 396 | } 397 | return stream; 398 | } 399 | 400 | inline 401 | std::ostream& on_blue(std::ostream& stream) 402 | { 403 | if (_internal::is_colorized(stream)) 404 | { 405 | #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) 406 | stream << "\033[44m"; 407 | #elif defined(TERMCOLOR_OS_WINDOWS) 408 | _internal::win_change_attributes(stream, -1, 409 | BACKGROUND_BLUE 410 | ); 411 | #endif 412 | } 413 | return stream; 414 | } 415 | 416 | inline 417 | std::ostream& on_magenta(std::ostream& stream) 418 | { 419 | if (_internal::is_colorized(stream)) 420 | { 421 | #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) 422 | stream << "\033[45m"; 423 | #elif defined(TERMCOLOR_OS_WINDOWS) 424 | _internal::win_change_attributes(stream, -1, 425 | BACKGROUND_BLUE | BACKGROUND_RED 426 | ); 427 | #endif 428 | } 429 | return stream; 430 | } 431 | 432 | inline 433 | std::ostream& on_cyan(std::ostream& stream) 434 | { 435 | if (_internal::is_colorized(stream)) 436 | { 437 | #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) 438 | stream << "\033[46m"; 439 | #elif defined(TERMCOLOR_OS_WINDOWS) 440 | _internal::win_change_attributes(stream, -1, 441 | BACKGROUND_GREEN | BACKGROUND_BLUE 442 | ); 443 | #endif 444 | } 445 | return stream; 446 | } 447 | 448 | inline 449 | std::ostream& on_white(std::ostream& stream) 450 | { 451 | if (_internal::is_colorized(stream)) 452 | { 453 | #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) 454 | stream << "\033[47m"; 455 | #elif defined(TERMCOLOR_OS_WINDOWS) 456 | _internal::win_change_attributes(stream, -1, 457 | BACKGROUND_GREEN | BACKGROUND_BLUE | BACKGROUND_RED 458 | ); 459 | #endif 460 | } 461 | 462 | return stream; 463 | } 464 | 465 | 466 | 467 | //! Since C++ hasn't a way to hide something in the header from 468 | //! the outer access, I have to introduce this namespace which 469 | //! is used for internal purpose and should't be access from 470 | //! the user code. 471 | namespace _internal 472 | { 473 | //! Since C++ hasn't a true way to extract stream handler 474 | //! from the a given `std::ostream` object, I have to write 475 | //! this kind of hack. 476 | inline 477 | FILE* get_standard_stream(const std::ostream& stream) 478 | { 479 | if (&stream == &std::cout) 480 | return stdout; 481 | else if ((&stream == &std::cerr) || (&stream == &std::clog)) 482 | return stderr; 483 | 484 | return 0; 485 | } 486 | 487 | // Say whether a given stream should be colorized or not. It's always 488 | // true for ATTY streams and may be true for streams marked with 489 | // colorize flag. 490 | inline 491 | bool is_colorized(std::ostream& stream) 492 | { 493 | return is_atty(stream) || static_cast(stream.iword(colorize_index)); 494 | } 495 | 496 | //! Test whether a given `std::ostream` object refers to 497 | //! a terminal. 498 | inline 499 | bool is_atty(const std::ostream& stream) 500 | { 501 | FILE* std_stream = get_standard_stream(stream); 502 | 503 | // Unfortunately, fileno() ends with segmentation fault 504 | // if invalid file descriptor is passed. So we need to 505 | // handle this case gracefully and assume it's not a tty 506 | // if standard stream is not detected, and 0 is returned. 507 | if (!std_stream) 508 | return false; 509 | 510 | #if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX) 511 | return ::isatty(fileno(std_stream)); 512 | #elif defined(TERMCOLOR_OS_WINDOWS) 513 | return ::_isatty(_fileno(std_stream)); 514 | #endif 515 | } 516 | 517 | #if defined(TERMCOLOR_OS_WINDOWS) 518 | //! Change Windows Terminal colors attribute. If some 519 | //! parameter is `-1` then attribute won't changed. 520 | inline void win_change_attributes(std::ostream& stream, int foreground, int background) 521 | { 522 | // yeah, i know.. it's ugly, it's windows. 523 | static WORD defaultAttributes = 0; 524 | 525 | // Windows doesn't have ANSI escape sequences and so we use special 526 | // API to change Terminal output color. That means we can't 527 | // manipulate colors by means of "std::stringstream" and hence 528 | // should do nothing in this case. 529 | if (!_internal::is_atty(stream)) 530 | return; 531 | 532 | // get terminal handle 533 | HANDLE hTerminal = INVALID_HANDLE_VALUE; 534 | if (&stream == &std::cout) 535 | hTerminal = GetStdHandle(STD_OUTPUT_HANDLE); 536 | else if (&stream == &std::cerr) 537 | hTerminal = GetStdHandle(STD_ERROR_HANDLE); 538 | 539 | // save default terminal attributes if it unsaved 540 | if (!defaultAttributes) 541 | { 542 | CONSOLE_SCREEN_BUFFER_INFO info; 543 | if (!GetConsoleScreenBufferInfo(hTerminal, &info)) 544 | return; 545 | defaultAttributes = info.wAttributes; 546 | } 547 | 548 | // restore all default settings 549 | if (foreground == -1 && background == -1) 550 | { 551 | SetConsoleTextAttribute(hTerminal, defaultAttributes); 552 | return; 553 | } 554 | 555 | // get current settings 556 | CONSOLE_SCREEN_BUFFER_INFO info; 557 | if (!GetConsoleScreenBufferInfo(hTerminal, &info)) 558 | return; 559 | 560 | if (foreground != -1) 561 | { 562 | info.wAttributes &= ~(info.wAttributes & 0x0F); 563 | info.wAttributes |= static_cast(foreground); 564 | } 565 | 566 | if (background != -1) 567 | { 568 | info.wAttributes &= ~(info.wAttributes & 0xF0); 569 | info.wAttributes |= static_cast(background); 570 | } 571 | 572 | SetConsoleTextAttribute(hTerminal, info.wAttributes); 573 | } 574 | #endif // TERMCOLOR_OS_WINDOWS 575 | 576 | } // namespace _internal 577 | 578 | } // namespace termcolor 579 | 580 | 581 | #undef TERMCOLOR_OS_WINDOWS 582 | #undef TERMCOLOR_OS_MACOS 583 | #undef TERMCOLOR_OS_LINUX 584 | 585 | #endif // TERMCOLOR_HPP_ 586 | -------------------------------------------------------------------------------- /img/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p-ranav/jsonlint/2d1428274f05fe3d443cf8642c39d65b6b7025ac/img/01.png -------------------------------------------------------------------------------- /img/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p-ranav/jsonlint/2d1428274f05fe3d443cf8642c39d65b6b7025ac/img/02.png -------------------------------------------------------------------------------- /img/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p-ranav/jsonlint/2d1428274f05fe3d443cf8642c39d65b6b7025ac/img/03.png -------------------------------------------------------------------------------- /img/04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p-ranav/jsonlint/2d1428274f05fe3d443cf8642c39d65b6b7025ac/img/04.png -------------------------------------------------------------------------------- /img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p-ranav/jsonlint/2d1428274f05fe3d443cf8642c39d65b6b7025ac/img/logo.png -------------------------------------------------------------------------------- /include/jsonlint/errors.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace jsonlint { 9 | 10 | namespace details { 11 | 12 | unsigned int GetNumberOfDigits(unsigned int number); 13 | 14 | template 15 | void ReportError(Stage &context, Token start, Token end, const std::string &brief_description, 16 | const std::string &detailed_description); 17 | 18 | void ReportLexerError(Lexer &context, Token start, Token end, const std::string &brief_description, 19 | const std::string &detailed_description); 20 | 21 | void ReportParserError(Parser &context, Token start, Token end, 22 | const std::string &brief_description, 23 | const std::string &detailed_description); 24 | 25 | } // namespace details 26 | 27 | } // namespace jsonlint 28 | -------------------------------------------------------------------------------- /include/jsonlint/lexer.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | namespace jsonlint { 6 | 7 | enum class TokenType { 8 | STRING, 9 | NUMBER, 10 | TRUE, 11 | FALSE, 12 | NULL_, 13 | COLON, 14 | COMMA, 15 | PLUS, 16 | MINUS, 17 | LEFT_BRACKET, 18 | RIGHT_BRACKET, 19 | LEFT_BRACE, 20 | RIGHT_BRACE, 21 | ILLEGAL, 22 | EOF_ 23 | }; 24 | 25 | struct Token { 26 | TokenType type; 27 | std::string literal; 28 | std::string filename; 29 | int line; 30 | int cursor_start; 31 | int cursor_end; 32 | }; 33 | 34 | struct Lexer { 35 | std::string source; 36 | int index; 37 | std::string filename; 38 | int line; 39 | int cursor; 40 | std::vector> errors; 41 | bool silent_mode; 42 | }; 43 | 44 | std::vector Tokenize(Lexer &context); 45 | 46 | } // namespace jsonlint 47 | -------------------------------------------------------------------------------- /include/jsonlint/parser.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace jsonlint { 9 | 10 | enum class Element { STRING, NUMBER, TRUE, FALSE, NULL_, ARRAY, OBJECT }; 11 | 12 | struct Parser { 13 | std::vector tokens; 14 | std::string source; 15 | unsigned int current_index; 16 | Token current; 17 | Token peek; 18 | std::map> visitors; 19 | std::vector> errors; 20 | bool silent_mode; 21 | explicit Parser(const std::vector &tokens, const std::string &source); 22 | bool IsCurrentToken(TokenType value); 23 | bool IsPeekToken(TokenType value); 24 | void PreviousToken(); 25 | void NextToken(); 26 | bool ExpectPeek(TokenType value); 27 | bool ParseJson(); 28 | void RegisterVisitor(TokenType, std::function); 29 | }; 30 | 31 | } // namespace jsonlint 32 | -------------------------------------------------------------------------------- /include/jsonlint/string.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | namespace jsonlint { 16 | 17 | namespace string { 18 | 19 | size_t Copy(char *dst, const char *src, size_t destination_size); 20 | 21 | std::string Format(std::string format_string, ...); 22 | std::string Slice(const std::string &input_string, int start_index, int end_index = INT_MAX, 23 | int step = 1); 24 | 25 | // split std::string based on a delimiter string 26 | // supports multi-character delimiter 27 | // returns a vector of substrings after split 28 | std::vector Split(const std::string &input_string, const std::string &delimiter, 29 | std::shared_ptr> result = 30 | std::make_shared>()); 31 | 32 | // join a vector of strings into a single string 33 | std::string Join(const std::vector &input, const std::string &connector = ""); 34 | 35 | // Check if std::string startswith some character 36 | bool StartsWith(const std::string &input_string, char c, bool ignore_case = false); 37 | 38 | // Check if std::string startswith some substring 39 | bool StartsWith(const std::string &input_string, const std::string &starter, 40 | bool ignore_case = false); 41 | 42 | // Check if std::string endswith character 43 | bool EndsWith(const std::string &input_string, char c, bool ignore_case = false); 44 | 45 | // std::string contains 46 | bool Contains(std::string input, std::string search_string, bool ignore_case = false); 47 | 48 | // Count number of times some search_std::string appears in input_string 49 | size_t Count(std::string input_string, std::string search_string, bool ignore_case = false); 50 | 51 | // repeat input std::string for count number of times, optionally with a 52 | // connector string 53 | std::string Repeat(const std::string &input_string, int count_value, 54 | const std::string &connector = ""); 55 | 56 | // returns copy of input std::string that is all upper case 57 | std::string Upper(const std::string &input_string); 58 | 59 | // returns copy of input std::string that is all lower case 60 | std::string Lower(const std::string &input_string); 61 | 62 | // find and replace substd::string in input string, optionally for a limited 63 | // number of times by default, every occurrence of find_std::string is replaced 64 | // by replace_string 65 | std::string Replace(const std::string &input_string, const std::string &find_string, 66 | const std::string &replace_string, int replace_count = -1); 67 | 68 | // bulk find and replace sub-std::string in input std::string using translation 69 | // table 70 | std::string Translate(const std::string &input_string, 71 | const std::map &translation_table); 72 | 73 | // trim white spaces from the left end of an input string 74 | std::string TrimLeft(const std::string &input_string); 75 | 76 | // trim white spaces from right end of an input string 77 | std::string TrimRight(const std::string &input_string); 78 | 79 | // trim white spaces from either end of an input string 80 | std::string Trim(const std::string &input_string); 81 | 82 | // Returns true if the two input strings are equal 83 | bool Equal(const std::string &lhs, const std::string &rhs); 84 | 85 | // generic find function 86 | // if sub-std::string is found, returns the index 87 | // if sub-std::string is not found, returns std::string::npos 88 | size_t Find(const std::string &input_string, const std::string &search_string); 89 | 90 | // finds first occurrence of sub-std::string in input string 91 | size_t FindFirst(const std::string &input_string, const std::string &search_string); 92 | 93 | // finds last occurrence of sub-std::string in input string 94 | size_t FindLast(const std::string &input_string, const std::string &search_string); 95 | 96 | // performs std regex search and returns a vector of matched results 97 | std::vector FindRegex(const std::string &input_string, 98 | const std::string ®ex_string); 99 | 100 | } // namespace string 101 | 102 | } // namespace jsonlint 103 | -------------------------------------------------------------------------------- /include/jsonlint/utf8.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | // The UTF-8 encoding is a variable width encoding 3 | // in which each Unicode code point can be encoded 4 | // as a sequence of 1 to 4 octects. Each octect is 5 | // composed by a heading byte and trailing bytes. 6 | // Since the encoding is a variable width one, we 7 | // need a way to tell where a sequence starts and 8 | // where it ends. Thatrmation is stored in the 9 | // head byte. 10 | // 11 | // Heading Byte: 12 | // The head byte can take one of these forms: 13 | // 0xxxxxxx: Single byte 14 | // 110xxxxx: Head of a sequence of two bytes 15 | // 1110xxxx: Head of a sequence of three bytes 16 | // 11110xxx: Head of a sequence of four bytes 17 | // 18 | // There's a pattern! 19 | // The number of 1s in a heading byte tells 20 | // us the length of the sequence 21 | // 22 | // Trailing Byte: 23 | // Trailing bytes in a multibyte UTF-8 24 | // sequence always have the following form: 25 | // 10xxxxxx 26 | // 27 | // Encoding: 28 | // The 0s and 1s in the heading and trailing bytes 29 | // format are called control bits. In the UTF-8 30 | // encoding, the concatenation of the non control 31 | // bits is the scalar value of the encoded Unicode 32 | // code point. Because of the structure of these 33 | // forms, the number of required bytes to encode a 34 | // specific Unicode code point with UTF-8 depends on 35 | // the Unicode range where it falls (the ranges are 36 | // given both hexadecimal and binary forms: 37 | // [U+000, U+007F] [00000000, 01111111]: one byte 38 | // [U+0080, U+07FF] [10000000, 111 11111111]: two bytes 39 | // [U+0800, U+FFFF] [1000 00000000, 11111111 11111111]: three bytes 40 | // [U+10000, U+10FFFF] [1 00000000 00000000, 10000 00000000 00000000]: 41 | // four bytes 42 | static bool IsUTF8(char c) { return (((c)&0xC0) != 0x80); } 43 | 44 | static const char TrailingBytesForUTF8[256] = { 45 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 46 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 47 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 49 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 50 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 51 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 52 | 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5}; 53 | 54 | // Returns the length of UTF-8 sequence 55 | static int GetUTF8SequenceLength(char *c) { 56 | return TrailingBytesForUTF8[(unsigned int)(unsigned char)c[0]] + 1; 57 | } 58 | -------------------------------------------------------------------------------- /src/errors.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | namespace jsonlint { 5 | 6 | namespace details { 7 | 8 | unsigned int GetNumberOfDigits(unsigned int number) { 9 | unsigned int digits = 0; 10 | while (number) { 11 | number /= 10; 12 | digits += 1; 13 | } 14 | return digits; 15 | } 16 | 17 | bool GetLines(std::string filename, std::vector &result) { 18 | std::ifstream stream(filename.c_str()); 19 | if (!stream) { 20 | std::cerr << "Cannot open the File : " << filename << std::endl; 21 | return false; 22 | } 23 | std::string line; 24 | // Read the next line from File untill it reaches the end. 25 | while (std::getline(stream, line)) { 26 | // Line contains string of length > 0 then save it in vector 27 | if (line.size() > 0) 28 | result.push_back(line); 29 | } 30 | stream.close(); 31 | return true; 32 | } 33 | 34 | template 35 | void ReportError(Stage &context, Token start, Token end, const std::string &brief_description, 36 | const std::string &detailed_description) { 37 | context.errors.push_back(std::make_tuple(start, end, brief_description, detailed_description)); 38 | 39 | std::string file = start.filename; 40 | unsigned int line = start.line; 41 | unsigned int cursor = start.cursor_start + 1; 42 | 43 | std::string what = file + ":" + std::to_string(line) + ":" + std::to_string(cursor) + ": " + 44 | brief_description + ". " + detailed_description; 45 | 46 | if (context.silent_mode) { 47 | throw std::runtime_error(what); 48 | return; 49 | } 50 | 51 | std::vector line_numbers = {}; 52 | if (line == 1) { 53 | line_numbers = std::vector{line, (line + 1)}; 54 | } else { 55 | line_numbers = std::vector{(line - 1), line, (line + 1)}; 56 | } 57 | unsigned int max_line_number = *(std::max_element(line_numbers.begin(), line_numbers.end())); 58 | std::string blanks(GetNumberOfDigits(max_line_number), ' '); 59 | 60 | std::string message_leading_blanks(cursor - 1, ' '); 61 | std::string message_carets = " "; 62 | 63 | if (start.cursor_start == end.cursor_start && start.cursor_end == end.cursor_end) { 64 | // start and end are the same token 65 | message_carets = "^ "; 66 | } else if (end.cursor_start - start.cursor_end == 1) { 67 | message_carets = " " + std::string(start.cursor_end - start.cursor_start, '^') + " "; 68 | } else if (end.cursor_start - start.cursor_end > 1) { 69 | message_carets = " " + std::string(end.cursor_start - cursor - 1, '^') + " "; 70 | } else { 71 | message_carets = std::string(start.cursor_end - start.cursor_start, '^') + " "; 72 | } 73 | 74 | if (message_carets == " " || message_carets == " ") { 75 | message_carets = " ^ "; 76 | } 77 | std::vector lines; 78 | if (start.filename != "") { 79 | auto return_val = GetLines(start.filename, lines); 80 | if (!return_val) { 81 | throw std::runtime_error(what); 82 | return; 83 | } 84 | } else { 85 | // Used for testing 86 | // Split context.source on \n 87 | lines = string::Split(context.source, "\n"); 88 | } 89 | 90 | std::string error_line = lines[line - 1]; 91 | 92 | std::cout << termcolor::red << termcolor::bold << "error: " << brief_description 93 | << termcolor::reset << std::endl; 94 | std::cout << termcolor::bold << blanks << "--> " << file << ":" << line << ":" << cursor 95 | << std::endl; 96 | 97 | if ((line - 1) > 0) { 98 | std::string line_leading_blanks = ""; 99 | line_leading_blanks.insert(line_leading_blanks.begin(), 100 | (GetNumberOfDigits(max_line_number) - GetNumberOfDigits(line - 1)), 101 | ' '); 102 | std::cout << termcolor::bold << blanks << " | " << std::endl; 103 | if ((line - 2) < lines.size()) 104 | std::cout << termcolor::bold << line_leading_blanks << (line - 1) << " | " << lines[line - 2] 105 | << std::endl; 106 | else 107 | std::cout << termcolor::bold << line_leading_blanks << (line - 1) << " | " << std::endl; 108 | } 109 | 110 | std::string line_leading_blanks = ""; 111 | line_leading_blanks.insert(line_leading_blanks.begin(), 112 | (GetNumberOfDigits(max_line_number) - GetNumberOfDigits(line)), ' '); 113 | 114 | std::cout << termcolor::bold << blanks << " | " << std::endl; 115 | std::cout << termcolor::bold << line_leading_blanks << line << " | " << error_line << std::endl; 116 | std::cout << termcolor::bold << blanks << " | " << message_leading_blanks << termcolor::red 117 | << message_carets << detailed_description << termcolor::reset << std::endl; 118 | 119 | line_leading_blanks = ""; 120 | line_leading_blanks.insert(line_leading_blanks.begin(), 121 | (GetNumberOfDigits(max_line_number) - GetNumberOfDigits(line + 1)), 122 | ' '); 123 | 124 | if ((line + 1) < lines.size()) 125 | std::cout << termcolor::bold << line_leading_blanks << (line + 1) << " | " << lines[line] 126 | << std::endl; 127 | else 128 | std::cout << termcolor::bold << line_leading_blanks << (line + 1) << " | " << std::endl; 129 | 130 | std::cout << blanks << " | " << std::endl; 131 | std::cout << std::endl; 132 | throw std::runtime_error(what); 133 | } 134 | 135 | void ReportLexerError(Lexer &context, Token start, Token end, const std::string &brief_description, 136 | const std::string &detailed_description) { 137 | ReportError(context, start, end, brief_description, detailed_description); 138 | } 139 | 140 | void ReportParserError(Parser &context, Token start, Token end, 141 | const std::string &brief_description, 142 | const std::string &detailed_description) { 143 | ReportError(context, start, end, brief_description, detailed_description); 144 | } 145 | 146 | } // namespace details 147 | 148 | } // namespace jsonlint 149 | -------------------------------------------------------------------------------- /src/lexer.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | namespace jsonlint { 7 | 8 | namespace details { 9 | 10 | std::string ReadCharacter(Lexer &context, bool update_index = true) { 11 | std::string result = ""; 12 | int length = 0; 13 | // get length of next multi-byte character 14 | if (context.index < context.source.size()) 15 | length = GetUTF8SequenceLength(&(context.source[context.index])); 16 | // append bytes of next multi-byte character to result 17 | for (int i = 0; i < length; i++, context.index++) 18 | result += context.source[context.index]; 19 | // update index if necessary 20 | if (!update_index) 21 | context.index -= length; 22 | else 23 | context.cursor += 1; 24 | return result; 25 | } 26 | 27 | std::string PeekCharacter(Lexer &context) { return ReadCharacter(context, false); } 28 | 29 | bool IsHexCharacter(const std::string &character) { 30 | if (character.size() == 0) 31 | return false; 32 | if (character.size() == 1) { 33 | auto c = character[0]; 34 | return (c == '0' || c == '1' || c == '2' || c == '3' || c == '4' || c == '5' || c == '6' || 35 | c == '7' || c == '8' || c == '9' || c == 'a' || c == 'b' || c == 'c' || c == 'd' || 36 | c == 'e' || c == 'f' || c == 'A' || c == 'B' || c == 'C' || c == 'D' || c == 'E' || 37 | c == 'F'); 38 | } 39 | return false; 40 | } 41 | 42 | Token ReadString(Lexer &context) { 43 | Token token{TokenType::STRING, "", context.filename, context.line, context.cursor}; 44 | // consume first double quote 45 | auto peek = ReadCharacter(context); 46 | while (true) { 47 | // peek at next character 48 | peek = PeekCharacter(context); 49 | if (peek[0] == 0x0A || peek[0] == EOF || peek == "") { 50 | token.cursor_start = context.cursor; 51 | token.cursor_end = context.cursor; 52 | ReportLexerError(context, token, token, "Failed to parse string", "Unterminated string"); 53 | } 54 | // check if peek is the start of an escape sequence 55 | if (peek[0] == '\\') { 56 | // consume escape character 57 | peek = ReadCharacter(context); 58 | // check what comes next 59 | peek = PeekCharacter(context); 60 | if (peek[0] == '"') { 61 | peek = ReadCharacter(context); 62 | // escaped double quote character 63 | token.literal += "\\" + peek; 64 | continue; 65 | } else if (peek[0] == '\\') { 66 | peek = ReadCharacter(context); 67 | // escaped backslash character 68 | token.literal += "\\" + peek; 69 | continue; 70 | } else if (peek[0] == 'b' || // backspace 71 | peek[0] == 'f' || // form feed 72 | peek[0] == 'n' || // newline 73 | peek[0] == 'r' || // carriage return 74 | peek[0] == 't') { // horizontal tab 75 | peek = ReadCharacter(context); 76 | token.literal += "\\" + peek; 77 | continue; 78 | } else if (peek[0] == 'u') { 79 | peek = ReadCharacter(context); // consume 'u' 80 | // Expect 4 hex characters here 81 | for (size_t i = 0; i < 4; i++) { 82 | peek = PeekCharacter(context); 83 | if (!IsHexCharacter(peek)) { 84 | token.cursor_start = context.cursor - 1; 85 | token.cursor_end = context.cursor - 1; 86 | ReportLexerError(context, token, token, "Failed to parse unicode escape sequence", 87 | "Expected hex character, instead got '" + peek + "'"); 88 | } 89 | ReadCharacter(context); // consume hex character 90 | } 91 | } else { 92 | peek = ReadCharacter(context); 93 | if (peek[0] == 0x0A || peek[0] == EOF || peek == "") { 94 | token.cursor_start = context.cursor; 95 | token.cursor_end = context.cursor; 96 | ReportLexerError(context, token, token, "Failed to parse string", "Unterminated string"); 97 | } 98 | token.literal += peek; 99 | continue; 100 | } 101 | } 102 | // check if peek terminates string literal 103 | // if not, save character in token literal 104 | peek = PeekCharacter(context); 105 | if (peek[0] != '"' && peek[0] != EOF) { 106 | peek = ReadCharacter(context); 107 | token.literal += peek; 108 | continue; 109 | } 110 | if (peek[0] == 0x0A || peek[0] == EOF || peek == "") { 111 | token.cursor_start = context.cursor + 1; 112 | token.cursor_end = context.cursor + 1; 113 | ReportLexerError(context, token, token, "Failed to parse string", "Unterminated string"); 114 | } 115 | ReadCharacter(context); 116 | break; 117 | } 118 | token.cursor_end = token.cursor_start + token.literal.size() + 2; 119 | return token; 120 | } 121 | 122 | Token ReadNumber(Lexer &context, const std::string &character) { 123 | Token token{TokenType::NUMBER, character, context.filename, context.line, context.cursor}; 124 | std::string next = ReadCharacter(context); 125 | std::string start = next; 126 | bool e_encountered = false; 127 | std::string prev = ""; 128 | while (true) { 129 | auto peek = PeekCharacter(context); 130 | if (peek.size() == 1 && 131 | (peek[0] == '.' || 132 | // encountering an 'e' or 'E' 133 | // check that it's the first 'e' encountered 134 | // e.g., 1EE-5 is not valid 135 | (!e_encountered && (peek[0] == 'E' || peek[0] == 'e')) || 136 | // handle scientific notation 137 | // If previous character was 'e' or 'E' 138 | // then the immediate next character is allowed to be 139 | // '-' or '+', e.g., e-2 E+10 etc. 140 | (e_encountered && prev.size() == 1 && (prev[0] == 'e' || prev[0] == 'E') && 141 | (peek[0] == '-' || peek[0] == '+')) || 142 | isdigit(peek[0]))) { 143 | if (peek[0] == 'e' || peek[0] == 'E') 144 | e_encountered = true; 145 | peek = ReadCharacter(context); 146 | token.literal += peek; 147 | prev = peek; 148 | } else { 149 | break; 150 | } 151 | } 152 | token.cursor_end = token.cursor_start + token.literal.size(); 153 | return token; 154 | } 155 | 156 | bool IsIdentifier(const std::string &character) { 157 | if (character.size() == 0) 158 | return false; 159 | for (auto &c : character) { 160 | if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || (c == '_') || 161 | (c == '$') || ((unsigned char)c >= 0x80)) { 162 | continue; 163 | } else { 164 | return false; 165 | } 166 | } 167 | return true; 168 | } 169 | 170 | Token ReadIdentifier(Lexer &context) { 171 | Token token{TokenType::ILLEGAL, "", context.filename, context.line, context.cursor}; 172 | std::string peek = ""; 173 | while (true) { 174 | peek = PeekCharacter(context); 175 | if (IsIdentifier(peek)) { 176 | token.literal += peek; 177 | peek = ReadCharacter(context); 178 | continue; 179 | } 180 | break; 181 | } 182 | if (token.literal == "true") 183 | token.type = TokenType::TRUE; 184 | else if (token.literal == "false") 185 | token.type = TokenType::FALSE; 186 | else if (token.literal == "null") 187 | token.type = TokenType::NULL_; 188 | else { 189 | auto current = token; 190 | current.cursor_start = current.cursor_start; 191 | current.cursor_end = context.cursor; 192 | auto peek = current; 193 | peek.cursor_start = current.cursor_start + token.literal.size(); 194 | ReportLexerError(context, current, peek, "Failed to parse keyword", 195 | "Expected 'true', 'false', or 'null', instead got '" + token.literal + "'"); 196 | } 197 | return token; 198 | } 199 | 200 | Token ReadPunctuation(Lexer &context, const std::string &character) { 201 | Token token{TokenType::ILLEGAL, character, context.filename, 202 | context.line, context.cursor, context.cursor + 1}; 203 | auto next = ReadCharacter(context); 204 | if (next == ",") { 205 | token.type = TokenType::COMMA; 206 | } else if (next == ":") { 207 | token.type = TokenType::COLON; 208 | } else if (next == "+") { 209 | token.type = TokenType::PLUS; 210 | } else if (next == "-") { 211 | token.type = TokenType::MINUS; 212 | } else if (next == "[") { 213 | token.type = TokenType::LEFT_BRACKET; 214 | } else if (next == "{") { 215 | token.type = TokenType::LEFT_BRACE; 216 | } else if (next == "]") { 217 | token.type = TokenType::RIGHT_BRACKET; 218 | } else if (next == "}") { 219 | token.type = TokenType::RIGHT_BRACE; 220 | } else { 221 | token.cursor_start = context.cursor - 2; 222 | token.cursor_end = context.cursor - 1; 223 | ReportLexerError(context, token, token, "Failed to parse punctuation", 224 | "Unexpected token '" + token.literal + "'"); 225 | } 226 | return token; 227 | } 228 | 229 | void ReadWhitespace(Lexer &context) { 230 | // consume whitespace 231 | ReadCharacter(context); 232 | while (true) { 233 | auto peek = PeekCharacter(context); 234 | char c = '\0'; 235 | if (peek.size() > 0) 236 | c = peek[0]; 237 | if (c == 0x20 || c == 0x0D || c == 0x08 || c == 0x09) { 238 | ReadCharacter(context); 239 | } else 240 | return; 241 | } 242 | } 243 | 244 | } // namespace details 245 | 246 | std::vector Tokenize(Lexer &context) { 247 | std::vector result; 248 | context.index = 0; 249 | using namespace details; 250 | while (context.index < context.source.size()) { 251 | if (IsUTF8(context.source[context.index])) { 252 | auto peek = PeekCharacter(context); 253 | if (isdigit(peek[0])) { 254 | result.push_back(ReadNumber(context, peek)); 255 | } else if (peek[0] == '"') { 256 | result.push_back(ReadString(context)); 257 | } else if (ispunct(peek[0])) { 258 | result.push_back(ReadPunctuation(context, peek)); 259 | } else if (IsIdentifier(peek)) { 260 | result.push_back(ReadIdentifier(context)); 261 | } else if (peek[0] == 0x20 || peek[0] == 0x0D || peek[0] == 0x08 || peek[0] == 0x09) { 262 | ReadWhitespace(context); 263 | } 264 | if (peek[0] == '\n') { 265 | context.line += 1; 266 | ReadCharacter(context); 267 | context.cursor = 1; 268 | } 269 | continue; 270 | } 271 | } 272 | Token token{TokenType::EOF_, "", context.filename, context.line, context.cursor, context.cursor}; 273 | result.push_back(token); 274 | return result; 275 | } 276 | 277 | } // namespace jsonlint 278 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | using namespace jsonlint; 6 | 7 | int main(int argc, char *argv[]) { 8 | argparse::ArgumentParser program("jsonlint"); 9 | program.add_argument("file").help("json file to validate"); 10 | try { 11 | program.parse_args(argc, argv); 12 | } catch (const std::runtime_error &err) { 13 | std::cout << err.what() << std::endl; 14 | std::cout << program; 15 | exit(0); 16 | } 17 | auto filename = program.get("file"); 18 | std::string source = ""; 19 | try { 20 | std::ifstream stream(filename, std::ifstream::in); 21 | if (stream.is_open()) { 22 | std::cout << "Parsing " << filename << std::endl; 23 | source = 24 | std::string((std::istreambuf_iterator(stream)), std::istreambuf_iterator()); 25 | Lexer lexer{source, 0, filename, 1, 1}; 26 | auto tokens = Tokenize(lexer); 27 | Parser parser(tokens, source); 28 | parser.ParseJson(); 29 | } else { 30 | std::cerr << termcolor::red << termcolor::bold << "error: failed to open " << filename 31 | << std::endl; 32 | } 33 | } catch (std::runtime_error &e) { 34 | } 35 | return 0; 36 | } 37 | -------------------------------------------------------------------------------- /src/parser.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | namespace jsonlint { 8 | 9 | namespace details { 10 | 11 | bool ParseElement(Parser &context) { 12 | auto prefix = context.visitors[context.current.type]; 13 | if (!prefix) { 14 | ReportParserError(context, context.current, context.peek, "Failed to parse element", 15 | "Unexpected token '" + context.current.literal + "'"); 16 | return false; 17 | } else { 18 | return prefix(context); 19 | } 20 | } 21 | 22 | bool ParsePrimitive(Parser &context) { return true; } 23 | 24 | bool ParseSignedNumber(Parser &context) { 25 | // assume current is '-' or '+' 26 | if (!context.ExpectPeek(TokenType::NUMBER)) { 27 | context.NextToken(); 28 | ReportParserError(context, context.current, context.peek, "Failed to parse signed number", 29 | "Expected NUMBER, instead got '" + context.current.literal + "'"); 30 | return false; 31 | } 32 | // current is number 33 | return true; 34 | } 35 | 36 | bool ParseArrayLiteral(Parser &context) { 37 | // assume current is '[' 38 | if (context.IsPeekToken(TokenType::RIGHT_BRACKET)) { 39 | context.NextToken(); 40 | return true; 41 | } 42 | // there's at least one element 43 | context.NextToken(); // get past '[' 44 | if (!ParseElement(context)) 45 | return false; 46 | while (context.IsPeekToken(TokenType::COMMA)) { 47 | context.NextToken(); // current is ',' 48 | if (context.IsPeekToken(TokenType::RIGHT_BRACKET)) { 49 | // comma was a trailing comma, e.g., [1, 2, 3, ] 50 | // ^^^ we're here 51 | ReportParserError(context, context.current, context.current, "Failed to parse array", 52 | "Expected ']', instead got ','"); 53 | } 54 | context.NextToken(); // get past ',' 55 | if (!ParseElement(context) || context.IsCurrentToken(TokenType::EOF_)) 56 | return false; 57 | } 58 | if (!context.ExpectPeek(TokenType::RIGHT_BRACKET)) { 59 | context.NextToken(); 60 | ReportParserError(context, context.current, context.peek, "Failed to parse array", 61 | "Expected ']', instead got '" + context.current.literal + "'"); 62 | return false; 63 | } 64 | return true; 65 | } 66 | 67 | bool ParseObject(Parser &context) { 68 | // assume current is '{' 69 | std::set keys; 70 | if (context.IsPeekToken(TokenType::RIGHT_BRACE)) { 71 | context.NextToken(); 72 | return true; 73 | } 74 | // there's at least one element 75 | context.NextToken(); // get past '{' 76 | while (!context.IsCurrentToken(TokenType::RIGHT_BRACE) && 77 | !context.IsCurrentToken(TokenType::EOF_)) { 78 | if (!context.IsCurrentToken(TokenType::STRING)) { 79 | ReportParserError(context, context.current, context.peek, "Failed to parse object", 80 | "Expected STRING, instead got '" + context.current.literal + "'"); 81 | return false; 82 | } 83 | if (keys.find(context.current.literal) == keys.end()) 84 | keys.insert(context.current.literal); 85 | else { 86 | ReportParserError(context, context.current, context.peek, "Failed to parse object", 87 | "Duplicate key '" + context.current.literal + "'"); 88 | } 89 | // current is string key 90 | if (!context.ExpectPeek(TokenType::COLON)) { 91 | context.NextToken(); 92 | ReportParserError(context, context.current, context.peek, "Failed to parse object", 93 | "Expected ':', instead got '" + context.current.literal + "'"); 94 | return false; 95 | } 96 | // current is colon 97 | context.NextToken(); // get past ':' 98 | auto value = ParseElement(context); 99 | if (!value) 100 | return false; 101 | if (!context.IsPeekToken(TokenType::RIGHT_BRACE) && !context.ExpectPeek(TokenType::COMMA)) { 102 | context.NextToken(); 103 | ReportParserError(context, context.current, context.peek, "Failed to parse object", 104 | "Expected '}', instead got '" + context.current.literal + "'"); 105 | return false; 106 | } 107 | context.NextToken(); 108 | } 109 | return true; 110 | } 111 | 112 | } // namespace details 113 | 114 | Parser::Parser(const std::vector &tokens, const std::string &source) 115 | : current_index(0), current(Token{}), peek(Token{}), tokens(tokens), source(source), errors({}), 116 | silent_mode(false) { 117 | RegisterVisitor(TokenType::NUMBER, details::ParsePrimitive); 118 | RegisterVisitor(TokenType::STRING, details::ParsePrimitive); 119 | RegisterVisitor(TokenType::TRUE, details::ParsePrimitive); 120 | RegisterVisitor(TokenType::FALSE, details::ParsePrimitive); 121 | RegisterVisitor(TokenType::NULL_, details::ParsePrimitive); 122 | RegisterVisitor(TokenType::PLUS, details::ParseSignedNumber); 123 | RegisterVisitor(TokenType::MINUS, details::ParseSignedNumber); 124 | RegisterVisitor(TokenType::LEFT_BRACKET, details::ParseArrayLiteral); 125 | RegisterVisitor(TokenType::LEFT_BRACE, details::ParseObject); 126 | } 127 | 128 | void Parser::PreviousToken() { 129 | peek = current; 130 | current_index -= 1; 131 | current = tokens[tokens.size() - 1]; // last token 132 | if (current_index - 2 < tokens.size()) 133 | current = tokens[current_index - 2]; 134 | else 135 | current = tokens[0]; 136 | } 137 | 138 | void Parser::NextToken() { 139 | current = peek; 140 | current_index += 1; 141 | peek = tokens[tokens.size() - 1]; 142 | if (current_index - 1 < tokens.size()) 143 | peek = tokens[current_index - 1]; 144 | } 145 | 146 | bool Parser::IsCurrentToken(TokenType value) { return (current.type == value); } 147 | 148 | bool Parser::IsPeekToken(TokenType value) { return (peek.type == value); } 149 | 150 | bool Parser::ExpectPeek(TokenType value) { 151 | if (IsPeekToken(value)) { 152 | NextToken(); 153 | return true; 154 | } else { 155 | return false; 156 | } 157 | } 158 | 159 | void Parser::RegisterVisitor(TokenType type, std::function function) { 160 | if (visitors.find(type) != visitors.end()) 161 | visitors[type] = function; 162 | else 163 | visitors.insert(std::make_pair(type, function)); 164 | } 165 | 166 | bool Parser::ParseJson() { 167 | if (!tokens.empty()) { 168 | // Initialize current and peek token 169 | NextToken(); 170 | NextToken(); 171 | if (!IsPeekToken(TokenType::EOF_)) { 172 | if (!details::ParseElement(*this)) 173 | return false; 174 | } 175 | if (!ExpectPeek(TokenType::EOF_)) { 176 | NextToken(); 177 | details::ReportParserError(*this, current, peek, "Failed to parse JSON", 178 | "Expected EOF, instead got '" + current.literal + "'"); 179 | return false; 180 | } 181 | } 182 | if (!silent_mode) { 183 | std::cout << termcolor::green << termcolor::bold << "Valid JSON" << termcolor::reset 184 | << std::endl; 185 | } 186 | return true; 187 | } 188 | 189 | } // namespace jsonlint 190 | -------------------------------------------------------------------------------- /src/string.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | namespace jsonlint { 4 | 5 | namespace string { 6 | 7 | // std::string operations 8 | size_t Copy(char *dst, const char *src, size_t destination_size) { 9 | size_t length = strlen(src); 10 | if (destination_size) { 11 | size_t bl = (length < destination_size - 1 ? length : destination_size - 1); 12 | ((char *)memcpy(dst, src, bl))[bl] = 0; 13 | } 14 | return length; 15 | } 16 | 17 | std::string Format(std::string format_string, ...) { 18 | int n = 19 | ((int)format_string.size()) * 2; /* Reserve two times as much as the length of the fmt_str */ 20 | std::unique_ptr formatted; 21 | va_list ap; 22 | while (1) { 23 | formatted.reset(new char[n]); /* Wrap the plain char array into the unique_ptr */ 24 | Copy(&formatted[0], format_string.c_str(), format_string.length()); 25 | va_start(ap, format_string); 26 | int final_n = vsnprintf(&formatted[0], n, format_string.c_str(), ap); 27 | va_end(ap); 28 | if (final_n < 0 || final_n >= n) 29 | n += abs(final_n - n + 1); 30 | else 31 | break; 32 | } 33 | return std::string(formatted.get()); 34 | } 35 | 36 | // Python-style std::string slicing 37 | std::string Slice(const std::string &input_string, int start_index, int end_index, int step) { 38 | std::string result; 39 | if (end_index == INT_MAX) { // if end index is default, i.e., till end of string 40 | if (start_index < 0) { // if start index is negative, then start start_index 41 | // characters from end of string 42 | for (int i = static_cast(input_string.size()) + start_index; 43 | i < static_cast(input_string.size()); i = i + step) { 44 | result += input_string[i]; 45 | } 46 | } else { // start_index is not negative; iterate from start_index till end 47 | // of string 48 | for (int i = start_index; i < static_cast(input_string.size()); i = i + step) { 49 | result += input_string[i]; 50 | } 51 | } 52 | } else { // end_index is not std::string::npos 53 | if (end_index < 0) { // if end_index is negative, start from start_index and 54 | // go till (end_of_string + end_index) 55 | for (int i = start_index; i < static_cast(input_string.size()) + end_index; 56 | i = i + step) { 57 | result += input_string[i]; 58 | } 59 | } else { // if end_index is not negative either, then this is the trivial 60 | // case 61 | for (int i = start_index; i < end_index; i = i + step) { 62 | result += input_string[i]; 63 | } 64 | } 65 | } 66 | return result; 67 | } 68 | 69 | // split string based on a delimiter string 70 | // supports multi-character delimiter 71 | // returns a vector of substrings after split 72 | std::vector Split(const std::string &input_string, const std::string &delimiter, 73 | std::shared_ptr> result) { 74 | std::string current_string(input_string); 75 | size_t delimiter_position = 76 | current_string.find(delimiter); // check if delimiter is in input string 77 | if (delimiter_position != std::string::npos) { // if delimiter position is not end_of_string 78 | size_t counter_position = 0; 79 | std::string split_string = current_string.substr(counter_position, delimiter_position); 80 | delimiter_position += delimiter.size(); 81 | std::string split_remaining = current_string.erase(counter_position, delimiter_position); 82 | result->push_back(split_string); 83 | Split(split_remaining, delimiter, result); 84 | } else { // delimiter not in input string. Just add entire input string to 85 | // result vector 86 | result->push_back(current_string); 87 | } 88 | return *result; 89 | } 90 | 91 | // Check if string startswith some character 92 | bool StartsWith(const std::string &input_string, char c, bool ignore_case) { 93 | bool result = false; 94 | if (input_string.size() > 0) { 95 | char first_character = input_string[0]; 96 | if (ignore_case) { 97 | first_character = toupper(first_character); 98 | c = toupper(c); 99 | } 100 | if (first_character == c) 101 | result = true; 102 | } 103 | return result; 104 | } 105 | 106 | // Check if string startswith some substring 107 | bool StartsWith(const std::string &input_string, const std::string &starter, bool ignore_case) { 108 | bool result = false; 109 | if (input_string.size() > 0 && input_string.size() > starter.size()) { 110 | std::string input_copy = input_string; 111 | for (size_t i = 0; i < starter.size(); i++) { 112 | char input_character = input_string[i]; 113 | char starter_character = starter[i]; 114 | if (ignore_case) { 115 | input_character = toupper(input_character); 116 | starter_character = toupper(starter_character); 117 | } 118 | if (input_character == starter_character) { 119 | result = true; 120 | continue; 121 | } else { 122 | result = false; 123 | break; 124 | } 125 | } 126 | } 127 | return result; 128 | } 129 | 130 | // Check if string endswith character 131 | bool EndsWith(const std::string &input_string, char c, bool ignore_case) { 132 | bool result = false; 133 | if (input_string.size() > 0) { 134 | char last_character = input_string[input_string.size() - 1]; 135 | if (ignore_case) { 136 | last_character = toupper(last_character); 137 | c = toupper(c); 138 | } 139 | if (last_character == c) 140 | result = true; 141 | } 142 | return result; 143 | } 144 | 145 | // std::string contains 146 | bool Contains(std::string input, std::string search_string, bool ignore_case) { 147 | bool result = false; 148 | if (ignore_case) { // if case is to be ignored, convert both input and search 149 | // string to all upper case 150 | std::transform(input.begin(), input.end(), input.begin(), ::toupper); 151 | std::transform(search_string.begin(), search_string.end(), search_string.begin(), ::toupper); 152 | } 153 | if (input.find(search_string) != std::string::npos) { // search using find algorithm 154 | result = true; 155 | } 156 | return result; 157 | } 158 | 159 | // Count number of times some search_string appears in input_string 160 | size_t Count(std::string input_string, std::string search_string, bool ignore_case) { 161 | size_t result = 0; 162 | if (Contains(input_string, search_string, ignore_case)) { 163 | if (ignore_case) { 164 | std::transform(input_string.begin(), input_string.end(), input_string.begin(), ::toupper); 165 | std::transform(search_string.begin(), search_string.end(), search_string.begin(), ::toupper); 166 | } 167 | auto split_vector = Split(input_string, search_string); 168 | result = split_vector.size() - 1; 169 | } 170 | return result; 171 | } 172 | 173 | // repeat input string for count number of times, optionally with a connector 174 | // string 175 | std::string Repeat(const std::string &input_string, int count_value, const std::string &connector) { 176 | std::string result = ""; 177 | while (count_value > 0) { 178 | result += input_string + connector; 179 | count_value--; 180 | } 181 | return result; 182 | } 183 | 184 | // returns copy of input string that is all upper case 185 | std::string Upper(const std::string &input_string) { 186 | std::string result; 187 | result.resize(input_string.size()); 188 | std::transform(input_string.begin(), input_string.end(), result.begin(), ::toupper); 189 | return result; 190 | } 191 | 192 | // returns copy of input string that is all lower case 193 | std::string Lower(const std::string &input_string) { 194 | std::string result; 195 | result.resize(input_string.size()); 196 | std::transform(input_string.begin(), input_string.end(), result.begin(), ::tolower); 197 | return result; 198 | } 199 | 200 | // find and replace substring in input string, optionally for a limited number 201 | // of times by default, every occurrence of find_string is replaced by 202 | // replace_string 203 | std::string Replace(const std::string &input_string, const std::string &find_string, 204 | const std::string &replace_string, int replace_count) { 205 | std::string result = ""; 206 | auto split_vector = Split(input_string, find_string); 207 | auto split_size = split_vector.size(); 208 | auto max_replace_count = split_size - 1; 209 | if (replace_count == -1) { 210 | for (auto &s : split_vector) { 211 | if (max_replace_count > 0) 212 | result += s + replace_string; 213 | else 214 | result += s; 215 | max_replace_count--; 216 | } 217 | } else { 218 | for (auto &s : split_vector) { 219 | if (replace_count > 0) { 220 | if (max_replace_count > 0) 221 | result += s + replace_string; 222 | else 223 | result += s; 224 | max_replace_count--; 225 | } else { 226 | if (max_replace_count > 0) 227 | result += s + find_string; 228 | else 229 | result += s; 230 | max_replace_count--; 231 | } 232 | replace_count--; 233 | } 234 | } 235 | return result; 236 | } 237 | 238 | // join a vector of strings into a single string 239 | std::string Join(const std::vector &input, const std::string &connector) { 240 | std::string result; 241 | size_t max_connector_count = input.size() - 1; 242 | for (auto &s : input) { 243 | if (max_connector_count > 0) 244 | result += s + connector; 245 | else 246 | result += s; 247 | max_connector_count--; 248 | } 249 | return result; 250 | } 251 | 252 | // bulk find and replace sub-string in input string using translation table 253 | std::string Translate(const std::string &input_string, 254 | const std::map &translation_table) { 255 | std::string result = input_string; 256 | for (auto &pair : translation_table) 257 | result = Replace(result, pair.first, pair.second); 258 | return result; 259 | } 260 | 261 | // trim white spaces from the left end of an input string 262 | std::string TrimLeft(const std::string &input_string) { 263 | std::string result = input_string; 264 | result.erase(result.begin(), std::find_if(result.begin(), result.end(), 265 | [](int ch) { return !std::isspace(ch); })); 266 | return result; 267 | } 268 | 269 | // trim white spaces from right end of an input string 270 | std::string TrimRight(const std::string &input_string) { 271 | std::string result = input_string; 272 | result.erase( 273 | std::find_if(result.rbegin(), result.rend(), [](int ch) { return !std::isspace(ch); }).base(), 274 | result.end()); 275 | return result; 276 | } 277 | 278 | // trim white spaces from either end of an input string 279 | std::string Trim(const std::string &input_string) { return TrimLeft(TrimRight(input_string)); } 280 | 281 | // Returns true if the two input strings are equal 282 | bool Equal(const std::string &lhs, const std::string &rhs) { return (lhs.compare(rhs) == 0); } 283 | 284 | // generic find function 285 | // if sub-string is found, returns the index 286 | // if sub-string is not found, returns std::string::npos 287 | size_t Find(const std::string &input_string, const std::string &search_string) { 288 | return input_string.find(search_string); 289 | } 290 | 291 | // finds first occurrence of sub-string in input string 292 | size_t FindFirst(const std::string &input_string, const std::string &search_string) { 293 | return input_string.find_first_of(search_string); 294 | } 295 | 296 | // finds last occurrence of sub-string in input string 297 | size_t FindLast(const std::string &input_string, const std::string &search_string) { 298 | return input_string.find_last_of(search_string); 299 | } 300 | 301 | // performs std regex search and returns a vector of matched results 302 | std::vector FindRegex(const std::string &input_string, 303 | const std::string ®ex_string) { 304 | std::string current_string(input_string); 305 | std::vector results; 306 | std::smatch match_result; 307 | while (std::regex_search(current_string, match_result, std::regex(regex_string))) { 308 | for (auto &m : match_result) 309 | results.push_back(m); 310 | current_string = match_result.suffix(); 311 | } 312 | return results; 313 | } 314 | 315 | } // namespace string 316 | 317 | } // namespace jsonlint 318 | -------------------------------------------------------------------------------- /test/lexer/keywords.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | using namespace jsonlint; 5 | 6 | TEST_CASE("Keyword 'true'", "[lexer]") { 7 | std::string filename = ""; 8 | std::string source = "true"; 9 | Lexer lexer{"", 0, "", 1, 1}; 10 | lexer.filename = filename; 11 | lexer.source = source; 12 | auto tokens = Tokenize(lexer); 13 | REQUIRE(tokens.size() == 2); 14 | REQUIRE(tokens[0].filename == ""); 15 | REQUIRE(tokens[0].line == 1); 16 | REQUIRE(tokens[0].cursor_start == 1); 17 | REQUIRE(tokens[0].type == TokenType::TRUE); 18 | REQUIRE(tokens[0].literal == source); 19 | REQUIRE(tokens[1].filename == ""); 20 | REQUIRE(tokens[1].line == 1); 21 | REQUIRE(tokens[1].cursor_start == source.size() + 1); 22 | REQUIRE(tokens[1].type == TokenType::EOF_); 23 | REQUIRE(tokens[1].literal == ""); 24 | } 25 | 26 | TEST_CASE("Keyword 'false'", "[lexer]") { 27 | std::string filename = ""; 28 | std::string source = "false"; 29 | Lexer lexer{"", 0, "", 1, 1}; 30 | lexer.filename = filename; 31 | lexer.source = source; 32 | auto tokens = Tokenize(lexer); 33 | REQUIRE(tokens.size() == 2); 34 | REQUIRE(tokens[0].filename == ""); 35 | REQUIRE(tokens[0].line == 1); 36 | REQUIRE(tokens[0].cursor_start == 1); 37 | REQUIRE(tokens[0].type == TokenType::FALSE); 38 | REQUIRE(tokens[0].literal == source); 39 | REQUIRE(tokens[1].filename == ""); 40 | REQUIRE(tokens[1].line == 1); 41 | REQUIRE(tokens[1].cursor_start == source.size() + 1); 42 | REQUIRE(tokens[1].type == TokenType::EOF_); 43 | REQUIRE(tokens[1].literal == ""); 44 | } 45 | 46 | TEST_CASE("Keyword 'null'", "[lexer]") { 47 | std::string filename = ""; 48 | std::string source = "null"; 49 | Lexer lexer{"", 0, "", 1, 1}; 50 | lexer.filename = filename; 51 | lexer.source = source; 52 | auto tokens = Tokenize(lexer); 53 | REQUIRE(tokens.size() == 2); 54 | REQUIRE(tokens[0].filename == ""); 55 | REQUIRE(tokens[0].line == 1); 56 | REQUIRE(tokens[0].cursor_start == 1); 57 | REQUIRE(tokens[0].type == TokenType::NULL_); 58 | REQUIRE(tokens[0].literal == source); 59 | REQUIRE(tokens[1].filename == ""); 60 | REQUIRE(tokens[1].line == 1); 61 | REQUIRE(tokens[1].cursor_start == source.size() + 1); 62 | REQUIRE(tokens[1].type == TokenType::EOF_); 63 | REQUIRE(tokens[1].literal == ""); 64 | } 65 | 66 | #include 67 | 68 | TEST_CASE("Failed to parse unexpected keyword 'foo'", "[lexer]") { 69 | std::string filename = ""; 70 | std::string source = "foo"; 71 | Lexer lexer{"", 0, "", 1, 1}; 72 | lexer.filename = filename; 73 | lexer.source = source; 74 | lexer.silent_mode = true; 75 | bool exception_thrown = false; 76 | try { 77 | auto tokens = Tokenize(lexer); 78 | } catch (std::runtime_error &e) { 79 | exception_thrown = true; 80 | } 81 | auto errors = lexer.errors; 82 | REQUIRE(errors.size() == 1); 83 | REQUIRE(std::get<2>(errors[0]) == std::string{"Failed to parse keyword"}); 84 | REQUIRE(std::get<3>(errors[0]) == 85 | std::string{"Expected 'true', 'false', or 'null', instead got 'foo'"}); 86 | } 87 | -------------------------------------------------------------------------------- /test/lexer/number.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | using namespace jsonlint; 5 | 6 | TEST_CASE("Integer '0'", "[lexer]") { 7 | std::string filename = ""; 8 | std::string source = "0"; 9 | Lexer lexer{"", 0, "", 1, 1}; 10 | lexer.filename = filename; 11 | lexer.source = source; 12 | auto tokens = Tokenize(lexer); 13 | REQUIRE(tokens.size() == 2); 14 | REQUIRE(tokens[0].filename == ""); 15 | REQUIRE(tokens[0].line == 1); 16 | REQUIRE(tokens[0].cursor_start == 1); 17 | REQUIRE(tokens[0].type == TokenType::NUMBER); 18 | REQUIRE(tokens[0].literal == "0"); 19 | REQUIRE(tokens[1].filename == ""); 20 | REQUIRE(tokens[1].line == 1); 21 | REQUIRE(tokens[1].cursor_start == 2); 22 | REQUIRE(tokens[1].type == TokenType::EOF_); 23 | REQUIRE(tokens[1].literal == ""); 24 | } 25 | 26 | TEST_CASE("Integer '5'", "[lexer]") { 27 | std::string filename = ""; 28 | std::string source = "5"; 29 | Lexer lexer{"", 0, "", 1, 1}; 30 | lexer.filename = filename; 31 | lexer.source = source; 32 | auto tokens = Tokenize(lexer); 33 | REQUIRE(tokens.size() == 2); 34 | REQUIRE(tokens[0].filename == ""); 35 | REQUIRE(tokens[0].line == 1); 36 | REQUIRE(tokens[0].cursor_start == 1); 37 | REQUIRE(tokens[0].type == TokenType::NUMBER); 38 | REQUIRE(tokens[0].literal == "5"); 39 | REQUIRE(tokens[1].filename == ""); 40 | REQUIRE(tokens[1].line == 1); 41 | REQUIRE(tokens[1].cursor_start == 2); 42 | REQUIRE(tokens[1].type == TokenType::EOF_); 43 | REQUIRE(tokens[1].literal == ""); 44 | } 45 | 46 | TEST_CASE("Integer '42'", "[lexer]") { 47 | std::string filename = ""; 48 | std::string source = "42"; 49 | Lexer lexer{"", 0, "", 1, 1}; 50 | lexer.filename = filename; 51 | lexer.source = source; 52 | auto tokens = Tokenize(lexer); 53 | REQUIRE(tokens.size() == 2); 54 | REQUIRE(tokens[0].filename == ""); 55 | REQUIRE(tokens[0].line == 1); 56 | REQUIRE(tokens[0].cursor_start == 1); 57 | REQUIRE(tokens[0].type == TokenType::NUMBER); 58 | REQUIRE(tokens[0].literal == "42"); 59 | REQUIRE(tokens[1].filename == ""); 60 | REQUIRE(tokens[1].line == 1); 61 | REQUIRE(tokens[1].cursor_start == 3); 62 | REQUIRE(tokens[1].type == TokenType::EOF_); 63 | REQUIRE(tokens[1].literal == ""); 64 | } 65 | 66 | TEST_CASE("Integer '-96'", "[lexer]") { 67 | std::string filename = ""; 68 | std::string source = "-96"; 69 | Lexer lexer{"", 0, "", 1, 1}; 70 | lexer.filename = filename; 71 | lexer.source = source; 72 | auto tokens = Tokenize(lexer); 73 | REQUIRE(tokens.size() == 3); 74 | REQUIRE(tokens[0].filename == ""); 75 | REQUIRE(tokens[0].line == 1); 76 | REQUIRE(tokens[0].cursor_start == 1); 77 | REQUIRE(tokens[0].type == TokenType::MINUS); 78 | REQUIRE(tokens[0].literal == "-"); 79 | REQUIRE(tokens[1].filename == ""); 80 | REQUIRE(tokens[1].line == 1); 81 | REQUIRE(tokens[1].cursor_start == 2); 82 | REQUIRE(tokens[1].type == TokenType::NUMBER); 83 | REQUIRE(tokens[1].literal == "96"); 84 | REQUIRE(tokens[2].filename == ""); 85 | REQUIRE(tokens[2].line == 1); 86 | REQUIRE(tokens[2].cursor_start == 4); 87 | REQUIRE(tokens[2].type == TokenType::EOF_); 88 | REQUIRE(tokens[2].literal == ""); 89 | } 90 | 91 | TEST_CASE("Double '0.0'", "[lexer]") { 92 | std::string filename = ""; 93 | std::string source = "0.0"; 94 | Lexer lexer{"", 0, "", 1, 1}; 95 | lexer.filename = filename; 96 | lexer.source = source; 97 | auto tokens = Tokenize(lexer); 98 | REQUIRE(tokens.size() == 2); 99 | REQUIRE(tokens[0].filename == ""); 100 | REQUIRE(tokens[0].line == 1); 101 | REQUIRE(tokens[0].cursor_start == 1); 102 | REQUIRE(tokens[0].type == TokenType::NUMBER); 103 | REQUIRE(tokens[0].literal == "0.0"); 104 | REQUIRE(tokens[1].filename == ""); 105 | REQUIRE(tokens[1].line == 1); 106 | REQUIRE(tokens[1].cursor_start == 4); 107 | REQUIRE(tokens[1].type == TokenType::EOF_); 108 | REQUIRE(tokens[1].literal == ""); 109 | } 110 | 111 | TEST_CASE("Double '3.14'", "[lexer]") { 112 | std::string filename = ""; 113 | std::string source = "3.14"; 114 | Lexer lexer{"", 0, "", 1, 1}; 115 | lexer.filename = filename; 116 | lexer.source = source; 117 | auto tokens = Tokenize(lexer); 118 | REQUIRE(tokens.size() == 2); 119 | REQUIRE(tokens[0].filename == ""); 120 | REQUIRE(tokens[0].line == 1); 121 | REQUIRE(tokens[0].cursor_start == 1); 122 | REQUIRE(tokens[0].type == TokenType::NUMBER); 123 | REQUIRE(tokens[0].literal == "3.14"); 124 | REQUIRE(tokens[1].filename == ""); 125 | REQUIRE(tokens[1].line == 1); 126 | REQUIRE(tokens[1].cursor_start == 5); 127 | REQUIRE(tokens[1].type == TokenType::EOF_); 128 | REQUIRE(tokens[1].literal == ""); 129 | } 130 | 131 | TEST_CASE("Double '2.71828'", "[lexer]") { 132 | std::string filename = ""; 133 | std::string source = "2.71828"; 134 | Lexer lexer{"", 0, "", 1, 1}; 135 | lexer.filename = filename; 136 | lexer.source = source; 137 | auto tokens = Tokenize(lexer); 138 | REQUIRE(tokens.size() == 2); 139 | REQUIRE(tokens[0].filename == ""); 140 | REQUIRE(tokens[0].line == 1); 141 | REQUIRE(tokens[0].cursor_start == 1); 142 | REQUIRE(tokens[0].type == TokenType::NUMBER); 143 | REQUIRE(tokens[0].literal == "2.71828"); 144 | REQUIRE(tokens[1].filename == ""); 145 | REQUIRE(tokens[1].line == 1); 146 | REQUIRE(tokens[1].cursor_start == 8); 147 | REQUIRE(tokens[1].type == TokenType::EOF_); 148 | REQUIRE(tokens[1].literal == ""); 149 | } 150 | 151 | TEST_CASE("Double '1.61803398875'", "[lexer]") { 152 | std::string filename = ""; 153 | std::string source = "1.61803398875"; 154 | Lexer lexer{"", 0, "", 1, 1}; 155 | lexer.filename = filename; 156 | lexer.source = source; 157 | auto tokens = Tokenize(lexer); 158 | REQUIRE(tokens.size() == 2); 159 | REQUIRE(tokens[0].filename == ""); 160 | REQUIRE(tokens[0].line == 1); 161 | REQUIRE(tokens[0].cursor_start == 1); 162 | REQUIRE(tokens[0].type == TokenType::NUMBER); 163 | REQUIRE(tokens[0].literal == "1.61803398875"); 164 | REQUIRE(tokens[1].filename == ""); 165 | REQUIRE(tokens[1].line == 1); 166 | REQUIRE(tokens[1].cursor_start == 14); 167 | REQUIRE(tokens[1].type == TokenType::EOF_); 168 | REQUIRE(tokens[1].literal == ""); 169 | } 170 | -------------------------------------------------------------------------------- /test/lexer/punctuation.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | using namespace jsonlint; 5 | 6 | TEST_CASE("Comma ','", "[lexer]") { 7 | std::string filename = ""; 8 | std::string source = ","; 9 | Lexer lexer{"", 0, "", 1, 1}; 10 | lexer.filename = filename; 11 | lexer.source = source; 12 | auto tokens = Tokenize(lexer); 13 | REQUIRE(tokens.size() == 2); 14 | REQUIRE(tokens[0].filename == ""); 15 | REQUIRE(tokens[0].line == 1); 16 | REQUIRE(tokens[0].cursor_start == 1); 17 | REQUIRE(tokens[0].type == TokenType::COMMA); 18 | REQUIRE(tokens[0].literal == ","); 19 | REQUIRE(tokens[1].filename == ""); 20 | REQUIRE(tokens[1].line == 1); 21 | REQUIRE(tokens[1].cursor_start == 2); 22 | REQUIRE(tokens[1].type == TokenType::EOF_); 23 | REQUIRE(tokens[1].literal == ""); 24 | } 25 | 26 | TEST_CASE("Colon ':'", "[lexer]") { 27 | std::string filename = ""; 28 | std::string source = ":"; 29 | Lexer lexer{"", 0, "", 1, 1}; 30 | lexer.filename = filename; 31 | lexer.source = source; 32 | auto tokens = Tokenize(lexer); 33 | REQUIRE(tokens.size() == 2); 34 | REQUIRE(tokens[0].filename == ""); 35 | REQUIRE(tokens[0].line == 1); 36 | REQUIRE(tokens[0].cursor_start == 1); 37 | REQUIRE(tokens[0].type == TokenType::COLON); 38 | REQUIRE(tokens[0].literal == ":"); 39 | REQUIRE(tokens[1].filename == ""); 40 | REQUIRE(tokens[1].line == 1); 41 | REQUIRE(tokens[1].cursor_start == 2); 42 | REQUIRE(tokens[1].type == TokenType::EOF_); 43 | REQUIRE(tokens[1].literal == ""); 44 | } 45 | 46 | TEST_CASE("Plus '+'", "[lexer]") { 47 | std::string filename = ""; 48 | std::string source = "+"; 49 | Lexer lexer{"", 0, "", 1, 1}; 50 | lexer.filename = filename; 51 | lexer.source = source; 52 | auto tokens = Tokenize(lexer); 53 | REQUIRE(tokens.size() == 2); 54 | REQUIRE(tokens[0].filename == ""); 55 | REQUIRE(tokens[0].line == 1); 56 | REQUIRE(tokens[0].cursor_start == 1); 57 | REQUIRE(tokens[0].type == TokenType::PLUS); 58 | REQUIRE(tokens[0].literal == "+"); 59 | REQUIRE(tokens[1].filename == ""); 60 | REQUIRE(tokens[1].line == 1); 61 | REQUIRE(tokens[1].cursor_start == 2); 62 | REQUIRE(tokens[1].type == TokenType::EOF_); 63 | REQUIRE(tokens[1].literal == ""); 64 | } 65 | 66 | TEST_CASE("Minus '-'", "[lexer]") { 67 | std::string filename = ""; 68 | std::string source = "-"; 69 | Lexer lexer{"", 0, "", 1, 1}; 70 | lexer.filename = filename; 71 | lexer.source = source; 72 | auto tokens = Tokenize(lexer); 73 | REQUIRE(tokens.size() == 2); 74 | REQUIRE(tokens[0].filename == ""); 75 | REQUIRE(tokens[0].line == 1); 76 | REQUIRE(tokens[0].cursor_start == 1); 77 | REQUIRE(tokens[0].type == TokenType::MINUS); 78 | REQUIRE(tokens[0].literal == "-"); 79 | REQUIRE(tokens[1].filename == ""); 80 | REQUIRE(tokens[1].line == 1); 81 | REQUIRE(tokens[1].cursor_start == 2); 82 | REQUIRE(tokens[1].type == TokenType::EOF_); 83 | REQUIRE(tokens[1].literal == ""); 84 | } 85 | 86 | TEST_CASE("Left Brace '{'", "[lexer]") { 87 | std::string filename = ""; 88 | std::string source = "{"; 89 | Lexer lexer{"", 0, "", 1, 1}; 90 | lexer.filename = filename; 91 | lexer.source = source; 92 | auto tokens = Tokenize(lexer); 93 | REQUIRE(tokens.size() == 2); 94 | REQUIRE(tokens[0].filename == ""); 95 | REQUIRE(tokens[0].line == 1); 96 | REQUIRE(tokens[0].cursor_start == 1); 97 | REQUIRE(tokens[0].type == TokenType::LEFT_BRACE); 98 | REQUIRE(tokens[0].literal == "{"); 99 | REQUIRE(tokens[1].filename == ""); 100 | REQUIRE(tokens[1].line == 1); 101 | REQUIRE(tokens[1].cursor_start == 2); 102 | REQUIRE(tokens[1].type == TokenType::EOF_); 103 | REQUIRE(tokens[1].literal == ""); 104 | } 105 | 106 | TEST_CASE("Left Bracket '['", "[lexer]") { 107 | std::string filename = ""; 108 | std::string source = "["; 109 | Lexer lexer{"", 0, "", 1, 1}; 110 | lexer.filename = filename; 111 | lexer.source = source; 112 | auto tokens = Tokenize(lexer); 113 | REQUIRE(tokens.size() == 2); 114 | REQUIRE(tokens[0].filename == ""); 115 | REQUIRE(tokens[0].line == 1); 116 | REQUIRE(tokens[0].cursor_start == 1); 117 | REQUIRE(tokens[0].type == TokenType::LEFT_BRACKET); 118 | REQUIRE(tokens[0].literal == "["); 119 | REQUIRE(tokens[1].filename == ""); 120 | REQUIRE(tokens[1].line == 1); 121 | REQUIRE(tokens[1].cursor_start == 2); 122 | REQUIRE(tokens[1].type == TokenType::EOF_); 123 | REQUIRE(tokens[1].literal == ""); 124 | } 125 | 126 | TEST_CASE("Right Brace '}'", "[lexer]") { 127 | std::string filename = ""; 128 | std::string source = "}"; 129 | Lexer lexer{"", 0, "", 1, 1}; 130 | lexer.filename = filename; 131 | lexer.source = source; 132 | auto tokens = Tokenize(lexer); 133 | REQUIRE(tokens.size() == 2); 134 | REQUIRE(tokens[0].filename == ""); 135 | REQUIRE(tokens[0].line == 1); 136 | REQUIRE(tokens[0].cursor_start == 1); 137 | REQUIRE(tokens[0].type == TokenType::RIGHT_BRACE); 138 | REQUIRE(tokens[0].literal == "}"); 139 | REQUIRE(tokens[1].filename == ""); 140 | REQUIRE(tokens[1].line == 1); 141 | REQUIRE(tokens[1].cursor_start == 2); 142 | REQUIRE(tokens[1].type == TokenType::EOF_); 143 | REQUIRE(tokens[1].literal == ""); 144 | } 145 | 146 | TEST_CASE("Right Bracket ']'", "[lexer]") { 147 | std::string filename = ""; 148 | std::string source = "]"; 149 | Lexer lexer{"", 0, "", 1, 1}; 150 | lexer.filename = filename; 151 | lexer.source = source; 152 | auto tokens = Tokenize(lexer); 153 | REQUIRE(tokens.size() == 2); 154 | REQUIRE(tokens[0].filename == ""); 155 | REQUIRE(tokens[0].line == 1); 156 | REQUIRE(tokens[0].cursor_start == 1); 157 | REQUIRE(tokens[0].type == TokenType::RIGHT_BRACKET); 158 | REQUIRE(tokens[0].literal == "]"); 159 | REQUIRE(tokens[1].filename == ""); 160 | REQUIRE(tokens[1].line == 1); 161 | REQUIRE(tokens[1].cursor_start == 2); 162 | REQUIRE(tokens[1].type == TokenType::EOF_); 163 | REQUIRE(tokens[1].literal == ""); 164 | } 165 | 166 | TEST_CASE("Unexpected punctuation", "[lexer]") { 167 | std::string filename = ""; 168 | std::string source = R"({"a": 1))"; 169 | Lexer lexer{"", 0, "", 1, 1}; 170 | lexer.filename = filename; 171 | lexer.source = source; 172 | lexer.silent_mode = true; 173 | bool exception_thrown = false; 174 | try { 175 | auto tokens = Tokenize(lexer); 176 | } catch (std::runtime_error &e) { 177 | exception_thrown = true; 178 | } 179 | auto errors = lexer.errors; 180 | REQUIRE(errors.size() == 1); 181 | REQUIRE(std::get<2>(errors[0]) == std::string{"Failed to parse punctuation"}); 182 | REQUIRE(std::get<3>(errors[0]) == std::string{"Unexpected token ')'"}); 183 | } 184 | -------------------------------------------------------------------------------- /test/lexer/string.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | using namespace jsonlint; 5 | 6 | TEST_CASE("String \"Hello World\"", "[lexer]") { 7 | std::string filename = ""; 8 | std::string source = "\"Hello World\""; 9 | Lexer lexer{"", 0, "", 1, 1}; 10 | lexer.filename = filename; 11 | lexer.source = source; 12 | auto tokens = Tokenize(lexer); 13 | REQUIRE(tokens.size() == 2); 14 | REQUIRE(tokens[0].filename == ""); 15 | REQUIRE(tokens[0].line == 1); 16 | REQUIRE(tokens[0].cursor_start == 1); 17 | REQUIRE(tokens[0].type == TokenType::STRING); 18 | REQUIRE(tokens[0].literal == "Hello World"); 19 | REQUIRE(tokens[1].filename == ""); 20 | REQUIRE(tokens[1].line == 1); 21 | REQUIRE(tokens[1].cursor_start == 14); 22 | REQUIRE(tokens[1].type == TokenType::EOF_); 23 | REQUIRE(tokens[1].literal == ""); 24 | } 25 | 26 | TEST_CASE("String \"Hello 世界\"", "[lexer]") { 27 | setlocale(LC_ALL, ""); 28 | std::string filename = ""; 29 | std::string source = "\"Hello 世界\""; 30 | Lexer lexer{"", 0, "", 1, 1}; 31 | lexer.filename = filename; 32 | lexer.source = source; 33 | auto tokens = Tokenize(lexer); 34 | REQUIRE(tokens.size() == 2); 35 | REQUIRE(tokens[0].filename == ""); 36 | REQUIRE(tokens[0].line == 1); 37 | REQUIRE(tokens[0].cursor_start == 1); 38 | REQUIRE(tokens[0].type == TokenType::STRING); 39 | REQUIRE(tokens[0].literal == "Hello 世界"); 40 | REQUIRE(tokens[1].filename == ""); 41 | REQUIRE(tokens[1].line == 1); 42 | REQUIRE(tokens[1].cursor_start == 11); 43 | REQUIRE(tokens[1].type == TokenType::EOF_); 44 | REQUIRE(tokens[1].literal == ""); 45 | } 46 | 47 | TEST_CASE("String \"ο Δίας\"", "[lexer]") { 48 | setlocale(LC_ALL, ""); 49 | std::string filename = ""; 50 | std::string source = "\"ο Δίας\""; 51 | Lexer lexer{"", 0, "", 1, 1}; 52 | lexer.filename = filename; 53 | lexer.source = source; 54 | auto tokens = Tokenize(lexer); 55 | REQUIRE(tokens.size() == 2); 56 | REQUIRE(tokens[0].filename == ""); 57 | REQUIRE(tokens[0].line == 1); 58 | REQUIRE(tokens[0].cursor_start == 1); 59 | REQUIRE(tokens[0].type == TokenType::STRING); 60 | REQUIRE(tokens[0].literal == "ο Δίας"); 61 | REQUIRE(tokens[1].filename == ""); 62 | REQUIRE(tokens[1].line == 1); 63 | REQUIRE(tokens[1].cursor_start == 9); 64 | REQUIRE(tokens[1].type == TokenType::EOF_); 65 | REQUIRE(tokens[1].literal == ""); 66 | } 67 | 68 | TEST_CASE("Unterminated string - detected newline", "[lexer]") { 69 | std::string filename = ""; 70 | std::string source = R"("AB 71 | CD")"; 72 | Lexer lexer{"", 0, "", 1, 1}; 73 | lexer.filename = filename; 74 | lexer.source = source; 75 | lexer.silent_mode = true; 76 | bool exception_thrown = false; 77 | try { 78 | auto tokens = Tokenize(lexer); 79 | } catch (std::runtime_error &e) { 80 | exception_thrown = true; 81 | } 82 | auto errors = lexer.errors; 83 | REQUIRE(errors.size() == 1); 84 | REQUIRE(std::get<2>(errors[0]) == std::string{"Failed to parse string"}); 85 | REQUIRE(std::get<3>(errors[0]) == std::string{"Unterminated string"}); 86 | } 87 | 88 | TEST_CASE("Unterminated string - reached EOF", "[lexer]") { 89 | std::string filename = ""; 90 | std::string source = R"("ABCD)"; 91 | Lexer lexer{"", 0, "", 1, 1}; 92 | lexer.filename = filename; 93 | lexer.source = source; 94 | lexer.silent_mode = true; 95 | bool exception_thrown = false; 96 | try { 97 | auto tokens = Tokenize(lexer); 98 | } catch (std::runtime_error &e) { 99 | exception_thrown = true; 100 | } 101 | auto errors = lexer.errors; 102 | REQUIRE(errors.size() == 1); 103 | REQUIRE(std::get<2>(errors[0]) == std::string{"Failed to parse string"}); 104 | REQUIRE(std::get<3>(errors[0]) == std::string{"Unterminated string"}); 105 | } 106 | 107 | TEST_CASE("Incorrect unicode escape sequence", "[lexer]") { 108 | std::string filename = ""; 109 | std::string source = R"("\uABCG")"; 110 | Lexer lexer{"", 0, "", 1, 1}; 111 | lexer.filename = filename; 112 | lexer.source = source; 113 | lexer.silent_mode = true; 114 | bool exception_thrown = false; 115 | try { 116 | auto tokens = Tokenize(lexer); 117 | } catch (std::runtime_error &e) { 118 | exception_thrown = true; 119 | } 120 | auto errors = lexer.errors; 121 | REQUIRE(errors.size() == 1); 122 | REQUIRE(std::get<2>(errors[0]) == std::string{"Failed to parse unicode escape sequence"}); 123 | REQUIRE(std::get<3>(errors[0]) == std::string{"Expected hex character, instead got 'G'"}); 124 | } 125 | -------------------------------------------------------------------------------- /test/lexer/whitespace.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | using namespace jsonlint; 5 | 6 | TEST_CASE("Whitespace ' '", "[lexer]") { 7 | std::string filename = ""; 8 | std::string source = " "; 9 | Lexer lexer{"", 0, "", 1, 1}; 10 | lexer.filename = filename; 11 | lexer.source = source; 12 | auto tokens = Tokenize(lexer); 13 | REQUIRE(tokens.size() == 1); 14 | REQUIRE(tokens[0].filename == ""); 15 | REQUIRE(tokens[0].line == 1); 16 | REQUIRE(tokens[0].cursor_start == 2); 17 | REQUIRE(tokens[0].type == TokenType::EOF_); 18 | REQUIRE(tokens[0].literal == ""); 19 | } 20 | 21 | TEST_CASE("Whitespace ' '", "[lexer]") { 22 | std::string filename = ""; 23 | std::string source = " "; 24 | Lexer lexer{"", 0, "", 1, 1}; 25 | lexer.filename = filename; 26 | lexer.source = source; 27 | auto tokens = Tokenize(lexer); 28 | REQUIRE(tokens.size() == 1); 29 | REQUIRE(tokens[0].filename == ""); 30 | REQUIRE(tokens[0].line == 1); 31 | REQUIRE(tokens[0].cursor_start == 5); 32 | REQUIRE(tokens[0].type == TokenType::EOF_); 33 | REQUIRE(tokens[0].literal == ""); 34 | } 35 | -------------------------------------------------------------------------------- /test/main.cpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_MAIN 2 | #include 3 | -------------------------------------------------------------------------------- /test/parser/array.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | using namespace jsonlint; 6 | 7 | TEST_CASE("The parser can parse the array [1, 2, 3]", "[parser]") { 8 | std::string filename = ""; 9 | std::string source = "[1, 2, 3]"; 10 | Lexer lexer{"", 0, "", 1, 1}; 11 | lexer.filename = filename; 12 | lexer.source = source; 13 | auto tokens = Tokenize(lexer); 14 | Parser parser(tokens, source); 15 | parser.silent_mode = true; 16 | auto result = parser.ParseJson(); 17 | REQUIRE(result); 18 | } 19 | 20 | TEST_CASE("The parser can detect the trailing comma in [1, 2, 3,]", "[parser]") { 21 | std::string filename = ""; 22 | std::string source = "[1, 2, 3,]"; 23 | Lexer lexer{"", 0, "", 1, 1}; 24 | lexer.filename = filename; 25 | lexer.source = source; 26 | auto tokens = Tokenize(lexer); 27 | Parser parser(tokens, source); 28 | parser.silent_mode = true; 29 | bool exception_thrown = false; 30 | try { 31 | auto result = parser.ParseJson(); 32 | } catch (std::runtime_error &e) { 33 | exception_thrown = true; 34 | } 35 | auto errors = parser.errors; 36 | REQUIRE(errors.size() == 1); 37 | REQUIRE(std::get<2>(errors[0]) == std::string{"Failed to parse array"}); 38 | REQUIRE(std::get<3>(errors[0]) == 39 | std::string{"Expected ']', instead got ','"}); 40 | } 41 | 42 | TEST_CASE("The parser can parse the array [1, 3.14, true, \"Hello\"]", "[parser]") { 43 | std::string filename = ""; 44 | std::string source = "[1, 3.14, true, \"Hello\"]"; 45 | Lexer lexer{"", 0, "", 1, 1}; 46 | lexer.filename = filename; 47 | lexer.source = source; 48 | auto tokens = Tokenize(lexer); 49 | Parser parser(tokens, source); 50 | parser.silent_mode = true; 51 | auto result = parser.ParseJson(); 52 | REQUIRE(result); 53 | } 54 | 55 | TEST_CASE("The parser can parse the array [1, 3.14, true, \"Hello\", [4, 5, 6]]", "[parser]") { 56 | std::string filename = ""; 57 | std::string source = "[1, 3.14, true, \"Hello\", [4, 5, 6]]"; 58 | Lexer lexer{"", 0, "", 1, 1}; 59 | lexer.filename = filename; 60 | lexer.source = source; 61 | auto tokens = Tokenize(lexer); 62 | Parser parser(tokens, source); 63 | parser.silent_mode = true; 64 | auto result = parser.ParseJson(); 65 | REQUIRE(result); 66 | } 67 | 68 | TEST_CASE("The parser can parse the array [{\"a\": 1, \"b\": [2, 3]}, {\"c\": true, \"d\": null}]", 69 | "[parser]") { 70 | std::string filename = ""; 71 | std::string source = "[{\"a\": 1, \"b\": [2, 3]}, {\"c\": true, \"d\": null}]"; 72 | Lexer lexer{"", 0, "", 1, 1}; 73 | lexer.filename = filename; 74 | lexer.source = source; 75 | auto tokens = Tokenize(lexer); 76 | Parser parser(tokens, source); 77 | parser.silent_mode = true; 78 | auto result = parser.ParseJson(); 79 | REQUIRE(result); 80 | } 81 | -------------------------------------------------------------------------------- /test/parser/number.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | using namespace jsonlint; 6 | 7 | TEST_CASE("The parser can parse the number '0'", "[parser]") { 8 | std::string filename = ""; 9 | std::string source = "0"; 10 | Lexer lexer{"", 0, "", 1, 1}; 11 | lexer.filename = filename; 12 | lexer.source = source; 13 | auto tokens = Tokenize(lexer); 14 | Parser parser(tokens, source); 15 | parser.silent_mode = true; 16 | auto result = parser.ParseJson(); 17 | REQUIRE(result); 18 | } 19 | 20 | TEST_CASE("The parser can parse the number '-1'", "[parser]") { 21 | std::string filename = ""; 22 | std::string source = "-1"; 23 | Lexer lexer{"", 0, "", 1, 1}; 24 | lexer.filename = filename; 25 | lexer.source = source; 26 | auto tokens = Tokenize(lexer); 27 | Parser parser(tokens, source); 28 | parser.silent_mode = true; 29 | auto result = parser.ParseJson(); 30 | REQUIRE(result); 31 | } 32 | 33 | TEST_CASE("The parser can parse the number '+2'", "[parser]") { 34 | std::string filename = ""; 35 | std::string source = "+2"; 36 | Lexer lexer{"", 0, "", 1, 1}; 37 | lexer.filename = filename; 38 | lexer.source = source; 39 | auto tokens = Tokenize(lexer); 40 | Parser parser(tokens, source); 41 | parser.silent_mode = true; 42 | auto result = parser.ParseJson(); 43 | REQUIRE(result); 44 | } 45 | 46 | TEST_CASE("The parser can parse the number '123E-4'", "[parser]") { 47 | std::string filename = ""; 48 | std::string source = "123E-4"; 49 | Lexer lexer{"", 0, "", 1, 1}; 50 | lexer.filename = filename; 51 | lexer.source = source; 52 | auto tokens = Tokenize(lexer); 53 | Parser parser(tokens, source); 54 | parser.silent_mode = true; 55 | auto result = parser.ParseJson(); 56 | REQUIRE(result); 57 | } 58 | -------------------------------------------------------------------------------- /test/parser/object.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | using namespace jsonlint; 6 | 7 | TEST_CASE("The parser can parse the object {\"a\": 2}", "[parser]") { 8 | std::string filename = ""; 9 | std::string source = "{\"a\": 2}"; 10 | Lexer lexer{"", 0, "", 1, 1}; 11 | lexer.filename = filename; 12 | lexer.source = source; 13 | auto tokens = Tokenize(lexer); 14 | Parser parser(tokens, source); 15 | parser.silent_mode = true; 16 | auto result = parser.ParseJson(); 17 | REQUIRE(result); 18 | } 19 | 20 | TEST_CASE("The parser can parse the object {\"a\": \"Hello\"}", "[parser]") { 21 | std::string filename = ""; 22 | std::string source = "{\"a\": \"Hello\"}"; 23 | Lexer lexer{"", 0, "", 1, 1}; 24 | lexer.filename = filename; 25 | lexer.source = source; 26 | auto tokens = Tokenize(lexer); 27 | Parser parser(tokens, source); 28 | parser.silent_mode = true; 29 | auto result = parser.ParseJson(); 30 | REQUIRE(result); 31 | } 32 | 33 | TEST_CASE("The parser can detect duplicate keys in {\"a\": 1, \"b\": 2, \"a\": 3, \"c\": 4}", 34 | "[parser]") { 35 | std::string filename = ""; 36 | std::string source = "{\"a\": 1, \"b\": 2, \"a\": 3, \"c\": 4}"; 37 | Lexer lexer{"", 0, "", 1, 1}; 38 | lexer.filename = filename; 39 | lexer.source = source; 40 | auto tokens = Tokenize(lexer); 41 | Parser parser(tokens, source); 42 | parser.silent_mode = true; 43 | bool exception_thrown = false; 44 | try { 45 | auto result = parser.ParseJson(); 46 | } catch (std::runtime_error &e) { 47 | exception_thrown = true; 48 | } 49 | auto errors = parser.errors; 50 | REQUIRE(errors.size() == 1); 51 | REQUIRE(std::get<2>(errors[0]) == std::string{"Failed to parse object"}); 52 | REQUIRE(std::get<3>(errors[0]) == std::string{"Duplicate key 'a'"}); 53 | } 54 | -------------------------------------------------------------------------------- /test/parser/string.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | using namespace jsonlint; 6 | 7 | TEST_CASE("The parser can parse the number 'Hello World'", "[parser]") { 8 | std::string filename = ""; 9 | std::string source = "\"Hello World\""; 10 | Lexer lexer{"", 0, "", 1, 1}; 11 | lexer.filename = filename; 12 | lexer.source = source; 13 | auto tokens = Tokenize(lexer); 14 | Parser parser(tokens, source); 15 | parser.silent_mode = true; 16 | auto result = parser.ParseJson(); 17 | REQUIRE(result); 18 | } 19 | 20 | TEST_CASE("The parser can parse the number '\uABCD'", "[parser]") { 21 | std::string filename = ""; 22 | std::string source = "\"\uABCD\""; 23 | Lexer lexer{"", 0, "", 1, 1}; 24 | lexer.filename = filename; 25 | lexer.source = source; 26 | auto tokens = Tokenize(lexer); 27 | Parser parser(tokens, source); 28 | parser.silent_mode = true; 29 | auto result = parser.ParseJson(); 30 | REQUIRE(result); 31 | } 32 | --------------------------------------------------------------------------------