From 1e07963d99071fdc6576dbceac60dfd586ba603c Mon Sep 17 00:00:00 2001 From: v Date: Sun, 8 Mar 2026 15:34:54 +0000 Subject: [PATCH 1/9] Migrate to .NET 10 and modernize build infrastructure - Migrate all projects to .NET 10 (Hjson, CLI, sample, and tests) - Replace legacy .sln files with modern .slnx format - Update project file structure for net10 target framework - Remove legacy build system (Travis CI, build-mono script) - Add GitHub Actions CI/CD workflows (ci.yml, format.yml, release.yml) - Add developer container configuration (.devcontainer) - Add EditorConfig and VS Code workspace settings - Major refactoring of core library (HjsonReader, JsonWriter, etc.) - Add new converter API (HjsonConvert, HjsonAttributes) - Rewrite test suite with proper unit tests (HjsonConvertTests, ParserTests) - Simplify CLI and sample applications --- .devcontainer/devcontainer.json | 18 + .editorconfig | 389 ++++++++++++++++++ .github/workflows/ci.yml | 28 ++ .github/workflows/format.yml | 22 + .github/workflows/release.yml | 28 ++ .travis.yml | 6 - .vscode/settings.json | 10 + Hjson.nuspec | 4 +- Hjson.sln | 22 - Hjson.slnx | 5 + Hjson/BaseReader.cs | 393 +++++++++--------- Hjson/Hjson.csproj | 2 +- Hjson/HjsonAttributes.cs | 27 ++ Hjson/HjsonConvert.cs | 325 +++++++++++++++ Hjson/HjsonDsf.cs | 224 +++++------ Hjson/HjsonOptions.cs | 21 +- Hjson/HjsonReader.cs | 672 +++++++++++++++---------------- Hjson/HjsonValue.cs | 93 +++-- Hjson/HjsonWriter.cs | 409 +++++++++---------- Hjson/HjsonWsc.cs | 39 +- Hjson/IJsonReader.cs | 9 +- Hjson/JsonArray.cs | 92 ++--- Hjson/JsonObject.cs | 130 ++---- Hjson/JsonPrimitive.cs | 100 ++--- Hjson/JsonReader.cs | 158 ++++---- Hjson/JsonType.cs | 11 +- Hjson/JsonUtil.cs | 223 ++++------ Hjson/JsonValue.cs | 180 ++++----- Hjson/JsonWriter.cs | 199 +++++---- Hjson/Properties/AssemblyInfo.cs | 2 +- Hjsonc.sln | 28 -- README.md | 8 +- Test.sln | 28 -- build-core | 2 +- build-mono | 22 - cli/Program.cs | 90 ++--- cli/cli.csproj | 2 +- legacy/Hjson.sln | 34 -- legacy/Hjson/Hjson.csproj | 73 ---- legacy/HjsonSample.sln | 22 - legacy/cli/Hjsonc.csproj | 62 --- legacy/sample/HjsonSample.csproj | 72 ---- legacy/sample/packages.config | 4 - legacy/test/Test.csproj | 67 --- sample/HjsonSample.sln | 22 - sample/HjsonSample.slnx | 3 + sample/Program.cs | 65 ++- sample/sample.csproj | 2 +- test/HjsonConvertTests.cs | 624 ++++++++++++++++++++++++++++ test/ParserTests.cs | 143 +++++++ test/Program.cs | 112 ------ test/assets/kan_result.hjson | 2 +- test/assets/kan_result.json | 2 +- test/test.csproj | 19 +- 54 files changed, 3059 insertions(+), 2290 deletions(-) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .editorconfig create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/format.yml create mode 100644 .github/workflows/release.yml delete mode 100644 .travis.yml create mode 100644 .vscode/settings.json delete mode 100644 Hjson.sln create mode 100644 Hjson.slnx create mode 100644 Hjson/HjsonAttributes.cs create mode 100644 Hjson/HjsonConvert.cs delete mode 100644 Hjsonc.sln delete mode 100644 Test.sln delete mode 100755 build-mono delete mode 100644 legacy/Hjson.sln delete mode 100644 legacy/Hjson/Hjson.csproj delete mode 100644 legacy/HjsonSample.sln delete mode 100644 legacy/cli/Hjsonc.csproj delete mode 100644 legacy/sample/HjsonSample.csproj delete mode 100644 legacy/sample/packages.config delete mode 100644 legacy/test/Test.csproj delete mode 100644 sample/HjsonSample.sln create mode 100644 sample/HjsonSample.slnx create mode 100644 test/HjsonConvertTests.cs create mode 100644 test/ParserTests.cs delete mode 100644 test/Program.cs diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..b6a8cd2 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,18 @@ +{ + "name": "C# (.NET 10)", + "image": "mcr.microsoft.com/dotnet/sdk:10.0", + "updateRemoteUserUID": true, + "workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/${localWorkspaceFolderBasename},type=bind,Z", + "runArgs": [ + "--userns=keep-id" + ], + "customizations": { + "vscode": { + "extensions": [ + "ms-dotnettools.csharp", + "ms-dotnettools.vscode-dotnet-runtime", + // "ms-dotnettools.csdevkit" + ] + } + } +} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..bb400d4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,389 @@ +root = true + +# All files +[*] +indent_style = space + +# Xml files +[*.xml] +indent_size = 2 + +# Xml project files +[*.{csproj,fsproj,vbproj,proj,slnx}] +indent_size = 2 + +# Xml config files +[*.{props,targets,config,nuspec}] +indent_size = 2 + +[*.json] +indent_size = 2 + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +tab_width = 4 + +# New line preferences +insert_final_newline = false + +#### .NET Coding Conventions #### +[*.{cs,vb}] + +# Organize usings +dotnet_separate_import_directive_groups = true +dotnet_sort_system_directives_first = true +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false:silent +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_property = false:silent + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent + +# Expression-level preferences +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_object_initializer = true:suggestion +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_collection_expression = true:warning +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = true:suggestion +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion + +# Field preferences +dotnet_style_readonly_field = true:warning + +# Parameter preferences +dotnet_code_quality_unused_parameters = all:suggestion + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +#### C# Coding Conventions #### +[*.cs] + +# var preferences +csharp_style_var_elsewhere = false:silent +csharp_style_var_for_built_in_types = false:silent +csharp_style_var_when_type_is_apparent = false:silent + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:suggestion +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_pattern_matching = true:silent +csharp_style_prefer_switch_expression = true:suggestion + +# Null-checking preferences +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier preferences +csharp_prefer_static_anonymous_function = true:suggestion +csharp_prefer_static_local_function = true:warning +csharp_preferred_modifier_order = public,private,protected,internal,file,const,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:suggestion +csharp_style_prefer_readonly_struct = true:suggestion +csharp_style_prefer_readonly_struct_member = true:suggestion + +# Code-block preferences +csharp_prefer_braces = true:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = file_scoped:suggestion +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_primary_constructors = true:warning +csharp_style_prefer_top_level_statements = true:silent + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:silent + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### +[*.{cs,vb}] + +# Naming rules + +dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces +dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion +dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces +dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase + +dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion +dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters +dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase + +dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods +dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties +dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.events_should_be_pascalcase.symbols = events +dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion +dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables +dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase + +dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion +dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants +dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase + +dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion +dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters +dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase + +dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields +dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion +dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields +dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase + +dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = warning +dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields +dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase + +dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields +dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields +dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields +dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields +dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums +dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions +dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase + +dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase + +# Symbol specifications + +dotnet_naming_symbols.interfaces.applicable_kinds = interface +dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interfaces.required_modifiers = + +dotnet_naming_symbols.enums.applicable_kinds = enum +dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.enums.required_modifiers = + +dotnet_naming_symbols.events.applicable_kinds = event +dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.events.required_modifiers = + +dotnet_naming_symbols.methods.applicable_kinds = method +dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.methods.required_modifiers = + +dotnet_naming_symbols.properties.applicable_kinds = property +dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.properties.required_modifiers = + +dotnet_naming_symbols.public_fields.applicable_kinds = field +dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal +dotnet_naming_symbols.public_fields.required_modifiers = + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_fields.required_modifiers = + +dotnet_naming_symbols.private_static_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_static_fields.required_modifiers = static + +dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum +dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types_and_namespaces.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +dotnet_naming_symbols.type_parameters.applicable_kinds = namespace +dotnet_naming_symbols.type_parameters.applicable_accessibilities = * +dotnet_naming_symbols.type_parameters.required_modifiers = + +dotnet_naming_symbols.private_constant_fields.applicable_kinds = field +dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_constant_fields.required_modifiers = const + +dotnet_naming_symbols.local_variables.applicable_kinds = local +dotnet_naming_symbols.local_variables.applicable_accessibilities = local +dotnet_naming_symbols.local_variables.required_modifiers = + +dotnet_naming_symbols.local_constants.applicable_kinds = local +dotnet_naming_symbols.local_constants.applicable_accessibilities = local +dotnet_naming_symbols.local_constants.required_modifiers = const + +dotnet_naming_symbols.parameters.applicable_kinds = parameter +dotnet_naming_symbols.parameters.applicable_accessibilities = * +dotnet_naming_symbols.parameters.required_modifiers = + +dotnet_naming_symbols.public_constant_fields.applicable_kinds = field +dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal +dotnet_naming_symbols.public_constant_fields.required_modifiers = const + +dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal +dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static + +dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static + +dotnet_naming_symbols.local_functions.applicable_kinds = local_function +dotnet_naming_symbols.local_functions.applicable_accessibilities = * +dotnet_naming_symbols.local_functions.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascalcase.required_prefix = +dotnet_naming_style.pascalcase.required_suffix = +dotnet_naming_style.pascalcase.word_separator = +dotnet_naming_style.pascalcase.capitalization = pascal_case + +dotnet_naming_style.ipascalcase.required_prefix = I +dotnet_naming_style.ipascalcase.required_suffix = +dotnet_naming_style.ipascalcase.word_separator = +dotnet_naming_style.ipascalcase.capitalization = pascal_case + +dotnet_naming_style.tpascalcase.required_prefix = T +dotnet_naming_style.tpascalcase.required_suffix = +dotnet_naming_style.tpascalcase.word_separator = +dotnet_naming_style.tpascalcase.capitalization = pascal_case + +dotnet_naming_style._camelcase.required_prefix = _ +dotnet_naming_style._camelcase.required_suffix = +dotnet_naming_style._camelcase.word_separator = +dotnet_naming_style._camelcase.capitalization = camel_case + +dotnet_naming_style.camelcase.required_prefix = +dotnet_naming_style.camelcase.required_suffix = +dotnet_naming_style.camelcase.word_separator = +dotnet_naming_style.camelcase.capitalization = camel_case + +dotnet_naming_style.s_camelcase.required_prefix = s_ +dotnet_naming_style.s_camelcase.required_suffix = +dotnet_naming_style.s_camelcase.word_separator = +dotnet_naming_style.s_camelcase.capitalization = camel_case + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5782b40 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI + +on: + push: + branches: [ master, main ] + pull_request: + branches: [ master, main ] + +jobs: + build-and-test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' # Uses latest stable .NET + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build --configuration Release --no-restore + + - name: Test + run: dotnet test test/test.csproj --configuration Release --no-build --verbosity normal diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 0000000..437d6be --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,22 @@ +name: Format Check + +on: + push: + branches: [ master, main ] + pull_request: + branches: [ master, main ] + +jobs: + format: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Format Check + run: dotnet format --verify-no-changes diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..9e8c016 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,28 @@ +name: Release + +on: + push: + tags: + - 'v*' # Trigger on tags like v1.0.0 + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Restore dependencies + run: dotnet restore Hjson/Hjson.csproj + + - name: Pack Hjson + run: dotnet pack Hjson/Hjson.csproj --configuration Release --no-restore -o ./artifacts + + # Uncomment this when you are ready to publish! + # - name: Push to NuGet + # run: dotnet nuget push ./artifacts/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 35cabb5..0000000 --- a/.travis.yml +++ /dev/null @@ -1,6 +0,0 @@ -language: csharp -mono: 4.0.0 -dotnet: 2.0.0 -script: - - ./build-core - - ./build-mono diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..bc54960 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "[csharp]": { + "editor.defaultFormatter": "ms-dotnettools.csharp", + "editor.formatOnSave": true, + "editor.formatOnType": true, + "editor.codeActionsOnSave": { + "source.fixAll.csharp": "explicit" + } + } +} \ No newline at end of file diff --git a/Hjson.nuspec b/Hjson.nuspec index ae5baee..e3b6b4c 100644 --- a/Hjson.nuspec +++ b/Hjson.nuspec @@ -7,9 +7,9 @@ Christian Zangl Christian Zangl https://github.com/laktak/hjson-cs/blob/master/LICENSE - http://hjson.org + http://hjson.github.io false - Hjson, a user interface for JSON. Relaxed syntax, fewer mistakes, more comments. Supports .NET Core, .NET 4.x and Mono. For details go to http://hjson.org. + Hjson, a user interface for JSON. Relaxed syntax, fewer mistakes, more comments. Supports .NET Core, .NET 4.x and Mono. For details go to http://hjson.github.io. Copyright Christian Zangl JSON comments config hjson parser serializer diff --git a/Hjson.sln b/Hjson.sln deleted file mode 100644 index 53ead1f..0000000 --- a/Hjson.sln +++ /dev/null @@ -1,22 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26114.2 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hjson", "Hjson\Hjson.csproj", "{FF9E2637-8BD3-4F8D-B563-D105B10D5354}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {FF9E2637-8BD3-4F8D-B563-D105B10D5354}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FF9E2637-8BD3-4F8D-B563-D105B10D5354}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FF9E2637-8BD3-4F8D-B563-D105B10D5354}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FF9E2637-8BD3-4F8D-B563-D105B10D5354}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/Hjson.slnx b/Hjson.slnx new file mode 100644 index 0000000..2e87ed8 --- /dev/null +++ b/Hjson.slnx @@ -0,0 +1,5 @@ + + + + + diff --git a/Hjson/BaseReader.cs b/Hjson/BaseReader.cs index 352bf08..19a2ad7 100644 --- a/Hjson/BaseReader.cs +++ b/Hjson/BaseReader.cs @@ -6,282 +6,287 @@ using System.Linq; using System.Text; -namespace Hjson +namespace Hjson; + +internal abstract class BaseReader { - internal abstract class BaseReader - { - string buffer; + readonly string buffer; TextReader r; - StringBuilder sb=new StringBuilder(); - StringBuilder white=new StringBuilder(); + readonly StringBuilder sb = new(); + readonly StringBuilder white = new(); // peek could be removed since we now use a buffer - List peek=new List(); + readonly List peek = []; bool prevLf; public int Line { get; private set; } public int Column { get; private set; } protected IJsonReader Reader { get; private set; } - protected bool HasReader { get { return Reader!=null; } } + protected bool HasReader => Reader != null; public bool ReadWsc { get; set; } public BaseReader(TextReader reader, IJsonReader jsonReader) { - if (reader==null) throw new ArgumentNullException("reader"); - // use a buffer so we can support reset - this.Reader=jsonReader; - buffer=reader.ReadToEnd(); - Reset(); + ArgumentNullException.ThrowIfNull(reader); + // use a buffer so we can support reset + Reader = jsonReader; + buffer = reader.ReadToEnd(); + Reset(); } public void Reset() { - Line=1; - this.r=new StringReader(buffer); - peek.Clear(); - white.Length=sb.Length=0; - prevLf=false; + Line = 1; + this.r = new StringReader(buffer); + peek.Clear(); + white.Length = sb.Length = 0; + prevLf = false; } - public int PeekChar(int idx=0) + public int PeekChar(int idx = 0) { - if (idx<0) throw new ArgumentOutOfRangeException(); - while (idx>=peek.Count) - { - int c=r.Read(); - if (c<0) return c; - peek.Add(c); - } - return peek[idx]; + if (idx < 0) throw new ArgumentOutOfRangeException(); + while (idx >= peek.Count) + { + int c = r.Read(); + if (c < 0) return c; + peek.Add(c); + } + return peek[idx]; } public virtual int SkipPeekChar() { - SkipWhite(); - return PeekChar(); + SkipWhite(); + return PeekChar(); } public int ReadChar() { - int v; - if (peek.Count>0) - { - // normally peek will only hold not more than one character so this should not matter for performance - v=peek[0]; - peek.RemoveAt(0); - } - else v=r.Read(); - - if (ReadWsc && v!='\r') white.Append((char)v); - - if (prevLf) - { - Line++; - Column=0; - prevLf=false; - } - - if (v=='\n') prevLf=true; - Column++; - - return v; + int v; + if (peek.Count > 0) + { + // normally peek will only hold not more than one character so this should not matter for performance + v = peek[0]; + peek.RemoveAt(0); + } + else v = r.Read(); + + if (ReadWsc && v != '\r') white.Append((char)v); + + if (prevLf) + { + Line++; + Column = 0; + prevLf = false; + } + + if (v == '\n') prevLf = true; + Column++; + + return v; } protected void ResetWhite() { - if (ReadWsc) white.Length=0; + if (ReadWsc) white.Length = 0; } protected virtual string GetWhite() { - if (!ReadWsc) throw new InvalidOperationException(); - return white.ToString(); + if (!ReadWsc) throw new InvalidOperationException(); + return white.ToString(); } public static bool IsWhite(char c) { - return c==' ' || c=='\t' || c=='\r' || c=='\n'; + return c == ' ' || c == '\t' || c == '\r' || c == '\n'; } public void SkipWhite() { - for (; ; ) - { - if (IsWhite((char)PeekChar())) ReadChar(); - else break; - } + for (; ; ) + { + if (IsWhite((char)PeekChar())) ReadChar(); + else break; + } } // It could return either long or double, depending on the parsed value. public JsonValue ReadNumericLiteral() { - int c, leadingZeros=0; - double val=0; - bool negative=false, testLeading=true; - - if (PeekChar()=='-') - { - negative=true; - ReadChar(); - if (PeekChar()<0) throw ParseError("Invalid JSON numeric literal; extra negation"); - } - - for (int x=0; ; x++) - { - c=PeekChar(); - if (c<'0' || c>'9') break; - if (testLeading) + int c, leadingZeros = 0; + bool testLeading = true; + var numStr = new StringBuilder(); + + if (PeekChar() == '-') { - if (c=='0') leadingZeros++; - else testLeading = false; + numStr.Append('-'); + ReadChar(); + if (PeekChar() < 0) throw ParseError("Invalid JSON numeric literal; extra negation"); } - val=val*10+(c-'0'); - ReadChar(); - } - if (testLeading) leadingZeros--; // single 0 is allowed - if (leadingZeros>0) throw ParseError("leading multiple zeros are not allowed"); - - // fraction - if (PeekChar()=='.') - { - int fdigits=0; - double frac=0; - ReadChar(); - if (PeekChar()<0) throw ParseError("Invalid JSON numeric literal; extra dot"); - double d=10; - for (; ; ) + + for (int x = 0; ; x++) { - c=PeekChar(); - if (c<'0' || '9' '9') break; + if (testLeading) + { + if (c == '0') leadingZeros++; + else testLeading = false; + } + numStr.Append((char)c); + ReadChar(); } - if (fdigits==0) throw ParseError("Invalid JSON numeric literal; extra dot"); - val+=frac; - } + if (testLeading) leadingZeros--; // single 0 is allowed + if (leadingZeros > 0) throw ParseError("leading multiple zeros are not allowed"); - c=PeekChar(); - if (c=='e' || c=='E') - { - // exponent - int exp=0, expSign=1; + bool hasFracOrExp = false; - ReadChar(); - if (PeekChar()<0) throw new ArgumentException("Invalid JSON numeric literal; incomplete exponent"); - - c=PeekChar(); - if (c=='-') + // fraction + if (PeekChar() == '.') { - ReadChar(); - expSign=-1; + hasFracOrExp = true; + int fdigits = 0; + numStr.Append('.'); + ReadChar(); + if (PeekChar() < 0) throw ParseError("Invalid JSON numeric literal; extra dot"); + for (; ; ) + { + c = PeekChar(); + if (c < '0' || '9' < c) break; + numStr.Append((char)c); + ReadChar(); + fdigits++; + } + if (fdigits == 0) throw ParseError("Invalid JSON numeric literal; extra dot"); } - else if (c=='+') ReadChar(); - if (PeekChar()<0) throw ParseError("Invalid JSON numeric literal; incomplete exponent"); - - for (; ; ) + c = PeekChar(); + if (c == 'e' || c == 'E') { - c=PeekChar(); - if (c<'0' || c>'9') break; - exp=exp*10+(c-'0'); - ReadChar(); + hasFracOrExp = true; + numStr.Append((char)c); + ReadChar(); + if (PeekChar() < 0) throw new ArgumentException("Invalid JSON numeric literal; incomplete exponent"); + + c = PeekChar(); + if (c == '-') + { + numStr.Append('-'); + ReadChar(); + } + else if (c == '+') + { + numStr.Append('+'); + ReadChar(); + } + + if (PeekChar() < 0) throw ParseError("Invalid JSON numeric literal; incomplete exponent"); + + for (; ; ) + { + c = PeekChar(); + if (c < '0' || c > '9') break; + numStr.Append((char)c); + ReadChar(); + } } - if (exp!=0) - val*=Math.Pow(10, exp*expSign); - } + double val = double.Parse(numStr.ToString(), NumberStyles.Float, NumberFormatInfo.InvariantInfo); - if (negative) val*=-1; - long lval=(long)val; - if (lval==val) return lval; - else return val; + if (val == 0.0 && double.IsNegative(val)) return -0.0; + + if (!hasFracOrExp) + { + long lval = (long)val; + if (lval == val) return lval; + } + return val; } public string ReadStringLiteral(Func allowML) { - // callers make sure that (exitCh == '"' || exitCh == "'") - - int exitCh=ReadChar(); - sb.Length=0; - for (; ; ) - { - int c=ReadChar(); - if (c<0) throw ParseError("JSON string is not closed"); - if (c==exitCh) - { - if (allowML!=null && exitCh=='\'' && PeekChar()=='\'' && sb.Length==0) - { - // ''' indicates a multiline string - ReadChar(); - return allowML(); - } - else return sb.ToString(); - } - else if (c=='\n' || c=='\r') - { - throw ParseError("Bad string containing newline"); - } - else if (c!='\\') - { - sb.Append((char)c); - continue; - } + // callers make sure that (exitCh == '"' || exitCh == "'") - // escaped expression - c=ReadChar(); - if (c<0) - throw ParseError("Invalid JSON string literal; incomplete escape sequence"); - switch (c) + int exitCh = ReadChar(); + sb.Length = 0; + for (; ; ) { - case '"': - case '\'': - case '\\': - case '/': sb.Append((char)c); break; - case 'b': sb.Append('\x8'); break; - case 'f': sb.Append('\f'); break; - case 'n': sb.Append('\n'); break; - case 'r': sb.Append('\r'); break; - case 't': sb.Append('\t'); break; - case 'u': - ushort cp=0; - for (int i=0; i<4; i++) + int c = ReadChar(); + if (c < 0) throw ParseError("JSON string is not closed"); + if (c == exitCh) + { + if (allowML != null && exitCh == '\'' && PeekChar() == '\'' && sb.Length == 0) + { + // ''' indicates a multiline string + ReadChar(); + return allowML(); + } + else return sb.ToString(); + } + else if (c == '\n' || c == '\r') + { + throw ParseError("Bad string containing newline"); + } + else if (c != '\\') + { + sb.Append((char)c); + continue; + } + + // escaped expression + c = ReadChar(); + if (c < 0) + throw ParseError("Invalid JSON string literal; incomplete escape sequence"); + switch (c) { - cp <<= 4; - if ((c=ReadChar())<0) - throw ParseError("Incomplete unicode character escape literal"); - if (c>='0' && c<='9') cp+=(ushort)(c-'0'); - else if (c>='A' && c<='F') cp+=(ushort)(c-'A'+10); - else if (c>='a' && c<='f') cp+=(ushort)(c-'a'+10); - else throw ParseError("Bad \\u char "+(char)c); + case '"': + case '\'': + case '\\': + case '/': sb.Append((char)c); break; + case 'b': sb.Append('\x8'); break; + case 'f': sb.Append('\f'); break; + case 'n': sb.Append('\n'); break; + case 'r': sb.Append('\r'); break; + case 't': sb.Append('\t'); break; + case 'u': + ushort cp = 0; + for (int i = 0; i < 4; i++) + { + cp <<= 4; + if ((c = ReadChar()) < 0) + throw ParseError("Incomplete unicode character escape literal"); + if (c >= '0' && c <= '9') cp += (ushort)(c - '0'); + else if (c >= 'A' && c <= 'F') cp += (ushort)(c - 'A' + 10); + else if (c >= 'a' && c <= 'f') cp += (ushort)(c - 'a' + 10); + else throw ParseError("Bad \\u char " + (char)c); + } + sb.Append((char)cp); + break; + default: + throw ParseError("Invalid JSON string literal; unexpected escape character"); } - sb.Append((char)cp); - break; - default: - throw ParseError("Invalid JSON string literal; unexpected escape character"); } - } } public void Expect(char expected) { - int c; - if ((c=ReadChar())!=expected) - throw ParseError(String.Format("Expected '{0}', got '{1}'", expected, (char)c)); + int c; + if ((c = ReadChar()) != expected) + throw ParseError($"Expected '{expected}', got '{(char)c}'"); } public void Expect(string expected) { - for (int i=0; i 3.0.1 - netstandard2.0 + net10.0 true Hjson Hjson diff --git a/Hjson/HjsonAttributes.cs b/Hjson/HjsonAttributes.cs new file mode 100644 index 0000000..581efaf --- /dev/null +++ b/Hjson/HjsonAttributes.cs @@ -0,0 +1,27 @@ +using System; + +namespace Hjson; + +/// Specifies the property name used in Hjson serialization. +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +public sealed class HjsonPropertyNameAttribute(string name) : Attribute +{ + /// Gets the name of the property. + public string Name { get; } = name; +} + +/// Indicates that a property should be ignored during Hjson serialization. +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +public sealed class HjsonIgnoreAttribute : Attribute; + +/// Indicates that a non-public property or field should be included during Hjson serialization. +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +public sealed class HjsonIncludeAttribute : Attribute; + +/// Specifies a comment to be written above the property in Hjson output. +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +public sealed class HjsonCommentAttribute(string comment) : Attribute +{ + /// Gets the comment text. + public string Comment { get; } = comment; +} diff --git a/Hjson/HjsonConvert.cs b/Hjson/HjsonConvert.cs new file mode 100644 index 0000000..fe30f50 --- /dev/null +++ b/Hjson/HjsonConvert.cs @@ -0,0 +1,325 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.Json.Serialization; + +namespace Hjson; + +/// Provides methods for serializing and deserializing objects to/from Hjson. +/// +/// Supports Hjson-specific attributes (, +/// , , +/// ) with fallback to System.Text.Json attributes +/// (, , +/// ). +/// +public static class HjsonConvert +{ + /// Serializes an object to an Hjson string. + public static string Serialize(object obj, HjsonOptions options = null) + { + var jsonValue = ToJsonValue(obj); + if (jsonValue == null) return "null"; + + using var sw = new StringWriter(); + bool hasComments = HasAnyComments(jsonValue); + var opts = options ?? new HjsonOptions(); + if (hasComments) opts.KeepWsc = true; + HjsonValue.Save(jsonValue, sw, opts); + return sw.ToString(); + } + + /// Deserializes an Hjson string to an object of type . + public static T Deserialize(string hjson, HjsonOptions options = null) + { + var jsonValue = options != null + ? HjsonValue.Parse(hjson, options) + : HjsonValue.Parse(hjson); + return (T)FromJsonValue(jsonValue, typeof(T)); + } + + // ── Serialization (Object → JsonValue) ────────────────────────── + + static JsonValue ToJsonValue(object obj) + { + if (obj == null) return null; + + var type = obj.GetType(); + + // Primitives handled by implicit operators + if (obj is bool b) return b; + if (obj is string s) return s; + if (obj is char c) return new string(c, 1); + if (obj is int i) return i; + if (obj is long l) return l; + if (obj is double d) return d; + if (obj is float f) return f; + if (obj is decimal dec) return dec; + if (obj is byte by) return by; + if (obj is short sh) return sh; + + // Enum → string + if (type.IsEnum) return obj.ToString(); + + // Note: Nullable is already unboxed to T by the CLR when boxed + + // Dictionary + if (obj is IDictionary dict && type.IsGenericType) + { + var keyType = type.GetGenericArguments()[0]; + if (keyType == typeof(string)) + { + var jsonObj = new JsonObject(); + foreach (DictionaryEntry entry in dict) + jsonObj.Add((string)entry.Key, ToJsonValue(entry.Value)); + return jsonObj; + } + } + + // Array / List / IEnumerable (but not string) + if (obj is IEnumerable enumerable) + { + var jsonArr = new JsonArray(); + foreach (var item in enumerable) + jsonArr.Add(ToJsonValue(item)); + return jsonArr; + } + + // Complex object → reflect + return ObjectToJsonValue(obj, type); + } + + static JsonValue ObjectToJsonValue(object obj, Type type) + { + var members = GetMembers(type); + bool hasComments = members.Any(m => m.Comment != null); + + WscJsonObject wscObj = null; + JsonObject jsonObj; + + if (hasComments) + { + wscObj = new WscJsonObject { RootBraces = true }; + jsonObj = wscObj; + wscObj.Comments[""] = ""; + } + else + { + jsonObj = new JsonObject(); + } + + foreach (var member in members) + { + var value = member.GetValue(obj); + var jsonValue = ToJsonValue(value); + jsonObj.Add(member.HjsonName, jsonValue); + + if (wscObj != null) + { + wscObj.Order.Add(member.HjsonName); + wscObj.Comments[member.HjsonName] = member.Comment != null + ? "\n" + FormatComment(member.Comment) + : ""; + } + } + + return jsonObj; + } + + static string FormatComment(string comment) + { + var lines = comment.Replace("\r\n", "\n").Split('\n'); + return string.Join("\n", lines.Select(line => "# " + line)); + } + + static bool HasAnyComments(JsonValue value) => value is WscJsonObject; + + // ── Deserialization (JsonValue → Object) ───────────────────────── + + static object FromJsonValue(JsonValue value, Type targetType) + { + if (value == null) + { + if (targetType.IsValueType && Nullable.GetUnderlyingType(targetType) == null) + return Activator.CreateInstance(targetType); + return null; + } + + // Unwrap Nullable + var underlying = Nullable.GetUnderlyingType(targetType); + if (underlying != null) targetType = underlying; + + // Primitives + if (targetType == typeof(bool)) return (bool)value; + if (targetType == typeof(string)) return (string)value; + if (targetType == typeof(char)) + { + var str = (string)value; + return str.Length > 0 ? str[0] : default; + } + if (targetType == typeof(int)) return (int)value; + if (targetType == typeof(long)) return (long)value; + if (targetType == typeof(double)) return (double)value; + if (targetType == typeof(float)) return (float)value; + if (targetType == typeof(decimal)) return (decimal)value; + if (targetType == typeof(byte)) return (byte)value; + if (targetType == typeof(short)) return (short)value; + + // Enum + if (targetType.IsEnum) + { + if (value.JsonType == JsonType.String) + return Enum.Parse(targetType, (string)value, ignoreCase: true); + if (value.JsonType == JsonType.Number) + return Enum.ToObject(targetType, (int)value); + } + + // JsonValue passthrough — if someone wants the raw value + if (targetType == typeof(JsonValue) || targetType == typeof(JsonObject) || + targetType == typeof(JsonArray) || targetType == typeof(JsonPrimitive)) + return value; + + // Array + if (targetType.IsArray) + { + var elementType = targetType.GetElementType()!; + var arr = value.Count > 0 ? new object[value.Count] : []; + for (int i = 0; i < value.Count; i++) + arr[i] = FromJsonValue(value[i], elementType); + var typed = Array.CreateInstance(elementType, arr.Length); + Array.Copy(arr, typed, arr.Length); + return typed; + } + + // List / IList / IEnumerable / ICollection + if (targetType.IsGenericType) + { + var genDef = targetType.GetGenericTypeDefinition(); + var genArgs = targetType.GetGenericArguments(); + + // Dictionary + if ((genDef == typeof(Dictionary<,>) || genDef == typeof(IDictionary<,>)) && + genArgs[0] == typeof(string)) + { + var dictType = genDef == typeof(IDictionary<,>) + ? typeof(Dictionary<,>).MakeGenericType(genArgs) + : targetType; + var dict = (IDictionary)Activator.CreateInstance(dictType)!; + if (value is JsonObject obj) + { + foreach (var key in obj.Keys) + dict[key] = FromJsonValue(obj[key], genArgs[1]); + } + return dict; + } + + // List, IList, ICollection, IEnumerable + if (genDef == typeof(List<>) || genDef == typeof(IList<>) || + genDef == typeof(ICollection<>) || genDef == typeof(IEnumerable<>)) + { + var listType = (genDef == typeof(List<>)) + ? targetType + : typeof(List<>).MakeGenericType(genArgs); + var list = (IList)Activator.CreateInstance(listType)!; + for (int i = 0; i < value.Count; i++) + list.Add(FromJsonValue(value[i], genArgs[0])); + return list; + } + } + + // Complex object + if (value is JsonObject jsonObj) + return ObjectFromJsonValue(jsonObj, targetType); + + throw new InvalidOperationException($"Cannot convert {value.JsonType} to {targetType.Name}"); + } + + static object ObjectFromJsonValue(JsonObject jsonObj, Type type) + { + var instance = Activator.CreateInstance(type)!; + var members = GetMembers(type); + + foreach (var member in members) + { + if (!jsonObj.ContainsKey(member.HjsonName)) continue; + var jsonValue = jsonObj[member.HjsonName]; + var converted = FromJsonValue(jsonValue, member.MemberType); + member.SetValue(instance, converted); + } + + return instance; + } + + // ── Member reflection ──────────────────────────────────────────── + + sealed class MemberDescriptor + { + public string HjsonName; + public string Comment; + public Type MemberType; + public Func GetValue; + public Action SetValue; + } + + static List GetMembers(Type type) + { + var result = new List(); + const BindingFlags allInstance = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + + // Properties + foreach (var prop in type.GetProperties(allInstance)) + { + if (!ShouldInclude(prop, prop.GetMethod?.IsPublic == true || prop.SetMethod?.IsPublic == true)) + continue; + result.Add(new MemberDescriptor + { + HjsonName = GetName(prop), + Comment = GetComment(prop), + MemberType = prop.PropertyType, + GetValue = prop.GetValue, + SetValue = prop.SetValue, + }); + } + + // Fields + foreach (var field in type.GetFields(allInstance)) + { + if (!ShouldInclude(field, field.IsPublic)) + continue; + result.Add(new MemberDescriptor + { + HjsonName = GetName(field), + Comment = GetComment(field), + MemberType = field.FieldType, + GetValue = field.GetValue, + SetValue = field.SetValue, + }); + } + + return result; + } + + static bool ShouldInclude(MemberInfo member, bool isPublic) + { + // Explicit ignore + if (member.GetCustomAttribute() != null) return false; + if (member.GetCustomAttribute() != null) return false; + + // Explicit include (for non-public) + if (member.GetCustomAttribute() != null) return true; + if (member.GetCustomAttribute() != null) return true; + + // Otherwise, only public + return isPublic; + } + + static string GetName(MemberInfo member) => + member.GetCustomAttribute()?.Name ?? + member.GetCustomAttribute()?.Name ?? + member.Name; + + static string GetComment(MemberInfo member) => member.GetCustomAttribute()?.Comment; +} diff --git a/Hjson/HjsonDsf.cs b/Hjson/HjsonDsf.cs index 8a41622..daae52e 100644 --- a/Hjson/HjsonDsf.cs +++ b/Hjson/HjsonDsf.cs @@ -5,13 +5,13 @@ using System.Text; using System.Text.RegularExpressions; -namespace Hjson +namespace Hjson; + +/// +/// A interface to support Domain Specific Formats for Hjson. +/// +public interface IHjsonDsfProvider { - /// - /// A interface to support Domain Specific Formats for Hjson. - /// - public interface IHjsonDsfProvider - { /// Gets the name of this DSF. string Name { get; } /// Gets the description of this DSF. @@ -20,13 +20,13 @@ public interface IHjsonDsfProvider JsonValue Parse(string text); /// Stringifies DSF values. string Stringify(JsonValue value); - } +} - /// - /// Provides standard DSF providers. - /// - public static class HjsonDsf - { +/// +/// Provides standard DSF providers. +/// +public static class HjsonDsf +{ /// Returns a math DSF provider. public static IHjsonDsfProvider Math() { return new DsfMath(); } /// Returns a hex DSF provider. @@ -36,154 +36,150 @@ public static class HjsonDsf static bool isInvalidDsfChar(char c) { - return c == '{' || c == '}' || c == '[' || c == ']' || c == ','; + return c == '{' || c == '}' || c == '[' || c == ']' || c == ','; } internal static JsonValue Parse(IEnumerable dsfProviders, string value) { - foreach (var dsf in dsfProviders) - { - try - { - var res=dsf.Parse(value); - if (res!=null) return res; - } - catch (Exception e) + foreach (var dsf in dsfProviders) { - throw new Exception("DSF-"+dsf.Name+" failed; "+e.Message, e); + try + { + var res = dsf.Parse(value); + if (res != null) return res; + } + catch (Exception e) + { + throw new Exception("DSF-" + dsf.Name + " failed; " + e.Message, e); + } } - } - return value; + return value; } internal static string Stringify(IEnumerable dsfProviders, JsonValue value) { - foreach (var dsf in dsfProviders) - { - try - { - var text=dsf.Stringify(value); - if (text!=null) - { - if (text.Length==0 || text.FirstOrDefault()=='"' || text.Any(c => isInvalidDsfChar(c))) - throw new Exception("value may not be empty, start with a quote or contain a punctuator character except colon: " + text); - return text; - } - } - catch (Exception e) + foreach (var dsf in dsfProviders) { - throw new Exception("DSF-"+dsf.Name+" failed; "+e.Message, e); + try + { + var text = dsf.Stringify(value); + if (text != null) + { + if (text.Length == 0 || text.FirstOrDefault() == '"' || text.Any(c => isInvalidDsfChar(c))) + throw new Exception("value may not be empty, start with a quote or contain a punctuator character except colon: " + text); + return text; + } + } + catch (Exception e) + { + throw new Exception("DSF-" + dsf.Name + " failed; " + e.Message, e); + } } - } - return null; + return null; } - } +} - class DsfMath : IHjsonDsfProvider - { - public string Name { get { return "math"; } } - public string Description { get { return "support for Inf/inf, -Inf/-inf, Nan/naN and -0"; } } +class DsfMath : IHjsonDsfProvider +{ + public string Name => "math"; + public string Description => "support for Inf/inf, -Inf/-inf, Nan/naN and -0"; - static readonly long NegativeZeroBits=BitConverter.DoubleToInt64Bits(-0.0); + static readonly long NegativeZeroBits = BitConverter.DoubleToInt64Bits(-0.0); static bool isNegativeZero(double x) { - return BitConverter.DoubleToInt64Bits(x)==NegativeZeroBits; + return BitConverter.DoubleToInt64Bits(x) == NegativeZeroBits; } public JsonValue Parse(string text) { - switch (text) - { - case "+inf": - case "inf": - case "+Inf": - case "Inf": - return double.PositiveInfinity; - case "-inf": - case "-Inf": - return double.NegativeInfinity; - case "nan": - case "NaN": - return double.NaN; - default: - return null; - } + switch (text) + { + case "+inf": + case "inf": + case "+Inf": + case "Inf": + return double.PositiveInfinity; + case "-inf": + case "-Inf": + return double.NegativeInfinity; + case "nan": + case "NaN": + return double.NaN; + default: + return null; + } } public string Stringify(JsonValue value) { - if (value.JsonType!=JsonType.Number) return null; - var val=value.Qd(); - if (double.IsPositiveInfinity(val)) return "Inf"; - else if (double.IsNegativeInfinity(val)) return "-Inf"; - else if (double.IsNaN(val)) return "NaN"; - else if (isNegativeZero(val)) return "-0"; - else return null; + if (value.JsonType != JsonType.Number) return null; + var val = value.Qd(); + if (double.IsPositiveInfinity(val)) return "Inf"; + else if (double.IsNegativeInfinity(val)) return "-Inf"; + else if (double.IsNaN(val)) return "NaN"; + else if (isNegativeZero(val)) return "-0"; + else return null; } - } - - class DsfHex : IHjsonDsfProvider - { - bool stringify; - static Regex isHex=new Regex(@"^0x[0-9A-Fa-f]+$"); +} - public DsfHex(bool stringify) { this.stringify=stringify; } +class DsfHex(bool stringify) : IHjsonDsfProvider +{ + static readonly Regex isHex = new(@"^0x[0-9A-Fa-f]+$"); - public string Name { get { return "hex"; } } - public string Description { get { return "parse hexadecimal numbers prefixed with 0x"; } } + public string Name => "hex"; + public string Description => "parse hexadecimal numbers prefixed with 0x"; public JsonValue Parse(string text) { - if (isHex.IsMatch(text)) - return long.Parse(text.Substring(2), NumberStyles.HexNumber); - else - return null; + if (isHex.IsMatch(text)) + return long.Parse(text.Substring(2), NumberStyles.HexNumber); + else + return null; } public string Stringify(JsonValue value) { - if (stringify && - value.JsonType==JsonType.Number && - value.Ql()==value.Qd()) - { - return "0x"+value.Ql().ToString("x"); - } - else - { - return null; - } + if (stringify && + value.JsonType == JsonType.Number && + value.Ql() == value.Qd()) + { + return "0x" + value.Ql().ToString("x"); + } + else + { + return null; + } } - } +} - class DsfDate : IHjsonDsfProvider - { - static Regex isDate=new Regex(@"^\d{4}-\d{2}-\d{2}$"); - static Regex isDateTime=new Regex(@"^\d{4}-\d{2}-\d{2}T\d{2}\:\d{2}\:\d{2}(?:.\d+)(?:Z|[+-]\d{2}:\d{2})$"); +class DsfDate : IHjsonDsfProvider +{ + static readonly Regex isDate = new(@"^\d{4}-\d{2}-\d{2}$"); + static readonly Regex isDateTime = new(@"^\d{4}-\d{2}-\d{2}T\d{2}\:\d{2}\:\d{2}(?:.\d+)(?:Z|[+-]\d{2}:\d{2})$"); - public string Name { get { return "date"; } } - public string Description { get { return "support ISO dates"; } } + public string Name => "date"; + public string Description => "support ISO dates"; public JsonValue Parse(string text) { - if (isDate.IsMatch(text) || isDateTime.IsMatch(text)) - return JsonValue.FromObject(DateTime.Parse(text)); - else - return null; + if (isDate.IsMatch(text) || isDateTime.IsMatch(text)) + return JsonValue.FromObject(DateTime.Parse(text)); + else + return null; } public string Stringify(JsonValue value) { - if (value.JsonType==JsonType.Unknown && value.ToValue().GetType()==typeof(DateTime)) - { - var dt=(DateTime)value.ToValue(); - return dt.ToString("yyyy-MM-ddTHH:mm:ssZ"); - } - else - { - return null; - } + if (value.JsonType == JsonType.Unknown && value.ToValue().GetType() == typeof(DateTime)) + { + var dt = (DateTime)value.ToValue(); + return dt.ToString("yyyy-MM-ddTHH:mm:ssZ"); + } + else + { + return null; + } } - } } diff --git a/Hjson/HjsonOptions.cs b/Hjson/HjsonOptions.cs index 72f1914..e5bdd43 100644 --- a/Hjson/HjsonOptions.cs +++ b/Hjson/HjsonOptions.cs @@ -6,32 +6,25 @@ using System.Linq; using System.Text; -namespace Hjson +namespace Hjson; + +/// Options for Save. +public class HjsonOptions { - /// Options for Save. - public class HjsonOptions - { IHjsonDsfProvider[] dsf; - /// Initializes a new instance of this class. - public HjsonOptions() - { - EmitRootBraces=true; - } - /// Keep white space and comments. public bool KeepWsc { get; set; } /// Show braces at the root level (default true). - public bool EmitRootBraces { get; set; } + public bool EmitRootBraces { get; set; } = true; /// /// Gets or sets DSF providers. /// public IEnumerable DsfProviders { - get { return dsf??Enumerable.Empty(); } - set { dsf=value.ToArray(); } + get => dsf ?? []; + set => dsf = [.. value]; } - } } diff --git a/Hjson/HjsonReader.cs b/Hjson/HjsonReader.cs index f8567dc..d6ce98c 100644 --- a/Hjson/HjsonReader.cs +++ b/Hjson/HjsonReader.cs @@ -6,425 +6,407 @@ using System.Linq; using System.Text; -namespace Hjson -{ - using JsonPair=KeyValuePair; +namespace Hjson; + +using JsonPair = KeyValuePair; - internal class HjsonReader : BaseReader - { - StringBuilder sb=new StringBuilder(); - IEnumerable dsfProviders=Enumerable.Empty(); +internal class HjsonReader : BaseReader +{ + readonly StringBuilder sb = new(); + readonly IEnumerable dsfProviders = []; public HjsonReader(TextReader reader, IJsonReader jsonReader, HjsonOptions options) : base(reader, jsonReader) { - if (options!=null) - { - ReadWsc=options.KeepWsc; - dsfProviders=options.DsfProviders; - } + if (options != null) + { + ReadWsc = options.KeepWsc; + dsfProviders = options.DsfProviders; + } } public JsonValue Read() { - // Braces for the root object are optional - - int c=SkipPeekChar(); - switch (c) - { - case '[': - case '{': - return checkTrailing(ReadCore()); - default: - try - { - // assume we have a root object without braces - return checkTrailing(ReadCore(true)); - } - catch (Exception) - { - // test if we are dealing with a single JSON value instead (true/false/null/num/"") - Reset(); - try { return checkTrailing(ReadCore()); } - catch (Exception) { } - throw; // throw original error - } - } + // Braces for the root object are optional + + int c = SkipPeekChar(); + switch (c) + { + case '[': + case '{': + return checkTrailing(ReadCore()); + default: + try + { + // assume we have a root object without braces + return checkTrailing(ReadCore(true)); + } + catch (Exception) + { + // test if we are dealing with a single JSON value instead (true/false/null/num/"") + Reset(); + try { return checkTrailing(ReadCore()); } + catch (Exception) { } + throw; // throw original error + } + } } JsonValue checkTrailing(JsonValue v) { - skipWhite2(); - if (ReadChar()>=0) throw ParseError("Extra characters in input"); - return v; + skipWhite2(); + if (ReadChar() >= 0) throw ParseError("Extra characters in input"); + return v; } void skipWhite2() { - while (PeekChar()>=0) - { - while (IsWhite((char)PeekChar())) ReadChar(); - int p=PeekChar(); - if (p=='#' || p=='/' && PeekChar(1)=='/') + while (PeekChar() >= 0) { - for (; ; ) - { - var ch=PeekChar(); - if (ch<0 || ch=='\n') break; - ReadChar(); - } - } - else if (p=='/' && PeekChar(1)=='*') - { - ReadChar(); ReadChar(); - for (; ; ) - { - var ch=PeekChar(); - if (ch<0 || ch=='*' && PeekChar(1)=='/') break; - ReadChar(); - } - if (PeekChar()>=0) { ReadChar(); ReadChar(); } + while (IsWhite((char)PeekChar())) ReadChar(); + int p = PeekChar(); + if (p == '#' || p == '/' && PeekChar(1) == '/') + { + for (; ; ) + { + var ch = PeekChar(); + if (ch < 0 || ch == '\n') break; + ReadChar(); + } + } + else if (p == '/' && PeekChar(1) == '*') + { + ReadChar(); ReadChar(); + for (; ; ) + { + var ch = PeekChar(); + if (ch < 0 || ch == '*' && PeekChar(1) == '/') break; + ReadChar(); + } + if (PeekChar() >= 0) { ReadChar(); ReadChar(); } + } + else break; } - else break; - } } protected override string GetWhite() { - var res=base.GetWhite(); - int to=res.Length-1; - if (to>=0) - { - // remove trailing whitespace - for (; to>0 && res[to]<=' ' && res[to]!='\n'; to--) ; - // but only up to EOL - if (res[to]=='\n') to--; - if (to>=0 && res[to]=='\r') to--; - res=res.Substring(0, to+1); - foreach (char c in res) if (c>' ') return res; - } - return ""; + var res = base.GetWhite(); + int to = res.Length - 1; + if (to >= 0) + { + // remove trailing whitespace + for (; to > 0 && res[to] <= ' ' && res[to] != '\n'; to--) ; + // but only up to EOL + if (res[to] == '\n') to--; + if (to >= 0 && res[to] == '\r') to--; + res = res.Substring(0, to + 1); + foreach (char c in res) if (c > ' ') return res; + } + return ""; } public override int SkipPeekChar() { - skipWhite2(); - return PeekChar(); + skipWhite2(); + return PeekChar(); } - JsonValue ReadCore(bool objectWithoutBraces=false) + JsonValue ReadCore(bool objectWithoutBraces = false) { - int c=objectWithoutBraces?'{':SkipPeekChar(); - if (c<0) throw ParseError("Incomplete input"); - switch (c) - { - case '[': - JsonArray list; - WscJsonArray wscL=null; - ReadChar(); - ResetWhite(); - if (ReadWsc) list=wscL=new WscJsonArray(); - else list=new JsonArray(); - SkipPeekChar(); - if (ReadWsc) wscL.Comments.Add(GetWhite()); - for (int i=0; ; i++) - { - if (SkipPeekChar()==']') { ReadChar(); break; } - if (HasReader) Reader.Index(i); - var value=ReadCore(); - if (HasReader) Reader.Value(value); - list.Add(value); - ResetWhite(); - if (SkipPeekChar()==',') { ReadChar(); ResetWhite(); SkipPeekChar(); } - if (ReadWsc) wscL.Comments.Add(GetWhite()); - } - return list; - case '{': - JsonObject obj; - WscJsonObject wsc=null; - if (!objectWithoutBraces) - { - ReadChar(); - ResetWhite(); - } - if (ReadWsc) obj=wsc=new WscJsonObject() { RootBraces=!objectWithoutBraces }; - else obj=new JsonObject(); - SkipPeekChar(); - if (ReadWsc) wsc.Comments[""]=GetWhite(); - for (; ; ) - { - if (objectWithoutBraces) { if (SkipPeekChar()<0) break; } - else if (SkipPeekChar()=='}') { ReadChar(); break; } - string name=readKeyName(); - skipWhite2(); - Expect(':'); - skipWhite2(); - if (HasReader) Reader.Key(name); - var value=ReadCore(); - if (HasReader) Reader.Value(value); - obj.Add(new JsonPair(name, value)); - ResetWhite(); - if (SkipPeekChar()==',') { ReadChar(); ResetWhite(); SkipPeekChar(); } - if (ReadWsc) { wsc.Comments[name]=GetWhite(); wsc.Order.Add(name); } - } - return obj; - case '\'': - case '"': return ReadStringLiteral(readMlString); - default: return readTfnns(c); - } + int c = objectWithoutBraces ? '{' : SkipPeekChar(); + if (c < 0) throw ParseError("Incomplete input"); + switch (c) + { + case '[': + JsonArray list; + WscJsonArray wscL = null; + ReadChar(); + ResetWhite(); + if (ReadWsc) list = wscL = new WscJsonArray(); + else list = new JsonArray(); + SkipPeekChar(); + if (ReadWsc) wscL.Comments.Add(GetWhite()); + for (int i = 0; ; i++) + { + if (SkipPeekChar() == ']') { ReadChar(); break; } + if (HasReader) Reader.Index(i); + var value = ReadCore(); + if (HasReader) Reader.Value(value); + list.Add(value); + ResetWhite(); + if (SkipPeekChar() == ',') { ReadChar(); ResetWhite(); SkipPeekChar(); } + if (ReadWsc) wscL.Comments.Add(GetWhite()); + } + return list; + case '{': + JsonObject obj; + WscJsonObject wsc = null; + if (!objectWithoutBraces) + { + ReadChar(); + ResetWhite(); + } + if (ReadWsc) obj = wsc = new WscJsonObject() { RootBraces = !objectWithoutBraces }; + else obj = new JsonObject(); + SkipPeekChar(); + if (ReadWsc) wsc.Comments[""] = GetWhite(); + for (; ; ) + { + if (objectWithoutBraces) { if (SkipPeekChar() < 0) break; } + else if (SkipPeekChar() == '}') { ReadChar(); break; } + string name = readKeyName(); + skipWhite2(); + Expect(':'); + skipWhite2(); + if (HasReader) Reader.Key(name); + var value = ReadCore(); + if (HasReader) Reader.Value(value); + obj.Add(new JsonPair(name, value)); + ResetWhite(); + if (SkipPeekChar() == ',') { ReadChar(); ResetWhite(); SkipPeekChar(); } + if (ReadWsc) { wsc.Comments[name] = GetWhite(); wsc.Order.Add(name); } + } + return obj; + case '\'': + case '"': return ReadStringLiteral(readMlString); + default: return readTfnns(c); + } } string readKeyName() { - // quotes for keys are optional in Hjson - // unless they include {}[],: or whitespace. - - int c=PeekChar(); - if (c=='"' || c=='\'') return ReadStringLiteral(null); - - sb.Length=0; - int space=-1; - for (; ; ) - { - c=PeekChar(); - if (c<0) throw ParseError("Name is not closed"); - char ch=(char)c; - if (ch==':') - { - if (sb.Length==0) throw ParseError("Found ':' but no key name (for an empty key name use quotes)"); - else if (space>=0 && space!=sb.Length) throw ParseError("Found whitespace in your key name (use quotes to include)"); - return sb.ToString(); - } - else if (IsWhite(ch)) - { - if (space<0) space=sb.Length; - ReadChar(); - } - else if (HjsonValue.IsPunctuatorChar(ch)) - throw ParseError("Found '"+ch+"' where a key name was expected (check your syntax or use quotes if the key name includes {}[],: or whitespace)"); - else + // quotes for keys are optional in Hjson + // unless they include {}[],: or whitespace. + + int c = PeekChar(); + if (c == '"' || c == '\'') return ReadStringLiteral(null); + + sb.Length = 0; + int space = -1; + for (; ; ) { - ReadChar(); - sb.Append(ch); + c = PeekChar(); + if (c < 0) throw ParseError("Name is not closed"); + char ch = (char)c; + if (ch == ':') + { + if (sb.Length == 0) throw ParseError("Found ':' but no key name (for an empty key name use quotes)"); + else if (space >= 0 && space != sb.Length) throw ParseError("Found whitespace in your key name (use quotes to include)"); + return sb.ToString(); + } + else if (IsWhite(ch)) + { + if (space < 0) space = sb.Length; + ReadChar(); + } + else if (HjsonValue.IsPunctuatorChar(ch)) + throw ParseError($"Found '{ch}' where a key name was expected (check your syntax or use quotes if the key name includes {{}}[],: or whitespace)"); + else + { + ReadChar(); + sb.Append(ch); + } } - } } void skipIndent(int indent) { - while (indent-->0) - { - char c=(char)PeekChar(); - if (IsWhite(c) && c!='\n') ReadChar(); - else break; - } + while (indent-- > 0) + { + char c = (char)PeekChar(); + if (IsWhite(c) && c != '\n') ReadChar(); + else break; + } } string readMlString() { - // Parse a multiline string value. - int triple=0; - sb.Length=0; - - // we are at ''' - var indent=Column-3; - - // skip white/to (newline) - for (; ; ) - { - char c=(char)PeekChar(); - if (IsWhite(c) && c!='\n') ReadChar(); - else break; - } - if (PeekChar()=='\n') { ReadChar(); skipIndent(indent); } - - // When parsing for string values, we must look for " and \ characters. - while (true) - { - int ch=PeekChar(); - if (ch<0) throw ParseError("Bad multiline string"); - else if (ch=='\'') - { - triple++; - ReadChar(); - if (triple==3) - { - if (sb[sb.Length-1]=='\n') sb.Length--; - return sb.ToString(); - } - else continue; - } - else - { - while (triple>0) - { - sb.Append('\''); - triple--; - } - } - if (ch=='\n') + // Parse a multiline string value. + int triple = 0; + sb.Length = 0; + + // we are at ''' + var indent = Column - 3; + + // skip white/to (newline) + for (; ; ) { - sb.Append('\n'); - ReadChar(); - skipIndent(indent); + char c = (char)PeekChar(); + if (IsWhite(c) && c != '\n') ReadChar(); + else break; } - else + if (PeekChar() == '\n') { ReadChar(); skipIndent(indent); } + + // When parsing for string values, we must look for " and \ characters. + while (true) { - if (ch!='\r') sb.Append((char)ch); - ReadChar(); + int ch = PeekChar(); + if (ch < 0) throw ParseError("Bad multiline string"); + else if (ch == '\'') + { + triple++; + ReadChar(); + if (triple == 3) + { + if (sb[sb.Length - 1] == '\n') sb.Length--; + return sb.ToString(); + } + else continue; + } + else + { + while (triple > 0) + { + sb.Append('\''); + triple--; + } + } + if (ch == '\n') + { + sb.Append('\n'); + ReadChar(); + skipIndent(indent); + } + else + { + if (ch != '\r') sb.Append((char)ch); + ReadChar(); + } } - } } internal static bool TryParseNumericLiteral(string text, bool stopAtNext, out JsonValue value) { - int c, leadingZeros=0, p=0; - double val=0; - bool negative=false, testLeading=true; - text+='\0'; - value=null; - - if (text[p]=='-') - { - negative=true; - p++; - if (text[p]==0) return false; - } - - for (int x=0; ; x++) - { - c=text[p]; - if (c<'0' || c>'9') break; - if (testLeading) + int c, leadingZeros = 0, p = 0; + bool testLeading = true; + text += '\0'; + value = null; + + if (text[p] == '-') { - if (c=='0') leadingZeros++; - else testLeading=false; + p++; + if (text[p] == 0) return false; } - val=val*10+(c-'0'); - p++; - } - if (testLeading) leadingZeros--; // single 0 is allowed - if (leadingZeros>0) return false; - - // fraction - if (text[p]=='.') - { - if (leadingZeros<0) return false; - int fdigits=0; - double frac=0; - p++; - if (text[p]==0) return false; - double d=10; - for (; ; ) + + for (int x = 0; ; x++) { - c=text[p]; - if (c<'0' || '9' '9') break; + if (testLeading) + { + if (c == '0') leadingZeros++; + else testLeading = false; + } + p++; } - if (fdigits==0) return false; - val+=frac; - } - - c=text[p]; - if (c=='e' || c=='E') - { - // exponent - int exp=0, expSign=1; + if (testLeading) leadingZeros--; // single 0 is allowed + if (leadingZeros > 0) return false; - p++; - if (text[p]==0) return false; + // fraction + if (text[p] == '.') + { + if (leadingZeros < 0) return false; + int fdigits = 0; + p++; + if (text[p] == 0) return false; + for (; ; ) + { + c = text[p]; + if (c < '0' || '9' < c) break; + p++; + fdigits++; + } + if (fdigits == 0) return false; + } - c=text[p]; - if (c=='-') + c = text[p]; + if (c == 'e' || c == 'E') { - p++; - expSign=-1; + p++; + if (text[p] == 0) return false; + + c = text[p]; + if (c == '-') + { + p++; + } + else if (c == '+') p++; + + if (text[p] == 0) return false; + + for (; ; ) + { + c = text[p]; + if (c < '0' || c > '9') break; + p++; + } } - else if (c=='+') p++; - if (text[p]==0) return false; + int numEnd = p; - for (; ; ) + while (p < text.Length && IsWhite(text[p])) p++; + + bool foundStop = false; + if (p < text.Length && stopAtNext) { - c=text[p]; - if (c<'0' || c>'9') break; - exp=exp*10+(c-'0'); - p++; + // end scan if we find a control character like ,}] or a comment + char ch = text[p]; + if (ch == ',' || ch == '}' || ch == ']' || ch == '#' || ch == '/' && (text.Length > p + 1 && (text[p + 1] == '/' || text[p + 1] == '*'))) + foundStop = true; } - if (exp!=0) - val*=Math.Pow(10, exp*expSign); - } - - while (pp+1 && (text[p+1]=='/' || text[p+1]=='*'))) - foundStop=true; - } - - if (p+1!=text.Length && !foundStop) return false; - - if (negative) - { - if (val==0.0) { value=-0.0; return true; } - val*=-1; - } - - long lval=(long)val; - if (lval==val) value=lval; - else value=val; - return true; + if (p + 1 != text.Length && !foundStop) return false; + + string numStr = text.Substring(0, numEnd); + double val = double.Parse(numStr, NumberStyles.Float, NumberFormatInfo.InvariantInfo); + + if (val == 0.0 && double.IsNegative(val)) { value = -0.0; return true; } + + long lval = (long)val; + value = lval == val ? lval : val; + return true; } JsonValue readTfnns(int c) { - if (HjsonValue.IsPunctuatorChar((char)c)) - throw ParseError("Found a punctuator character '" + c + "' when expecting a quoteless string (check your syntax)"); - - sb.Length=0; - for (; ; ) - { - bool isEol=c<0 || c=='\n'; - if (isEol || c==',' || - c=='}' || c==']' || - c=='#' || - c=='/' && (PeekChar(1)=='/' || PeekChar(1)=='*')) + if (HjsonValue.IsPunctuatorChar((char)c)) + throw ParseError($"Found a punctuator character '{c}' when expecting a quoteless string (check your syntax)"); + + sb.Length = 0; + for (; ; ) { - if (sb.Length>0) - { - char ch=sb[0]; - switch (ch) + bool isEol = c < 0 || c == '\n'; + if (isEol || c == ',' || + c == '}' || c == ']' || + c == '#' || + c == '/' && (PeekChar(1) == '/' || PeekChar(1) == '*')) { - case 'f': if (sb.ToString().Trim()=="false") return false; break; - case 'n': if (sb.ToString().Trim()=="null") return null; break; - case 't': if (sb.ToString().Trim()=="true") return true; break; - default: - if (ch=='-' || ch>='0' && ch<='9') + if (sb.Length > 0) { - JsonValue res; - if (TryParseNumericLiteral(sb.ToString(), false, out res)) return res; + char ch = sb[0]; + switch (ch) + { + case 'f': if (sb.ToString().Trim() == "false") return false; break; + case 'n': if (sb.ToString().Trim() == "null") return null; break; + case 't': if (sb.ToString().Trim() == "true") return true; break; + default: + if (ch is '-' || (ch is >= '0' and <= '9')) + { + if (TryParseNumericLiteral(sb.ToString(), false, out var res)) return res; + } + break; + } + } + if (isEol) + { + // remove any whitespace at the end (ignored in quoteless strings) + return HjsonDsf.Parse(dsfProviders, sb.ToString().Trim()); } - break; } - } - if (isEol) - { - // remove any whitespace at the end (ignored in quoteless strings) - return HjsonDsf.Parse(dsfProviders, sb.ToString().Trim()); - } + ReadChar(); + if (c != '\r') sb.Append((char)c); + c = PeekChar(); } - ReadChar(); - if (c!='\r') sb.Append((char)c); - c=PeekChar(); - } } - } } diff --git a/Hjson/HjsonValue.cs b/Hjson/HjsonValue.cs index e6aa3a5..6e82775 100644 --- a/Hjson/HjsonValue.cs +++ b/Hjson/HjsonValue.cs @@ -6,117 +6,117 @@ using System.Linq; using System.Text; -namespace Hjson +namespace Hjson; + +/// Contains functions to load and save in the Hjson format. +public static class HjsonValue { - /// Contains functions to load and save in the Hjson format. - public static class HjsonValue - { /// Loads Hjson/JSON from a file. public static JsonValue Load(string path) { - return load(path, null, null); + return load(path, null, null); } /// Loads Hjson/JSON from a file, optionally preserving whitespace and comments. public static JsonValue Load(string path, HjsonOptions options) { - return load(path, null, options); + return load(path, null, options); } /// Loads Hjson/JSON from a stream. public static JsonValue Load(Stream stream) { - return load(stream, null, null); + return load(stream, null, null); } /// Loads Hjson/JSON from a stream, optionally preserving whitespace and comments. public static JsonValue Load(Stream stream, HjsonOptions options) { - return load(stream, null, options); + return load(stream, null, options); } /// Loads Hjson/JSON from a TextReader. - public static JsonValue Load(TextReader textReader, IJsonReader jsonReader=null) + public static JsonValue Load(TextReader textReader, IJsonReader jsonReader = null) { - return load(textReader, jsonReader, null); + return load(textReader, jsonReader, null); } /// Loads Hjson/JSON from a TextReader, optionally preserving whitespace and comments. - public static JsonValue Load(TextReader textReader, HjsonOptions options, IJsonReader jsonReader=null) + public static JsonValue Load(TextReader textReader, HjsonOptions options, IJsonReader jsonReader = null) { - return load(textReader, jsonReader, options); + return load(textReader, jsonReader, options); } /// Loads Hjson/JSON from a TextReader, preserving whitespace and comments. [Obsolete("Use Load", true)] public static JsonValue LoadWsc(TextReader textReader) { - return load(textReader, null, new HjsonOptions { KeepWsc=true }); + return load(textReader, null, new HjsonOptions { KeepWsc = true }); } static JsonValue load(string path, IJsonReader jsonReader, HjsonOptions options) { - if (Path.GetExtension(path).ToLower()==".json") return JsonValue.Load(path); - try - { - using (var s=File.OpenRead(path)) - return load(s, jsonReader, options); - } - catch (Exception e) { throw new Exception(e.Message+" (in "+path+")", e); } + if (Path.GetExtension(path).ToLower() == ".json") return JsonValue.Load(path); + try + { + using (var s = File.OpenRead(path)) + return load(s, jsonReader, options); + } + catch (Exception e) { throw new Exception($"{e.Message} (in {path})", e); } } static JsonValue load(Stream stream, IJsonReader jsonReader, HjsonOptions options) { - if (stream==null) throw new ArgumentNullException("stream"); - return load(new StreamReader(stream, true), jsonReader, options); + ArgumentNullException.ThrowIfNull(stream); + return load(new StreamReader(stream, true), jsonReader, options); } static JsonValue load(TextReader textReader, IJsonReader jsonReader, HjsonOptions options) { - if (textReader==null) throw new ArgumentNullException("textReader"); - return new HjsonReader(textReader, jsonReader, options).Read(); + ArgumentNullException.ThrowIfNull(textReader); + return new HjsonReader(textReader, jsonReader, options).Read(); } /// Parses the specified Hjson/JSON string. public static JsonValue Parse(string hjsonString) { - if (hjsonString==null) throw new ArgumentNullException("hjsonString"); - return Load(new StringReader(hjsonString)); + ArgumentNullException.ThrowIfNull(hjsonString); + return Load(new StringReader(hjsonString)); } /// Parses the specified Hjson/JSON string, optionally preserving whitespace and comments. public static JsonValue Parse(string hjsonString, HjsonOptions options) { - if (hjsonString==null) throw new ArgumentNullException("hjsonString"); - return Load(new StringReader(hjsonString), options); + ArgumentNullException.ThrowIfNull(hjsonString); + return Load(new StringReader(hjsonString), options); } /// Saves Hjson to a file. - public static void Save(JsonValue json, string path, HjsonOptions options=null) + public static void Save(JsonValue json, string path, HjsonOptions options = null) { - if (Path.GetExtension(path).ToLower()==".json") { json.Save(path, Stringify.Formatted); return; } - using (var s=File.CreateText(path)) - Save(json, s, options); + if (Path.GetExtension(path).ToLower() == ".json") { json.Save(path, Stringify.Formatted); return; } + using (var s = File.CreateText(path)) + Save(json, s, options); } /// Saves Hjson to a stream. - public static void Save(JsonValue json, Stream stream, HjsonOptions options=null) + public static void Save(JsonValue json, Stream stream, HjsonOptions options = null) { - if (stream==null) throw new ArgumentNullException("stream"); - Save(json, new StreamWriter(stream), options); + ArgumentNullException.ThrowIfNull(stream); + Save(json, new StreamWriter(stream), options); } /// Saves Hjson to a TextWriter. - public static void Save(JsonValue json, TextWriter textWriter, HjsonOptions options=null) + public static void Save(JsonValue json, TextWriter textWriter, HjsonOptions options = null) { - if (textWriter==null) throw new ArgumentNullException("textWriter"); - new HjsonWriter(options).Save(json, textWriter, 0, false, "", true, true); - textWriter.Flush(); + ArgumentNullException.ThrowIfNull(textWriter); + new HjsonWriter(options).Save(json, textWriter, 0, false, "", true, true); + textWriter.Flush(); } internal static bool IsPunctuatorChar(char ch) { - return ch=='{' || ch=='}' || ch=='[' || ch==']' || ch==',' || ch==':'; + return ch == '{' || ch == '}' || ch == '[' || ch == ']' || ch == ',' || ch == ':'; } #region obsolete @@ -125,31 +125,30 @@ internal static bool IsPunctuatorChar(char ch) [Obsolete("Use HjsonOptions for preserveComments")] public static JsonValue Load(string path, bool preserveComments) { - return load(path, null, new HjsonOptions { KeepWsc=preserveComments }); + return load(path, null, new HjsonOptions { KeepWsc = preserveComments }); } /// Loads Hjson/JSON from a stream, optionally preserving whitespace and comments. [Obsolete("Use HjsonOptions for preserveComments")] public static JsonValue Load(Stream stream, bool preserveComments) { - return load(stream, null, new HjsonOptions { KeepWsc=preserveComments }); + return load(stream, null, new HjsonOptions { KeepWsc = preserveComments }); } /// Loads Hjson/JSON from a TextReader, optionally preserving whitespace and comments. [Obsolete("Use HjsonOptions for preserveComments")] - public static JsonValue Load(TextReader textReader, bool preserveComments, IJsonReader jsonReader=null) + public static JsonValue Load(TextReader textReader, bool preserveComments, IJsonReader jsonReader = null) { - return load(textReader, jsonReader, new HjsonOptions { KeepWsc=preserveComments }); + return load(textReader, jsonReader, new HjsonOptions { KeepWsc = preserveComments }); } /// Parses the specified Hjson/JSON string, optionally preserving whitespace and comments. [Obsolete("Use HjsonOptions for preserveComments")] public static JsonValue Parse(string hjsonString, bool preserveComments) { - if (hjsonString==null) throw new ArgumentNullException("hjsonString"); - return Load(new StringReader(hjsonString), new HjsonOptions { KeepWsc=preserveComments }); + if (hjsonString == null) throw new ArgumentNullException("hjsonString"); + return Load(new StringReader(hjsonString), new HjsonOptions { KeepWsc = preserveComments }); } #endregion - } } diff --git a/Hjson/HjsonWriter.cs b/Hjson/HjsonWriter.cs index e3a858d..c7d0fc0 100644 --- a/Hjson/HjsonWriter.cs +++ b/Hjson/HjsonWriter.cs @@ -7,267 +7,232 @@ using System.Text; using System.Text.RegularExpressions; -namespace Hjson -{ - using JsonPair=KeyValuePair; +namespace Hjson; + +using JsonPair = KeyValuePair; - internal class HjsonWriter - { - bool writeWsc; - bool emitRootBraces; - IEnumerable dsfProviders=Enumerable.Empty(); - static Regex needsEscapeName=new Regex(@"[,\{\[\}\]\s:#""']|\/\/|\/\*|'''"); +internal class HjsonWriter +{ + readonly bool writeWsc; + readonly bool emitRootBraces; + readonly IEnumerable dsfProviders = []; + static readonly Regex needsEscapeName = new(@"[,\{\[\}\]\s:#""']|\/\/|\/\*|'''"); public HjsonWriter(HjsonOptions options) { - if (options!=null) - { - writeWsc=options.KeepWsc; - emitRootBraces=options.EmitRootBraces; - dsfProviders=options.DsfProviders; - } - else emitRootBraces=true; + if (options != null) + { + writeWsc = options.KeepWsc; + emitRootBraces = options.EmitRootBraces; + dsfProviders = options.DsfProviders; + } + else emitRootBraces = true; } void nl(TextWriter tw, int level) { - tw.Write(JsonValue.eol); - tw.Write(new string(' ', level*2)); + tw.Write(JsonValue.eol); + tw.Write(new string(' ', level * 2)); } string getWsc(string str) { - if (string.IsNullOrEmpty(str)) return ""; - for (int i=0; i' ') return " # "+str; - } - return str; + if (string.IsNullOrEmpty(str)) return ""; + for (int i = 0; i < str.Length; i++) + { + char c = str[i]; + if (c == '\n' || + c == '#' || + c == '/' && i + 1 < str.Length && (str[i + 1] == '/' || str[i + 1] == '*')) break; + if (c > ' ') return $" # {str}"; + } + return str; } - string getWsc(Dictionary white, string key) { return white.ContainsKey(key)?getWsc(white[key]):""; } - string getWsc(List white, int index) { return white.Count>index?getWsc(white[index]):""; } - bool testWsc(string str) { return str.Length>0 && str[str[0]=='\r' && str.Length>1?1:0]!='\n'; } + string getWsc(Dictionary white, string key) => white.TryGetValue(key, out string value) ? getWsc(value) : ""; + string getWsc(List white, int index) => white.Count > index ? getWsc(white[index]) : ""; + bool testWsc(string str) => str.Length > 0 && str[str[0] == '\r' && str.Length > 1 ? 1 : 0] != '\n'; - public void Save(JsonValue value, TextWriter tw, int level, bool hasComment, string separator, bool noIndent=false, bool isRootObject=false) + public void Save(JsonValue value, TextWriter tw, int level, bool hasComment, string separator, bool noIndent = false, bool isRootObject = false) { - if (value==null) - { - tw.Write(separator); - tw.Write("null"); - return; - } - - // check for DSF - string dsfValue=HjsonDsf.Stringify(dsfProviders, value); - if (dsfValue!=null) - { - tw.Write(separator); - tw.Write(dsfValue); - return; - } + if (value == null) + { + tw.Write(separator); + tw.Write("null"); + return; + } - switch (value.JsonType) - { - case JsonType.Object: - var obj=value.Qo(); - WscJsonObject kw=writeWsc?obj as WscJsonObject:null; - bool showBraces=!isRootObject || (kw!=null?kw.RootBraces:emitRootBraces); - if (!noIndent) { if (obj.Count>0) nl(tw, level); else tw.Write(separator); } - if (showBraces) tw.Write('{'); - else level--; // reduce level for root - if (kw!=null) - { - var kwl=getWsc(kw.Comments, ""); - foreach (string key in kw.Order.Concat(kw.Keys).Distinct()) - { - if (!obj.ContainsKey(key)) continue; - var val=obj[key]; - tw.Write(kwl); - nl(tw, level+1); - kwl=getWsc(kw.Comments, key); + // check for DSF + string dsfValue = HjsonDsf.Stringify(dsfProviders, value); + if (dsfValue != null) + { + tw.Write(separator); + tw.Write(dsfValue); + return; + } - tw.Write(escapeName(key)); - tw.Write(":"); - Save(val, tw, level+1, testWsc(kwl), " "); - } - tw.Write(kwl); - if (showBraces) nl(tw, level); - } - else - { - bool skipFirst=!showBraces; - foreach (JsonPair pair in obj) - { - if (!skipFirst) nl(tw, level+1); else skipFirst=false; - tw.Write(escapeName(pair.Key)); - tw.Write(":"); - Save(pair.Value, tw, level+1, false, " "); - } - if (showBraces && obj.Count>0) nl(tw, level); - } - if (showBraces) tw.Write('}'); - break; - case JsonType.Array: - int i=0, n=value.Count; - if (!noIndent) { if (n>0) nl(tw, level); else tw.Write(separator); } - tw.Write('['); - WscJsonArray whiteL=null; - string wsl=null; - if (writeWsc) - { - whiteL=value as WscJsonArray; - if (whiteL!=null) wsl=getWsc(whiteL.Comments, 0); - } - for (; i0) nl(tw, level); - tw.Write(']'); - break; - case JsonType.Boolean: - tw.Write(separator); - tw.Write((bool)value?"true":"false"); - break; - case JsonType.String: - writeString(((JsonPrimitive)value).GetRawString(), tw, level, hasComment, separator); - break; - default: - tw.Write(separator); - tw.Write(((JsonPrimitive)value).GetRawString()); - break; - } + switch (value.JsonType) + { + case JsonType.Object: + var obj = value.Qo(); + WscJsonObject kw = writeWsc ? obj as WscJsonObject : null; + bool showBraces = !isRootObject || (kw != null ? kw.RootBraces : emitRootBraces); + if (!noIndent) { if (obj.Count > 0) nl(tw, level); else tw.Write(separator); } + if (showBraces) tw.Write('{'); + else level--; // reduce level for root + if (kw != null) + { + var kwl = getWsc(kw.Comments, ""); + foreach (string key in kw.Order.Concat(kw.Keys).Distinct()) + { + if (!obj.ContainsKey(key)) continue; + var val = obj[key]; + tw.Write(kwl); + nl(tw, level + 1); + kwl = getWsc(kw.Comments, key); + + tw.Write(escapeName(key)); + tw.Write(":"); + Save(val, tw, level + 1, testWsc(kwl), " "); + } + tw.Write(kwl); + if (showBraces) nl(tw, level); + } + else + { + bool skipFirst = !showBraces; + foreach (JsonPair pair in obj) + { + if (!skipFirst) nl(tw, level + 1); else skipFirst = false; + tw.Write(escapeName(pair.Key)); + tw.Write(":"); + Save(pair.Value, tw, level + 1, false, " "); + } + if (showBraces && obj.Count > 0) nl(tw, level); + } + if (showBraces) tw.Write('}'); + break; + case JsonType.Array: + int i = 0, n = value.Count; + if (!noIndent) { if (n > 0) nl(tw, level); else tw.Write(separator); } + tw.Write('['); + WscJsonArray whiteL = null; + string wsl = null; + if (writeWsc) + { + whiteL = value as WscJsonArray; + if (whiteL != null) wsl = getWsc(whiteL.Comments, 0); + } + for (; i < n; i++) + { + var v = value[i]; + if (whiteL != null) + { + tw.Write(wsl); + wsl = getWsc(whiteL.Comments, i + 1); + } + nl(tw, level + 1); + Save(v, tw, level + 1, wsl != null && testWsc(wsl), "", true); + } + if (whiteL != null) tw.Write(wsl); + if (n > 0) nl(tw, level); + tw.Write(']'); + break; + case JsonType.Boolean: + tw.Write(separator); + tw.Write((bool)value ? "true" : "false"); + break; + case JsonType.String: + writeString(((JsonPrimitive)value).GetRawString(), tw, level, hasComment, separator); + break; + default: + tw.Write(separator); + tw.Write(((JsonPrimitive)value).GetRawString()); + break; + } } static string escapeName(string name) { - if (name.Length==0 || needsEscapeName.IsMatch(name)) - return "\""+JsonWriter.EscapeString(name)+"\""; - else - return name; + if (name.Length == 0 || needsEscapeName.IsMatch(name)) + return $"\"{JsonWriter.EscapeString(name)}\""; + else + return name; } void writeString(string value, TextWriter tw, int level, bool hasComment, string separator) { - if (value=="") { tw.Write(separator+"\"\""); return; } - - char left=value[0], right=value[value.Length-1]; - char left1=value.Length>1?value[1]:'\0', left2=value.Length>2?value[2]:'\0'; - bool doEscape=hasComment || value.Any(c => needsQuotes(c)); - JsonValue dummy; - - if (doEscape || - BaseReader.IsWhite(left) || BaseReader.IsWhite(right) || - left=='"' || - left=='\'' || - left=='#' || - left=='/' && (left1=='*' || left1=='/') || - HjsonValue.IsPunctuatorChar(left) || - HjsonReader.TryParseNumericLiteral(value, true, out dummy) || - startsWithKeyword(value)) - { - // If the string contains no control characters, no quote characters, and no - // backslash characters, then we can safely slap some quotes around it. - // Otherwise we first check if the string can be expressed in multiline - // format or we must replace the offending characters with safe escape - // sequences. - - if (!value.Any(c => needsEscape(c))) tw.Write(separator+"\""+value+"\""); - else if (!value.Any(c => needsEscapeML(c)) && !value.Contains("'''") && !value.All(c => BaseReader.IsWhite(c))) writeMLString(value, tw, level, separator); - else tw.Write(separator+"\""+JsonWriter.EscapeString(value)+"\""); - } - else tw.Write(separator+value); + if (value == "") { tw.Write(separator + "\"\""); return; } + + char left = value[0], right = value[value.Length - 1]; + char left1 = value.Length > 1 ? value[1] : '\0', left2 = value.Length > 2 ? value[2] : '\0'; + bool doEscape = hasComment || value.Any(c => needsQuotes(c)); + JsonValue dummy; + + if (doEscape || + BaseReader.IsWhite(left) || BaseReader.IsWhite(right) || + left == '"' || + left == '\'' || + left == '#' || + left == '/' && (left1 == '*' || left1 == '/') || + HjsonValue.IsPunctuatorChar(left) || + HjsonReader.TryParseNumericLiteral(value, true, out dummy) || + startsWithKeyword(value)) + { + // If the string contains no control characters, no quote characters, and no + // backslash characters, then we can safely slap some quotes around it. + // Otherwise we first check if the string can be expressed in multiline + // format or we must replace the offending characters with safe escape + // sequences. + + if (!value.Any(c => needsEscape(c))) tw.Write($"{separator}\"{value}\""); + else if (!value.Any(c => needsEscapeML(c)) && !value.Contains("'''") && !value.All(c => BaseReader.IsWhite(c))) writeMLString(value, tw, level, separator); + else tw.Write($"{separator}\"{JsonWriter.EscapeString(value)}\""); + } + else tw.Write(separator + value); } void writeMLString(string value, TextWriter tw, int level, string separator) { - var lines=value.Replace("\r", "").Split('\n'); - - if (lines.Length==1) - { - tw.Write(separator+"'''"); - tw.Write(lines[0]); - tw.Write("'''"); - } - else - { - level++; - nl(tw, level); - tw.Write("'''"); + var lines = value.Replace("\r", "").Split('\n'); - foreach (var line in lines) + if (lines.Length == 1) + { + tw.Write(separator + "'''"); + tw.Write(lines[0]); + tw.Write("'''"); + } + else { - nl(tw, !string.IsNullOrEmpty(line)?level:0); - tw.Write(line); + level++; + nl(tw, level); + tw.Write("'''"); + + foreach (var line in lines) + { + nl(tw, !string.IsNullOrEmpty(line) ? level : 0); + tw.Write(line); + } + nl(tw, level); + tw.Write("'''"); } - nl(tw, level); - tw.Write("'''"); - } } static bool startsWithKeyword(string text) { - int p; - if (text.StartsWith("true") || text.StartsWith("null")) p=4; - else if (text.StartsWith("false")) p=5; - else return false; - while (pp+1 && (text[p+1]=='/' || text[p+1]=='*')); + int p; + if (text.StartsWith("true") || text.StartsWith("null")) p = 4; + else if (text.StartsWith("false")) p = 5; + else return false; + while (p < text.Length && BaseReader.IsWhite(text[p])) p++; + if (p == text.Length) return true; + char ch = text[p]; + return ch == ',' || ch == '}' || ch == ']' || ch == '#' || ch == '/' && (text.Length > p + 1 && (text[p + 1] == '/' || text[p + 1] == '*')); } - static bool needsQuotes(char c) - { - switch (c) - { - case '\t': - case '\f': - case '\b': - case '\n': - case '\r': - return true; - default: - return false; - } - } + static bool needsQuotes(char c) => c is '\t' or '\f' or '\b' or '\n' or '\r'; - static bool needsEscape(char c) - { - switch (c) - { - case '\"': - case '\\': - return true; - default: - return needsQuotes(c); - } - } + static bool needsEscape(char c) => c is '\"' or '\\' || needsQuotes(c); - static bool needsEscapeML(char c) - { - switch (c) - { - case '\n': - case '\r': - case '\t': - return false; - default: - return needsQuotes(c); - } - } - } + static bool needsEscapeML(char c) => c is '\f' or '\b'; } diff --git a/Hjson/HjsonWsc.cs b/Hjson/HjsonWsc.cs index 3430ac9..011e9a7 100644 --- a/Hjson/HjsonWsc.cs +++ b/Hjson/HjsonWsc.cs @@ -3,36 +3,23 @@ using System.Linq; using System.Text; -namespace Hjson -{ - /// Implements an object value, including whitespace and comments. - public class WscJsonObject : JsonObject - { - /// Initializes a new instance of this class. - public WscJsonObject() - { - Order=new List(); - Comments=new Dictionary(); - } +namespace Hjson; - /// Defines if braces are shown (root object only). - public bool RootBraces { get; set; } +/// Implements an object value, including whitespace and comments. +public class WscJsonObject : JsonObject +{ /// Defines the order of the keys. - public List Order { get; private set; } + public List Order { get; private set; } = []; /// Defines a comment for each key. The "" entry is emitted before any element. - public Dictionary Comments { get; private set; } - } + public Dictionary Comments { get; private set; } = []; - /// Implements an array value, including whitespace and comments. - public class WscJsonArray : JsonArray - { - /// Initializes a new instance of this class. - public WscJsonArray() - { - Comments=new List(); - } + /// Defines if braces are shown (root object only). + public bool RootBraces { get; set; } +} +/// Implements an array value, including whitespace and comments. +public class WscJsonArray : JsonArray +{ /// Defines a comment for each item. The [0] entry is emitted before any element. - public List Comments { get; private set; } - } + public List Comments { get; private set; } = []; } diff --git a/Hjson/IJsonReader.cs b/Hjson/IJsonReader.cs index 41e041e..bb1bf04 100644 --- a/Hjson/IJsonReader.cs +++ b/Hjson/IJsonReader.cs @@ -1,15 +1,14 @@ using System; -namespace Hjson +namespace Hjson; + +/// Defines the reader interface. +public interface IJsonReader { - /// Defines the reader interface. - public interface IJsonReader - { /// Triggered when an item for an object is read. void Key(string name); /// Triggered when an item for an array is read. void Index(int idx); /// Triggered when a value is read. void Value(JsonValue value); - } } diff --git a/Hjson/JsonArray.cs b/Hjson/JsonArray.cs index ebd0bcd..9c89b25 100644 --- a/Hjson/JsonArray.cs +++ b/Hjson/JsonArray.cs @@ -5,114 +5,74 @@ using System.IO; using System.Text; -namespace Hjson +namespace Hjson; + +/// Implements an array value. +public class JsonArray : JsonValue, IList { - /// Implements an array value. - public class JsonArray : JsonValue, IList - { - List list; + readonly List list; /// Initializes a new instance of this class. public JsonArray(params JsonValue[] items) { - list=new List(); - AddRange(items); + list = []; + AddRange(items); } /// Initializes a new instance of this class. public JsonArray(IEnumerable items) { - if (items==null) throw new ArgumentNullException("items"); - list=new List(items); + ArgumentNullException.ThrowIfNull(items); + list = [.. items]; } /// Gets the count of the contained items. - public override int Count - { - get { return list.Count; } - } + public override int Count => list.Count; - bool ICollection.IsReadOnly - { - get { return false; } - } + bool ICollection.IsReadOnly => false; /// Gets or sets the value for the specified index. public override sealed JsonValue this[int index] { - get { return list[index]; } - set { list[index]=value; } + get => list[index]; + set => list[index] = value; } /// The type of this value. - public override JsonType JsonType - { - get { return JsonType.Array; } - } + public override JsonType JsonType => JsonType.Array; /// Adds a new item. - public void Add(JsonValue item) - { - list.Add(item); - } + public void Add(JsonValue item) => list.Add(item); /// Adds a range of items. public void AddRange(IEnumerable items) { - if (items==null) throw new ArgumentNullException("items"); - list.AddRange(items); + ArgumentNullException.ThrowIfNull(items); + list.AddRange(items); } /// Clears the array. - public void Clear() - { - list.Clear(); - } + public void Clear() => list.Clear(); /// Determines whether the array contains a specific value. - public bool Contains(JsonValue item) - { - return list.Contains(item); - } + public bool Contains(JsonValue item) => list.Contains(item); /// Copies the elements to an System.Array, starting at a particular System.Array index. - public void CopyTo(JsonValue[] array, int arrayIndex) - { - list.CopyTo(array, arrayIndex); - } + public void CopyTo(JsonValue[] array, int arrayIndex) => list.CopyTo(array, arrayIndex); /// Determines the index of a specific item. - public int IndexOf(JsonValue item) - { - return list.IndexOf(item); - } + public int IndexOf(JsonValue item) => list.IndexOf(item); /// Inserts an item. - public void Insert(int index, JsonValue item) - { - list.Insert(index, item); - } + public void Insert(int index, JsonValue item) => list.Insert(index, item); /// Removes the specified item. - public bool Remove(JsonValue item) - { - return list.Remove(item); - } + public bool Remove(JsonValue item) => list.Remove(item); /// Removes the item with the specified index. - public void RemoveAt(int index) - { - list.RemoveAt(index); - } + public void RemoveAt(int index) => list.RemoveAt(index); - IEnumerator IEnumerable.GetEnumerator() - { - return list.GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() => list.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() - { - return list.GetEnumerator(); - } - } + IEnumerator IEnumerable.GetEnumerator() => list.GetEnumerator(); } diff --git a/Hjson/JsonObject.cs b/Hjson/JsonObject.cs index 804f6e9..6db8833 100644 --- a/Hjson/JsonObject.cs +++ b/Hjson/JsonObject.cs @@ -5,161 +5,109 @@ using System.IO; using System.Text; -namespace Hjson -{ - using JsonPair=KeyValuePair; +namespace Hjson; + +using JsonPair = KeyValuePair; - /// Implements an object value. - public class JsonObject : JsonValue, IDictionary, ICollection - { - Dictionary map; +/// Implements an object value. +public class JsonObject : JsonValue, IDictionary, ICollection +{ + readonly Dictionary map; /// Initializes a new instance of this class. /// You can also initialize an object using the C# add syntax: new JsonObject { { "key", "value" }, ... } public JsonObject(params JsonPair[] items) { - map=new Dictionary(); - if (items!=null) AddRange(items); + map = []; + if (items != null) AddRange(items); } /// Initializes a new instance of this class. /// You can also initialize an object using the C# add syntax: new JsonObject { { "key", "value" }, ... } public JsonObject(IEnumerable items) { - if (items==null) throw new ArgumentNullException("items"); - map=new Dictionary(); - AddRange(items); + ArgumentNullException.ThrowIfNull(items); + map = []; + AddRange(items); } /// Gets the count of the contained items. - public override int Count - { - get { return map.Count; } - } + public override int Count => map.Count; - IEnumerator IEnumerable.GetEnumerator() - { - return map.GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() => map.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() - { - return map.GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() => map.GetEnumerator(); /// Gets or sets the value for the specified key. public override sealed JsonValue this[string key] { - get { return map[key]; } - set { map[key]=value; } + get => map[key]; + set => map[key] = value; } /// The type of this value. - public override JsonType JsonType - { - get { return JsonType.Object; } - } + public override JsonType JsonType => JsonType.Object; /// Gets the keys of this object. - public ICollection Keys - { - get { return map.Keys; } - } + public ICollection Keys => map.Keys; /// Gets the values of this object. - public ICollection Values - { - get { return map.Values; } - } + public ICollection Values => map.Values; /// Adds a new item. /// You can also initialize an object using the C# add syntax: new JsonObject { { "key", "value" }, ... } public void Add(string key, JsonValue value) { - if (key==null) throw new ArgumentNullException("key"); - map[key]=value; // json allows duplicate keys + ArgumentNullException.ThrowIfNull(key); + map[key] = value; // json allows duplicate keys } /// Adds a new item. - public void Add(JsonPair pair) - { - Add(pair.Key, pair.Value); - } + public void Add(JsonPair pair) => Add(pair.Key, pair.Value); /// Adds a range of items. public void AddRange(IEnumerable items) { - if (items==null) throw new ArgumentNullException("items"); - foreach (var pair in items) Add(pair); + ArgumentNullException.ThrowIfNull(items); + foreach (var pair in items) Add(pair); } /// Clears the object. - public void Clear() - { - map.Clear(); - } + public void Clear() => map.Clear(); - bool ICollection.Contains(JsonPair item) - { - return (map as ICollection).Contains(item); - } + bool ICollection.Contains(JsonPair item) => ((ICollection)map).Contains(item); - bool ICollection.Remove(JsonPair item) - { - return (map as ICollection).Remove(item); - } + bool ICollection.Remove(JsonPair item) => ((ICollection)map).Remove(item); /// Determines whether the array contains a specific key. public override bool ContainsKey(string key) { - if (key==null) throw new ArgumentNullException("key"); - return map.ContainsKey(key); + ArgumentNullException.ThrowIfNull(key); + return map.ContainsKey(key); } /// Copies the elements to an System.Array, starting at a particular System.Array index. - public void CopyTo(JsonPair[] array, int arrayIndex) - { - (map as ICollection).CopyTo(array, arrayIndex); - } + public void CopyTo(JsonPair[] array, int arrayIndex) => ((ICollection)map).CopyTo(array, arrayIndex); /// Removes the item with the specified key. /// The key of the element to remove. /// true if the element is successfully found and removed; otherwise, false. public bool Remove(string key) { - if (key==null) throw new ArgumentNullException("key"); - return map.Remove(key); + ArgumentNullException.ThrowIfNull(key); + return map.Remove(key); } - bool ICollection.IsReadOnly - { - get { return false; } - } + bool ICollection.IsReadOnly => false; /// Gets the value associated with the specified key. - public bool TryGetValue(string key, out JsonValue value) - { - return map.TryGetValue(key, out value); - } + public bool TryGetValue(string key, out JsonValue value) => map.TryGetValue(key, out value); - void ICollection.Add(JsonPair item) - { - this.Add(item); - } + void ICollection.Add(JsonPair item) => this.Add(item); - void ICollection.Clear() - { - this.Clear(); - } + void ICollection.Clear() => this.Clear(); - void ICollection.CopyTo(JsonPair[] array, int arrayIndex) - { - this.CopyTo(array, arrayIndex); - } + void ICollection.CopyTo(JsonPair[] array, int arrayIndex) => this.CopyTo(array, arrayIndex); - int ICollection.Count - { - get { return this.Count; } - } - } + int ICollection.Count => this.Count; } diff --git a/Hjson/JsonPrimitive.cs b/Hjson/JsonPrimitive.cs index 04cd130..566bd47 100644 --- a/Hjson/JsonPrimitive.cs +++ b/Hjson/JsonPrimitive.cs @@ -5,88 +5,72 @@ using System.IO; using System.Text; -namespace Hjson +namespace Hjson; + +/// Implements a primitive value. +internal class JsonPrimitive : JsonValue { - /// Implements a primitive value. - internal class JsonPrimitive : JsonValue - { object value; /// Initializes a new string. - public JsonPrimitive(string value) { this.value=value; } + public JsonPrimitive(string value) { this.value = value; } /// Initializes a new char. - public JsonPrimitive(char value) { this.value=value.ToString(); } + public JsonPrimitive(char value) { this.value = value.ToString(); } /// Initializes a new bool. - public JsonPrimitive(bool value) { this.value=value; } + public JsonPrimitive(bool value) { this.value = value; } /// Initializes a new decimal. - public JsonPrimitive(decimal value) { this.value=value; } + public JsonPrimitive(decimal value) { this.value = value; } /// Initializes a new double. - public JsonPrimitive(double value) { this.value=value; } + public JsonPrimitive(double value) { this.value = value; } /// Initializes a new float. - public JsonPrimitive(float value) { this.value=(double)value; } + public JsonPrimitive(float value) { this.value = (double)value; } /// Initializes a new long. - public JsonPrimitive(long value) { this.value=value; } + public JsonPrimitive(long value) { this.value = value; } /// Initializes a new int. - public JsonPrimitive(int value) { this.value=(long)value; } + public JsonPrimitive(int value) { this.value = (long)value; } /// Initializes a new byte. - public JsonPrimitive(byte value) { this.value=(long)value; } + public JsonPrimitive(byte value) { this.value = (long)value; } /// Initializes a new short. - public JsonPrimitive(short value) { this.value=(long)value; } + public JsonPrimitive(short value) { this.value = (long)value; } JsonPrimitive() { } - public static new JsonPrimitive FromObject(object value) { return new JsonPrimitive { value=value }; } + public static new JsonPrimitive FromObject(object value) { return new JsonPrimitive { value = value }; } - internal object Value - { - get { return value; } - } + // Using property + internal object Value => value; /// The type of this value. public override JsonType JsonType { - get - { - if (value==null) return JsonType.String; + get + { + if (value == null) return JsonType.String; - var type=value.GetType(); - if (type==typeof(Boolean)) return JsonType.Boolean; - if (type==typeof(String)) return JsonType.String; - if (type==typeof(Byte) || - type==typeof(SByte) || - type==typeof(Int16) || - type==typeof(UInt16) || - type==typeof(Int32) || - type==typeof(UInt32) || - type==typeof(Int64) || - type==typeof(UInt64) || - type==typeof(Single) || - type==typeof(Double) || - type==typeof(Decimal)) return JsonType.Number; - return JsonType.Unknown; - } + var type = value.GetType(); + if (type == typeof(bool)) return JsonType.Boolean; + if (type == typeof(string)) return JsonType.String; + if (type == typeof(byte) || + type == typeof(sbyte) || + type == typeof(short) || + type == typeof(ushort) || + type == typeof(int) || + type == typeof(uint) || + type == typeof(long) || + type == typeof(ulong) || + type == typeof(float) || + type == typeof(double) || + type == typeof(decimal)) return JsonType.Number; + return JsonType.Unknown; + } } internal string GetRawString() { - switch (JsonType) - { - case JsonType.String: - return ((string)value)??""; - case JsonType.Number: -#if __MonoCS__ // mono bug ca 2014 - if (value is decimal) - { - var res=((IFormattable)value).ToString("G", NumberFormatInfo.InvariantInfo); - while (res.EndsWith("0")) res=res.Substring(0, res.Length-1); - if (res.EndsWith(".") || res.EndsWith("e", StringComparison.OrdinalIgnoreCase)) res=res.Substring(0, res.Length-1); - return res.ToLowerInvariant(); - } -#endif - // use ToLowerInvariant() to convert E to e - return ((IFormattable)value).ToString("G", NumberFormatInfo.InvariantInfo).ToLowerInvariant(); - default: - throw new InvalidOperationException(); - } + return JsonType switch + { + JsonType.String => ((string)value) ?? "", + JsonType.Number => ((IFormattable)value).ToString("G", NumberFormatInfo.InvariantInfo).ToLowerInvariant(), + _ => throw new InvalidOperationException(), + }; } - } } diff --git a/Hjson/JsonReader.cs b/Hjson/JsonReader.cs index d7f22ac..320f2e5 100644 --- a/Hjson/JsonReader.cs +++ b/Hjson/JsonReader.cs @@ -6,95 +6,89 @@ using System.Linq; using System.Text; -namespace Hjson -{ - using JsonPair=KeyValuePair; +namespace Hjson; - internal class JsonReader : BaseReader - { - public JsonReader(TextReader reader, IJsonReader jsonReader) - : base(reader, jsonReader) - { - } +using JsonPair = KeyValuePair; +internal class JsonReader(TextReader reader, IJsonReader jsonReader) : BaseReader(reader, jsonReader) +{ public JsonValue Read() { - JsonValue v=ReadCore(); - SkipWhite(); - if (ReadChar()>=0) throw ParseError("Extra characters in JSON input"); - return v; + JsonValue v = ReadCore(); + SkipWhite(); + if (ReadChar() >= 0) throw ParseError("Extra characters in JSON input"); + return v; } JsonValue ReadCore() { - int c=SkipPeekChar(); - if (c<0) throw ParseError("Incomplete JSON input"); - switch (c) - { - case '[': - ReadChar(); - if (SkipPeekChar()==']') - { - ReadChar(); - return new JsonArray(); - } - var list=new List(); - for (int i=0; ; i++) - { - if (HasReader) Reader.Index(i); - var value=ReadCore(); - if (HasReader) Reader.Value(value); - list.Add(value); - c=SkipPeekChar(); - if (c!=',') break; - ReadChar(); - } - if (ReadChar()!=']') - throw ParseError("Array must end with ']'"); - return new JsonArray(list); - case '{': - ReadChar(); - if (SkipPeekChar()=='}') - { - ReadChar(); - return new JsonObject(); - } - var obj=new List(); - for (; ; ) - { - if (SkipPeekChar()=='}') { ReadChar(); break; } - if (PeekChar()!='"') throw ParseError("Invalid JSON string literal format"); - string name=ReadStringLiteral(null); - SkipWhite(); - Expect(':'); - SkipWhite(); - if (HasReader) Reader.Key(name); - var value=ReadCore(); - if (HasReader) Reader.Value(value); - obj.Add(new JsonPair(name, value)); - SkipWhite(); - c=ReadChar(); - if (c=='}') break; - //if (c==',') continue; - } - return new JsonObject(obj); - case 't': - Expect("true"); - return true; - case 'f': - Expect("false"); - return false; - case 'n': - Expect("null"); - return (JsonValue)null; - case '"': - return ReadStringLiteral(null); - default: - if (c>='0' && c<='9' || c=='-') - return ReadNumericLiteral(); - else - throw ParseError(String.Format("Unexpected character '{0}'", (char)c)); - } + int c = SkipPeekChar(); + if (c < 0) throw ParseError("Incomplete JSON input"); + switch (c) + { + case '[': + ReadChar(); + if (SkipPeekChar() == ']') + { + ReadChar(); + return new JsonArray(); + } + var list = new List(); + for (int i = 0; ; i++) + { + if (HasReader) Reader.Index(i); + var value = ReadCore(); + if (HasReader) Reader.Value(value); + list.Add(value); + c = SkipPeekChar(); + if (c != ',') break; + ReadChar(); + } + if (ReadChar() != ']') + throw ParseError("Array must end with ']'"); + return new JsonArray(list); + case '{': + ReadChar(); + if (SkipPeekChar() == '}') + { + ReadChar(); + return new JsonObject(); + } + var obj = new List(); + for (; ; ) + { + if (SkipPeekChar() == '}') { ReadChar(); break; } + if (PeekChar() != '"') throw ParseError("Invalid JSON string literal format"); + string name = ReadStringLiteral(null); + SkipWhite(); + Expect(':'); + SkipWhite(); + if (HasReader) Reader.Key(name); + var value = ReadCore(); + if (HasReader) Reader.Value(value); + obj.Add(new JsonPair(name, value)); + SkipWhite(); + c = ReadChar(); + if (c == '}') break; + //if (c==',') continue; + } + return new JsonObject(obj); + case 't': + Expect("true"); + return true; + case 'f': + Expect("false"); + return false; + case 'n': + Expect("null"); + return (JsonValue)null; + case '"': + return ReadStringLiteral(null); + default: + if (c is '-' || (c is >= '0' and <= '9')) + return ReadNumericLiteral(); + else + throw ParseError($"Unexpected character '{(char)c}'"); + } } - } } diff --git a/Hjson/JsonType.cs b/Hjson/JsonType.cs index 9ee1ab4..d0d1aa8 100644 --- a/Hjson/JsonType.cs +++ b/Hjson/JsonType.cs @@ -1,9 +1,9 @@ -namespace Hjson +namespace Hjson; + +/// Defines the known json types. +/// There is no null type as the primitive will be null instead of the JsonPrimitive containing null. +public enum JsonType { - /// Defines the known json types. - /// There is no null type as the primitive will be null instead of the JsonPrimitive containing null. - public enum JsonType - { /// Json value of type string. String, /// Json value of type number. @@ -16,5 +16,4 @@ public enum JsonType Boolean, /// Json value of an unknown type. Unknown, - } } diff --git a/Hjson/JsonUtil.cs b/Hjson/JsonUtil.cs index 270559a..6f6739e 100644 --- a/Hjson/JsonUtil.cs +++ b/Hjson/JsonUtil.cs @@ -4,241 +4,176 @@ using System.Linq; using System.Text; -namespace Hjson +namespace Hjson; + +/// Provides Json extension methods. +public static class JsonUtil { - /// Provides Json extension methods. - public static class JsonUtil - { - static Exception failQ(JsonValue forObject, string op) - { - string type=forObject!=null?forObject.JsonType.ToString().ToLower():"null"; - return new Exception("JsonUtil."+op+" not supported for type "+type+"!"); - } + static Exception failQ(JsonValue forObject, string op) => + new($"JsonUtil.{op} not supported for type {(forObject != null ? forObject.JsonType.ToString().ToLower() : "null")}!"); - static Exception failM(Exception e, string key) - { - string msg=e.Message; - if (msg.EndsWith("!")) msg=msg.Substring(0, msg.Length-1); - return new Exception(msg+" [key:"+key+"]!"); - } + static Exception failM(Exception e, string key) => + new($"{(e.Message.EndsWith('!') ? e.Message[..^1] : e.Message)} [key:{key}]!"); /// For JsonValues with type boolean, this method will return its /// value as bool, otherwise it will throw. - public static bool Qb(this JsonValue json) - { - if (json!=null && json.JsonType==JsonType.Boolean) return (bool)json.ToValue(); - else throw failQ(json, "Qb"); - } + public static bool Qb(this JsonValue json) => + json != null && json.JsonType == JsonType.Boolean ? (bool)json.ToValue() : throw failQ(json, "Qb"); /// Gets the value of the member specified by key, then calls . /// If the object does not contain the key, the defaultValue is returned. - public static bool Qb(this JsonObject json, string key, bool defaultValue=false) + public static bool Qb(this JsonObject json, string key, bool defaultValue = false) { - try - { - if (json.ContainsKey(key)) return json[key].Qb(); - else return defaultValue; - } - catch (Exception e) { throw failM(e, key); } + try { return json.ContainsKey(key) ? json[key].Qb() : defaultValue; } + catch (Exception e) { throw failM(e, key); } } /// For JsonValues with type number, this method will return its /// value as int, otherwise it will throw. - public static int Qi(this JsonValue json) - { - if (json!=null && json.JsonType==JsonType.Number) return Convert.ToInt32(json.ToValue()); - else throw failQ(json, "Qi"); - } + public static int Qi(this JsonValue json) => + json != null && json.JsonType == JsonType.Number ? Convert.ToInt32(json.ToValue()) : throw failQ(json, "Qi"); /// Gets the value of the member specified by key, then calls . /// If the object does not contain the key, the defaultValue is returned. - public static int Qi(this JsonObject json, string key, int defaultValue=0) + public static int Qi(this JsonObject json, string key, int defaultValue = 0) { - try - { - if (json.ContainsKey(key)) return json[key].Qi(); - else return defaultValue; - } - catch (Exception e) { throw failM(e, key); } + try { return json.ContainsKey(key) ? json[key].Qi() : defaultValue; } + catch (Exception e) { throw failM(e, key); } } /// For JsonValues with type number, this method will return its /// value as long, otherwise it will throw. - public static long Ql(this JsonValue json) - { - if (json!=null && json.JsonType==JsonType.Number) return Convert.ToInt64(json.ToValue()); - else throw failQ(json, "Ql"); - } + public static long Ql(this JsonValue json) => + json != null && json.JsonType == JsonType.Number ? Convert.ToInt64(json.ToValue()) : throw failQ(json, "Ql"); /// Gets the value of the member specified by key, then calls . /// If the object does not contain the key, the defaultValue is returned. - public static long Ql(this JsonObject json, string key, long defaultValue=0) + public static long Ql(this JsonObject json, string key, long defaultValue = 0) { - try - { - if (json.ContainsKey(key)) return json[key].Ql(); - else return defaultValue; - } - catch (Exception e) { throw failM(e, key); } + try { return json.ContainsKey(key) ? json[key].Ql() : defaultValue; } + catch (Exception e) { throw failM(e, key); } } /// For JsonValues with type number, this method will return its /// value as double, otherwise it will throw. - public static double Qd(this JsonValue json) - { - if (json!=null && json.JsonType==JsonType.Number) return Convert.ToDouble(json.ToValue()); - else throw failQ(json, "Qd"); - } + public static double Qd(this JsonValue json) => + json != null && json.JsonType == JsonType.Number ? Convert.ToDouble(json.ToValue()) : throw failQ(json, "Qd"); /// Gets the value of the member specified by key, then calls . /// If the object does not contain the key, the defaultValue is returned. - public static double Qd(this JsonObject json, string key, double defaultValue=0) + public static double Qd(this JsonObject json, string key, double defaultValue = 0) { - try - { - if (json.ContainsKey(key)) return json[key].Qd(); - else return defaultValue; - } - catch (Exception e) { throw failM(e, key); } + try { return json.ContainsKey(key) ? json[key].Qd() : defaultValue; } + catch (Exception e) { throw failM(e, key); } } /// For JsonValues with type string, this method will return its /// value as string, otherwise it will throw. Use /// to get a string value from number or boolean types as well. - public static string Qs(this JsonValue json) - { - if (json==null) return null; - else if (json.JsonType==JsonType.String) return (string)json; - else throw failQ(json, "Qs"); - } + public static string Qs(this JsonValue json) => + json?.JsonType == JsonType.String ? (string)json : json == null ? null : throw failQ(json, "Qs"); /// Gets the value of the member specified by key, then calls . /// If the object does not contain the key, the defaultValue is returned. - public static string Qs(this JsonObject json, string key, string defaultValue="") + public static string Qs(this JsonObject json, string key, string defaultValue = "") { - try - { - if (json.ContainsKey(key)) return json[key].Qs(); - else return defaultValue; - } - catch (Exception e) { throw failM(e, key); } + try { return json.ContainsKey(key) ? json[key].Qs() : defaultValue; } + catch (Exception e) { throw failM(e, key); } } /// For JsonValues with type string, number or boolean, this method will return /// its value as a string (converted if necessary). For arrays or objects it will throw. - public static string Qstr(this JsonValue json) + public static string Qstr(this JsonValue json) => json?.JsonType switch { - if (json==null) return null; - else if (json.JsonType==JsonType.String) return (string)json; - else if (json.JsonType==JsonType.Boolean || json.JsonType==JsonType.Number) return json.ToString(); - else throw failQ(json, "Qstr"); - } + null => null, + JsonType.String => (string)json, + JsonType.Boolean or JsonType.Number => json.ToString(), + _ => throw failQ(json, "Qstr") + }; /// Gets the value of the member specified by key, then, /// for string, number or boolean JsonValues, this method will return /// its value as a string (converted if necessary). - public static string Qstr(this JsonObject json, string key, string defaultValue="") + public static string Qstr(this JsonObject json, string key, string defaultValue = "") { - try - { - if (json.ContainsKey(key)) return json[key].Qstr(); - else return defaultValue; - } - catch (Exception e) { throw failM(e, key); } + try { return json.ContainsKey(key) ? json[key].Qstr() : defaultValue; } + catch (Exception e) { throw failM(e, key); } } /// Gets the JsonValue of the member specified by key. - public static JsonValue Qv(this JsonObject json, string key) - { - if (json.ContainsKey(key)) return json[key]; - else return null; - } + public static JsonValue Qv(this JsonObject json, string key) => + json.TryGetValue(key, out var val) ? val : null; /// Gets a JsonObject from a JsonObject. public static JsonObject Qo(this JsonObject json, string key) { - try { return (JsonObject)json.Qv(key); } - catch (Exception e) { throw failM(e, key); } + try { return (JsonObject)json.Qv(key); } + catch (Exception e) { throw failM(e, key); } } /// Gets the JsonObject from a JsonValue. public static JsonObject Qo(this JsonValue json) { - try { return (JsonObject)json; } - catch { throw failQ(json, "Qo"); } + try { return (JsonObject)json; } + catch { throw failQ(json, "Qo"); } } /// Gets a JsonArray from a JsonObject. public static JsonArray Qa(this JsonObject json, string key) { - try { return (JsonArray)json.Qv(key); } - catch (Exception e) { throw failM(e, key); } + try { return (JsonArray)json.Qv(key); } + catch (Exception e) { throw failM(e, key); } } /// Gets the JsonArray from a JsonValue. public static JsonArray Qa(this JsonValue json) { - try { return (JsonArray)json; } - catch { throw failQ(json, "Qo"); } + try { return (JsonArray)json; } + catch { throw failQ(json, "Qa"); } } /// Enumerates JsonObjects from a JsonObject. - public static IEnumerable> Qqo(this JsonObject json) - { - return json.Select(x => new KeyValuePair(x.Key, x.Value.Qo())); - } + public static IEnumerable> Qqo(this JsonObject json) => + json.Select(x => new KeyValuePair(x.Key, x.Value.Qo())); - static readonly DateTime UnixEpochUtc=new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + static readonly DateTime UnixEpochUtc = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); /// Convert the date to json (unix epoch date offset). - public static long ToJsonDate(this DateTime dt) - { - if (dt==DateTime.MinValue) return 0; - else return (long)(dt.ToUniversalTime()-UnixEpochUtc).TotalMilliseconds; - } + public static long ToJsonDate(this DateTime dt) => + dt == DateTime.MinValue ? 0 : (long)(dt.ToUniversalTime() - UnixEpochUtc).TotalMilliseconds; /// Convert the json date (unix epoch date offset) to a DateTime. - public static DateTime ToDateTime(long unixEpochDateOffset) - { - if (unixEpochDateOffset>0) return UnixEpochUtc.AddMilliseconds(unixEpochDateOffset); - else return DateTime.MinValue; - } + public static DateTime ToDateTime(long unixEpochDateOffset) => + unixEpochDateOffset > 0 ? UnixEpochUtc.AddMilliseconds(unixEpochDateOffset) : DateTime.MinValue; /// Convert the date to JSON/ISO 8601, compatible with ES5 Date.toJSON(). /// Use DateTime.Parse() to convert back (will be of local kind). - public static string ToJson(this DateTime dt) - { - if (dt==DateTime.MinValue) return ""; - else if (dt.Kind==DateTimeKind.Unspecified) return dt.ToString("yyyy-MM-ddTHH:mm:ss.fff"); - else return dt.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); - } + public static string ToJson(this DateTime dt) => dt == DateTime.MinValue ? "" : + dt.Kind == DateTimeKind.Unspecified ? dt.ToString("yyyy-MM-ddTHH:mm:ss.fff") : + dt.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); /// Convert the date to a precise string representations (ten millionths of a second). /// Use DateTime.Parse() to convert back (will be of local kind). - public static string ToPrecise(this DateTime dt) - { - if (dt==DateTime.MinValue) return ""; - else if (dt.Kind==DateTimeKind.Unspecified) return dt.ToString("yyyy-MM-ddTHH:mm:ss.fffffff"); - else return dt.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ"); - } + public static string ToPrecise(this DateTime dt) => dt == DateTime.MinValue ? "" : + dt.Kind == DateTimeKind.Unspecified ? dt.ToString("yyyy-MM-ddTHH:mm:ss.fffffff") : + dt.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ"); /// Convert the timespan to JSON/ISO 8601. public static string ToJson(this TimeSpan ts) { - StringBuilder rc=new StringBuilder(), rct=new StringBuilder(); - if (ts0) rc.Append(ts.Days.ToString(CultureInfo.InvariantCulture)+'D'); - if (ts.Hours>0) rct.Append(ts.Hours.ToString(CultureInfo.InvariantCulture)+'H'); - if (ts.Minutes>0) rct.Append(ts.Minutes.ToString(CultureInfo.InvariantCulture)+'M'); - if (ts.Seconds>0 || ts.Milliseconds>0) - { - rct.Append(ts.Seconds.ToString(CultureInfo.InvariantCulture)); - if (ts.Milliseconds>0) rct.Append("."+ts.Milliseconds.ToString(CultureInfo.InvariantCulture)); - rct.Append('S'); - } - if (rct.Length>0) { rc.Append('T'); rc.Append(rct.ToString()); } - return rc.ToString(); + StringBuilder rc = new(), rct = new(); + if (ts < TimeSpan.Zero) { rc.Append('-'); ts = ts.Negate(); } + rc.Append('P'); + if (ts.Days > 0) rc.Append($"{ts.Days.ToString(CultureInfo.InvariantCulture)}D"); + if (ts.Hours > 0) rct.Append($"{ts.Hours.ToString(CultureInfo.InvariantCulture)}H"); + if (ts.Minutes > 0) rct.Append($"{ts.Minutes.ToString(CultureInfo.InvariantCulture)}M"); + if (ts.Seconds > 0 || ts.Milliseconds > 0) + { + rct.Append(ts.Seconds.ToString(CultureInfo.InvariantCulture)); + if (ts.Milliseconds > 0) rct.Append($".{ts.Milliseconds.ToString(CultureInfo.InvariantCulture)}"); + rct.Append('S'); + } + if (rct.Length > 0) { rc.Append('T'); rc.Append(rct.ToString()); } + return rc.ToString(); } - } } diff --git a/Hjson/JsonValue.cs b/Hjson/JsonValue.cs index 4e3cf97..fc984a8 100644 --- a/Hjson/JsonValue.cs +++ b/Hjson/JsonValue.cs @@ -5,41 +5,38 @@ using System.Linq; using System.Text; -namespace Hjson -{ - using JsonPair=KeyValuePair; +namespace Hjson; + +using JsonPair = KeyValuePair; - /// The ToString format. - public enum Stringify - { +/// The ToString format. +public enum Stringify +{ /// JSON (no whitespace). Plain, /// Formatted JSON. Formatted, /// Hjson. Hjson, - } +} - /// - /// JsonValue is the abstract base class for all values (string, number, true, false, null, object or array). - /// - public abstract class JsonValue : IEnumerable - { - internal static string eol=Environment.NewLine; +/// +/// JsonValue is the abstract base class for all values (string, number, true, false, null, object or array). +/// +public abstract class JsonValue : IEnumerable +{ + internal static string eol = Environment.NewLine; /// Gets or sets the newline charater(s). /// Defaults to Environment.NewLine. public static string Eol { - get { return eol; } - set { if (value=="\r\n" || value=="\n") eol=value; } + get { return eol; } + set { if (value == "\r\n" || value == "\n") eol = value; } } /// Gets the count of the contained items for arrays/objects. - public virtual int Count - { - get { throw new InvalidOperationException(); } - } + public virtual int Count => throw new InvalidOperationException(); /// The type of this value. public abstract JsonType JsonType { get; } @@ -47,209 +44,198 @@ public virtual int Count /// Gets or sets the value for the specified index. public virtual JsonValue this[int index] { - get { throw new InvalidOperationException(); } - set { throw new InvalidOperationException(); } + get => throw new InvalidOperationException(); + set => throw new InvalidOperationException(); } /// Gets or sets the value for the specified key. public virtual JsonValue this[string key] { - get { throw new InvalidOperationException(); } - set { throw new InvalidOperationException(); } + get => throw new InvalidOperationException(); + set => throw new InvalidOperationException(); } /// Returns true if the object contains the specified key. - public virtual bool ContainsKey(string key) - { - throw new InvalidOperationException(); - } + public virtual bool ContainsKey(string key) => throw new InvalidOperationException(); /// Saves the JSON to a file. - public void Save(string path, Stringify format=Stringify.Plain) + public void Save(string path, Stringify format = Stringify.Plain) { - using (var s=File.CreateText(path)) - Save(s, format); + using (var s = File.CreateText(path)) + Save(s, format); } /// Saves the JSON to a stream. - public void Save(Stream stream, Stringify format=Stringify.Plain) + public void Save(Stream stream, Stringify format = Stringify.Plain) { - if (stream==null) throw new ArgumentNullException("stream"); - Save(new StreamWriter(stream), format); + if (stream == null) throw new ArgumentNullException("stream"); + Save(new StreamWriter(stream), format); } /// Saves the JSON to a TextWriter. - public void Save(TextWriter textWriter, Stringify format=Stringify.Plain) + public void Save(TextWriter textWriter, Stringify format = Stringify.Plain) { - if (textWriter==null) throw new ArgumentNullException("textWriter"); - if (format==Stringify.Hjson) HjsonValue.Save(this, textWriter); - else new JsonWriter(format==Stringify.Formatted).Save(this, textWriter, 0); - textWriter.Flush(); + if (textWriter == null) throw new ArgumentNullException("textWriter"); + if (format == Stringify.Hjson) HjsonValue.Save(this, textWriter); + else new JsonWriter(format == Stringify.Formatted).Save(this, textWriter, 0); + textWriter.Flush(); } /// Saves as Hjson to a string. public string ToString(HjsonOptions options) { - if (options==null) throw new ArgumentNullException("options"); - var sw=new StringWriter(); - HjsonValue.Save(this, sw, options); - return sw.ToString(); + if (options == null) throw new ArgumentNullException("options"); + var sw = new StringWriter(); + HjsonValue.Save(this, sw, options); + return sw.ToString(); } /// Saves the JSON to a string. public string ToString(Stringify format) { - var sw=new StringWriter(); - Save(sw, format); - return sw.ToString(); + var sw = new StringWriter(); + Save(sw, format); + return sw.ToString(); } /// Saves the JSON to a string. public override string ToString() { - return ToString(Stringify.Plain); + return ToString(Stringify.Plain); } /// Returns the contained primitive value. public object ToValue() { - return ((JsonPrimitive)this).Value; + return ((JsonPrimitive)this).Value; } /// Wraps an unknown object into a JSON value (to be used with DSF). public static JsonValue FromObject(object value) { - return JsonPrimitive.FromObject(value); + return JsonPrimitive.FromObject(value); } - IEnumerator IEnumerable.GetEnumerator() - { - throw new InvalidOperationException(); - } + IEnumerator IEnumerable.GetEnumerator() => throw new InvalidOperationException(); /// Loads JSON from a file. public static JsonValue Load(string path) { - using (var s=File.OpenRead(path)) - return Load(s); + using (var s = File.OpenRead(path)) + return Load(s); } /// Loads JSON from a stream. public static JsonValue Load(Stream stream) { - if (stream==null) throw new ArgumentNullException("stream"); - return Load(new StreamReader(stream, true)); + if (stream == null) throw new ArgumentNullException("stream"); + return Load(new StreamReader(stream, true)); } /// Loads JSON from a TextReader. - public static JsonValue Load(TextReader textReader, IJsonReader jsonReader=null) + public static JsonValue Load(TextReader textReader, IJsonReader jsonReader = null) { - if (textReader==null) throw new ArgumentNullException("textReader"); - var ret=new JsonReader(textReader, jsonReader).Read(); - return ret; + if (textReader == null) throw new ArgumentNullException("textReader"); + var ret = new JsonReader(textReader, jsonReader).Read(); + return ret; } /// Parses the specified JSON string. public static JsonValue Parse(string jsonString) { - if (jsonString==null) - throw new ArgumentNullException("jsonString"); - return Load(new StringReader(jsonString)); + if (jsonString == null) + throw new ArgumentNullException("jsonString"); + return Load(new StringReader(jsonString)); } // CLI -> JsonValue /// Converts from bool. - public static implicit operator JsonValue(bool value) { return new JsonPrimitive(value); } + public static implicit operator JsonValue(bool value) => new JsonPrimitive(value); /// Converts from byte. - public static implicit operator JsonValue(byte value) { return new JsonPrimitive(value); } + public static implicit operator JsonValue(byte value) => new JsonPrimitive(value); /// Converts from char. - public static implicit operator JsonValue(char value) { return new JsonPrimitive(value); } + public static implicit operator JsonValue(char value) => new JsonPrimitive(value); /// Converts from decimal. - public static implicit operator JsonValue(decimal value) { return new JsonPrimitive(value); } + public static implicit operator JsonValue(decimal value) => new JsonPrimitive(value); /// Converts from double. - public static implicit operator JsonValue(double value) { return new JsonPrimitive(value); } + public static implicit operator JsonValue(double value) => new JsonPrimitive(value); /// Converts from float. - public static implicit operator JsonValue(float value) { return new JsonPrimitive(value); } + public static implicit operator JsonValue(float value) => new JsonPrimitive(value); /// Converts from int. - public static implicit operator JsonValue(int value) { return new JsonPrimitive(value); } + public static implicit operator JsonValue(int value) => new JsonPrimitive(value); /// Converts from long. - public static implicit operator JsonValue(long value) { return new JsonPrimitive(value); } + public static implicit operator JsonValue(long value) => new JsonPrimitive(value); /// Converts from short. - public static implicit operator JsonValue(short value) { return new JsonPrimitive(value); } + public static implicit operator JsonValue(short value) => new JsonPrimitive(value); /// Converts from string. - public static implicit operator JsonValue(string value) { return new JsonPrimitive(value); } + public static implicit operator JsonValue(string value) => new JsonPrimitive(value); // JsonValue -> CLI /// Converts to bool. Also see . public static implicit operator bool(JsonValue value) { - if (value==null) throw new ArgumentNullException("value"); - return Convert.ToBoolean(((JsonPrimitive)value).Value); + ArgumentNullException.ThrowIfNull(value); + return Convert.ToBoolean(((JsonPrimitive)value).Value); } /// Converts to byte. Also see . public static implicit operator byte(JsonValue value) { - if (value==null) throw new ArgumentNullException("value"); - return Convert.ToByte(((JsonPrimitive)value).Value); + ArgumentNullException.ThrowIfNull(value); + return Convert.ToByte(((JsonPrimitive)value).Value); } /// Converts to char. Also see . public static implicit operator char(JsonValue value) { - if (value==null) throw new ArgumentNullException("value"); - return Convert.ToChar(((JsonPrimitive)value).Value); + ArgumentNullException.ThrowIfNull(value); + return Convert.ToChar(((JsonPrimitive)value).Value); } /// Converts to decimal. Also see . public static implicit operator decimal(JsonValue value) { - if (value==null) throw new ArgumentNullException("value"); - return Convert.ToDecimal(((JsonPrimitive)value).Value); + ArgumentNullException.ThrowIfNull(value); + return Convert.ToDecimal(((JsonPrimitive)value).Value); } /// Converts to double. Also see . public static implicit operator double(JsonValue value) { - if (value==null) throw new ArgumentNullException("value"); - return Convert.ToDouble(((JsonPrimitive)value).Value); + ArgumentNullException.ThrowIfNull(value); + return Convert.ToDouble(((JsonPrimitive)value).Value); } /// Converts to float. Also see . public static implicit operator float(JsonValue value) { - if (value==null) throw new ArgumentNullException("value"); - return Convert.ToSingle(((JsonPrimitive)value).Value); + ArgumentNullException.ThrowIfNull(value); + return Convert.ToSingle(((JsonPrimitive)value).Value); } /// Converts to int. Also see . public static implicit operator int(JsonValue value) { - if (value==null) throw new ArgumentNullException("value"); - return Convert.ToInt32(((JsonPrimitive)value).Value); + ArgumentNullException.ThrowIfNull(value); + return Convert.ToInt32(((JsonPrimitive)value).Value); } /// Converts to long. Also see . public static implicit operator long(JsonValue value) { - if (value==null) throw new ArgumentNullException("value"); - return Convert.ToInt64(((JsonPrimitive)value).Value); + ArgumentNullException.ThrowIfNull(value); + return Convert.ToInt64(((JsonPrimitive)value).Value); } /// Converts to short. Also see . public static implicit operator short(JsonValue value) { - if (value==null) throw new ArgumentNullException("value"); - return Convert.ToInt16(((JsonPrimitive)value).Value); + ArgumentNullException.ThrowIfNull(value); + return Convert.ToInt16(((JsonPrimitive)value).Value); } /// Converts to string. Also see . - public static implicit operator string(JsonValue value) - { - if (value==null) return null; - return (string)((JsonPrimitive)value).Value; - } - } + public static implicit operator string(JsonValue value) => (string)((JsonPrimitive)value)?.Value; } diff --git a/Hjson/JsonWriter.cs b/Hjson/JsonWriter.cs index 9134b0f..208277c 100644 --- a/Hjson/JsonWriter.cs +++ b/Hjson/JsonWriter.cs @@ -6,133 +6,124 @@ using System.Linq; using System.Text; -namespace Hjson -{ - using JsonPair=KeyValuePair; +namespace Hjson; - internal class JsonWriter - { - bool format; +using JsonPair = KeyValuePair; - public JsonWriter(bool format) - { - this.format=format; - } +internal class JsonWriter(bool format) +{ + readonly bool format = format; void nl(TextWriter tw, int level) { - if (format) - { - tw.Write(JsonValue.eol); - tw.Write(new string(' ', level*2)); - } + if (format) + { + tw.Write(JsonValue.eol); + tw.Write(new string(' ', level * 2)); + } } public void Save(JsonValue value, TextWriter tw, int level) { - bool following=false; - switch (value.JsonType) - { - case JsonType.Object: - if (level>0) nl(tw, level); - tw.Write('{'); - foreach (JsonPair pair in ((JsonObject)value)) - { - if (following) tw.Write(","); - nl(tw, level+1); - tw.Write('\"'); - tw.Write(EscapeString(pair.Key)); - tw.Write("\":"); - var nextType=pair.Value!=null?(JsonType?)pair.Value.JsonType:null; - if (format && nextType!=JsonType.Array && nextType!=JsonType.Object) tw.Write(" "); - if (pair.Value==null) tw.Write("null"); - else Save(pair.Value, tw, level+1); - following=true; - } - if (following) nl(tw, level); - tw.Write('}'); - break; - case JsonType.Array: - if (level>0) nl(tw, level); - tw.Write('['); - foreach (JsonValue v in ((JsonArray)value)) - { - if (following) tw.Write(","); - if (v!=null) - { - if (v.JsonType!=JsonType.Array && v.JsonType!=JsonType.Object) nl(tw, level+1); - Save(v, tw, level+1); - } - else - { - nl(tw, level+1); - tw.Write("null"); - } - following=true; - } - if (following) nl(tw, level); - tw.Write(']'); - break; - case JsonType.Boolean: - tw.Write((bool)value?"true":"false"); - break; - case JsonType.String: - tw.Write('"'); - tw.Write(EscapeString(((JsonPrimitive)value).GetRawString())); - tw.Write('"'); - break; - default: - tw.Write(((JsonPrimitive)value).GetRawString()); - break; - } + bool following = false; + switch (value.JsonType) + { + case JsonType.Object: + if (level > 0) nl(tw, level); + tw.Write('{'); + foreach (JsonPair pair in ((JsonObject)value)) + { + if (following) tw.Write(","); + nl(tw, level + 1); + tw.Write('\"'); + tw.Write(EscapeString(pair.Key)); + tw.Write("\":"); + var nextType = pair.Value != null ? (JsonType?)pair.Value.JsonType : null; + if (format && nextType != JsonType.Array && nextType != JsonType.Object) tw.Write(" "); + if (pair.Value == null) tw.Write("null"); + else Save(pair.Value, tw, level + 1); + following = true; + } + if (following) nl(tw, level); + tw.Write('}'); + break; + case JsonType.Array: + if (level > 0) nl(tw, level); + tw.Write('['); + foreach (JsonValue v in ((JsonArray)value)) + { + if (following) tw.Write(","); + if (v != null) + { + if (v.JsonType != JsonType.Array && v.JsonType != JsonType.Object) nl(tw, level + 1); + Save(v, tw, level + 1); + } + else + { + nl(tw, level + 1); + tw.Write("null"); + } + following = true; + } + if (following) nl(tw, level); + tw.Write(']'); + break; + case JsonType.Boolean: + tw.Write((bool)value ? "true" : "false"); + break; + case JsonType.String: + tw.Write('"'); + tw.Write(EscapeString(((JsonPrimitive)value).GetRawString())); + tw.Write('"'); + break; + default: + tw.Write(((JsonPrimitive)value).GetRawString()); + break; + } } internal static string EscapeString(string src) { - if (src==null) return null; + if (src == null) return null; - for (int i=0; i0) sb.Append(src, 0, i); - return doEscapeString(sb, src, i); + if (getEscapedChar(src[i]) != null) + { + StringBuilder sb = new(); + if (i > 0) sb.Append(src, 0, i); + return doEscapeString(sb, src, i); + } } - } - return src; + return src; } static string doEscapeString(StringBuilder sb, string src, int cur) { - int start=cur; - for (int i=cur; i c switch { - switch (c) - { - case '\"': return "\\\""; - case '\t': return "\\t"; - case '\n': return "\\n"; - case '\r': return "\\r"; - case '\f': return "\\f"; - case '\b': return "\\b"; - case '\\': return "\\\\"; - default: return null; - } - } - } + '\"' => "\\\"", + '\t' => "\\t", + '\n' => "\\n", + '\r' => "\\r", + '\f' => "\\f", + '\b' => "\\b", + '\\' => "\\\\", + _ => null, + }; } diff --git a/Hjson/Properties/AssemblyInfo.cs b/Hjson/Properties/AssemblyInfo.cs index 2069807..ff59006 100644 --- a/Hjson/Properties/AssemblyInfo.cs +++ b/Hjson/Properties/AssemblyInfo.cs @@ -4,4 +4,4 @@ [assembly: System.Reflection.AssemblyCopyright("See LICENSE")] [assembly: System.Reflection.AssemblyFileVersion("3.0.1.0")] [assembly: System.Reflection.AssemblyTitle("Hjson.dll")] -[assembly: System.CLSCompliant(true)] +[assembly: System.CLSCompliant(true)] \ No newline at end of file diff --git a/Hjsonc.sln b/Hjsonc.sln deleted file mode 100644 index 7346c50..0000000 --- a/Hjsonc.sln +++ /dev/null @@ -1,28 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26114.2 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Hjsonc", "cli\Hjsonc.csproj", "{30E9C2B9-CD8D-4BE8-9596-29831C4E034F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hjson", "Hjson\Hjson.csproj", "{FF9E2637-8BD3-4F8D-B563-D105B10D5354}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {30E9C2B9-CD8D-4BE8-9596-29831C4E034F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {30E9C2B9-CD8D-4BE8-9596-29831C4E034F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {30E9C2B9-CD8D-4BE8-9596-29831C4E034F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {30E9C2B9-CD8D-4BE8-9596-29831C4E034F}.Release|Any CPU.Build.0 = Release|Any CPU - {FF9E2637-8BD3-4F8D-B563-D105B10D5354}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FF9E2637-8BD3-4F8D-B563-D105B10D5354}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FF9E2637-8BD3-4F8D-B563-D105B10D5354}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FF9E2637-8BD3-4F8D-B563-D105B10D5354}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/README.md b/README.md index f419b7b..527adae 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ [![nuget version](https://img.shields.io/nuget/v/Hjson.svg?style=flat-square)](https://www.nuget.org/packages/Hjson/) [![License](https://img.shields.io/github/license/hjson/hjson-cs.svg?style=flat-square)](https://github.com/hjson/hjson-cs/blob/master/LICENSE) -[Hjson](http://hjson.org), a user interface for JSON +[Hjson](http://hjson.github.io), a user interface for JSON -![Hjson Intro](http://hjson.org/hjson1.gif) +![Hjson Intro](http://hjson.github.io/hjson1.gif) JSON is easy for humans to read and write... in theory. In practice JSON gives us plenty of opportunities to make mistakes without even realizing it. @@ -38,7 +38,7 @@ This library includes two readers/writers that fully conform to the respective s - JSON - Hjson -The C# implementation of Hjson is based on [System.Json](https://github.com/mono/mono). For other platforms see [hjson.org](http://hjson.org). +The C# implementation of Hjson is based on [System.Json](https://github.com/mono/mono). For other platforms see [hjson.github.io](http://hjson.github.io). # Install from nuget @@ -158,7 +158,7 @@ See [api.md](api.md). A commandline tool to convert from/to Hjson is available in the cli folder. -For other tools see [hjson.org](http://hjson.org). +For other tools see [hjson.github.io](http://hjson.github.io). # Source/Projects diff --git a/Test.sln b/Test.sln deleted file mode 100644 index c417efe..0000000 --- a/Test.sln +++ /dev/null @@ -1,28 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26114.2 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Test", "test\Test.csproj", "{51D33DF6-4DFE-4DED-9A06-098B8FE2B493}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hjson", "Hjson\Hjson.csproj", "{FF9E2637-8BD3-4F8D-B563-D105B10D5354}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {51D33DF6-4DFE-4DED-9A06-098B8FE2B493}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {51D33DF6-4DFE-4DED-9A06-098B8FE2B493}.Debug|Any CPU.Build.0 = Debug|Any CPU - {51D33DF6-4DFE-4DED-9A06-098B8FE2B493}.Release|Any CPU.ActiveCfg = Release|Any CPU - {51D33DF6-4DFE-4DED-9A06-098B8FE2B493}.Release|Any CPU.Build.0 = Release|Any CPU - {FF9E2637-8BD3-4F8D-B563-D105B10D5354}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FF9E2637-8BD3-4F8D-B563-D105B10D5354}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FF9E2637-8BD3-4F8D-B563-D105B10D5354}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FF9E2637-8BD3-4F8D-B563-D105B10D5354}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/build-core b/build-core index ac943dd..c718d52 100755 --- a/build-core +++ b/build-core @@ -19,7 +19,7 @@ dotnet build cli -c Release dotnet build test -c Release # test -dotnet run -p test/test.csproj -- test/assets +dotnet run --project test/test.csproj -- test/assets echo ------------------ echo pkg diff --git a/build-mono b/build-mono deleted file mode 100755 index b49ae6a..0000000 --- a/build-mono +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -set -e # exit script on error -BASE=`dirname "$(readlink -f "$0")"` -cd $BASE - -VERSION=`cat Hjson/Hjson.csproj | sed -rn 's/.*VersionPrefix>(.*)<\/.*/\1/p'` -VERSION2=`cat Hjson/Properties/AssemblyInfo.cs | sed -rn 's/.*AssemblyFileVersion\("(.*).0"\).*/\1/p'` -if [[ "$VERSION" != "$VERSION2" ]]; then echo error: version mismatch $VERSION:$VERSION2; exit; fi - -echo ------------------ -echo build .net - -xbuild /p:Configuration=Release legacy/Hjson.sln - -# test -mono legacy/test/bin/Release/Test.exe test/assets - -echo ------------------ -echo pkg - -mkdir -p nuget -nuget pack Hjson.nuspec -Version $VERSION -OutputDirectory nuget/ diff --git a/cli/Program.cs b/cli/Program.cs index e2af64d..9ce6fb7 100644 --- a/cli/Program.cs +++ b/cli/Program.cs @@ -1,53 +1,49 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +#nullable enable +using System; + using Hjson; -namespace HjsonCli +string? file = null; +var todo = Stringify.Hjson; +bool err = false, roundtrip = false, rootBraces = true; + +foreach (var arg in args) { - class Program - { - static int Main(string[] args) + switch (arg) { - string file=null; - Stringify todo=Stringify.Hjson; - bool err=false, roundtrip=false, rootBraces=true; - foreach (string arg in args) - { - if (arg=="-j") todo=Stringify.Formatted; - else if (arg=="-c") todo=Stringify.Plain; - else if (arg=="-h") todo=Stringify.Hjson; - else if (arg=="-r") { roundtrip=true; todo=Stringify.Hjson; } - else if (arg=="-n") rootBraces=false; - else if (!arg.StartsWith("-")) - { - if (file==null) file=arg; - else err=true; - } - else err=true; - } - - if (err || file==null) - { - Console.WriteLine("hjsonc [OPTION] FILE"); - Console.WriteLine("Options:"); - Console.WriteLine(" -h Hjson output (default)"); - Console.WriteLine(" -r Hjson output, round trip with comments"); - Console.WriteLine(" -j JSON output (formatted)"); - Console.WriteLine(" -c JSON output (compact)"); - Console.WriteLine(" -n omit braces for the root object (Hjson)."); - return 1; - } - - JsonValue data=HjsonValue.Load(file, new HjsonOptions { KeepWsc=roundtrip }); - if (todo==Stringify.Hjson) - Console.WriteLine(data.ToString(new HjsonOptions { KeepWsc=roundtrip, EmitRootBraces=rootBraces })); - else - Console.WriteLine(data.ToString(todo)); - return 0; + case "-j": todo = Stringify.Formatted; break; + case "-c": todo = Stringify.Plain; break; + case "-h": todo = Stringify.Hjson; break; + case "-r": roundtrip = true; todo = Stringify.Hjson; break; + case "-n": rootBraces = false; break; + default: + if (!arg.StartsWith('-') && file is null) + file = arg; + else + err = true; + break; } - } } + +if (err || file is null) +{ + Console.WriteLine(""" + hjsonc [OPTION] FILE + Options: + -h Hjson output (default) + -r Hjson output, round trip with comments + -j JSON output (formatted) + -c JSON output (compact) + -n omit braces for the root object (Hjson). + """); + return 1; +} + +var data = HjsonValue.Load(file, new HjsonOptions { KeepWsc = roundtrip }); + +if (todo == Stringify.Hjson) + Console.WriteLine(data.ToString(new HjsonOptions { KeepWsc = roundtrip, EmitRootBraces = rootBraces })); +else + Console.WriteLine(data.ToString(todo)); + +return 0; \ No newline at end of file diff --git a/cli/cli.csproj b/cli/cli.csproj index fd139ed..0dbdc3d 100755 --- a/cli/cli.csproj +++ b/cli/cli.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.0 + net10.0 cli cli diff --git a/legacy/Hjson.sln b/legacy/Hjson.sln deleted file mode 100644 index b1932d6..0000000 --- a/legacy/Hjson.sln +++ /dev/null @@ -1,34 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25420.1 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hjson", "Hjson\Hjson.csproj", "{FF9E2637-8BD3-4F8D-B563-D105B10D5354}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hjsonc", "cli\Hjsonc.csproj", "{30E9C2B9-CD8D-4BE8-9596-29831C4E034F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test", "test\Test.csproj", "{FEDD354F-316F-4B07-B622-D788808AB8C2}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {FF9E2637-8BD3-4F8D-B563-D105B10D5354}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FF9E2637-8BD3-4F8D-B563-D105B10D5354}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FF9E2637-8BD3-4F8D-B563-D105B10D5354}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FF9E2637-8BD3-4F8D-B563-D105B10D5354}.Release|Any CPU.Build.0 = Release|Any CPU - {30E9C2B9-CD8D-4BE8-9596-29831C4E034F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {30E9C2B9-CD8D-4BE8-9596-29831C4E034F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {30E9C2B9-CD8D-4BE8-9596-29831C4E034F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {30E9C2B9-CD8D-4BE8-9596-29831C4E034F}.Release|Any CPU.Build.0 = Release|Any CPU - {FEDD354F-316F-4B07-B622-D788808AB8C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FEDD354F-316F-4B07-B622-D788808AB8C2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FEDD354F-316F-4B07-B622-D788808AB8C2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FEDD354F-316F-4B07-B622-D788808AB8C2}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/legacy/Hjson/Hjson.csproj b/legacy/Hjson/Hjson.csproj deleted file mode 100644 index d774e34..0000000 --- a/legacy/Hjson/Hjson.csproj +++ /dev/null @@ -1,73 +0,0 @@ - - - - Debug - AnyCPU - 8.0.30703 - 2.0 - {FF9E2637-8BD3-4F8D-B563-D105B10D5354} - Library - Properties - Hjson - Hjson - v4.0 - 512 - - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - false - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - false - bin\Release\Hjson.xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/legacy/HjsonSample.sln b/legacy/HjsonSample.sln deleted file mode 100644 index eb70bf3..0000000 --- a/legacy/HjsonSample.sln +++ /dev/null @@ -1,22 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25420.1 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HjsonSample", "sample\HjsonSample.csproj", "{9F5BC310-5CBB-4B03-B7A4-9AD81F95DDC0}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {9F5BC310-5CBB-4B03-B7A4-9AD81F95DDC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9F5BC310-5CBB-4B03-B7A4-9AD81F95DDC0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9F5BC310-5CBB-4B03-B7A4-9AD81F95DDC0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9F5BC310-5CBB-4B03-B7A4-9AD81F95DDC0}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/legacy/cli/Hjsonc.csproj b/legacy/cli/Hjsonc.csproj deleted file mode 100644 index dd0c393..0000000 --- a/legacy/cli/Hjsonc.csproj +++ /dev/null @@ -1,62 +0,0 @@ - - - - - Debug - AnyCPU - {30E9C2B9-CD8D-4BE8-9596-29831C4E034F} - Exe - Properties - Hjson.Cli - Hjsonc - v4.5 - 512 - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - {ff9e2637-8bd3-4f8d-b563-d105b10d5354} - Hjson - - - - - - - - \ No newline at end of file diff --git a/legacy/sample/HjsonSample.csproj b/legacy/sample/HjsonSample.csproj deleted file mode 100644 index 87427ac..0000000 --- a/legacy/sample/HjsonSample.csproj +++ /dev/null @@ -1,72 +0,0 @@ - - - - - Debug - AnyCPU - {9F5BC310-5CBB-4B03-B7A4-9AD81F95DDC0} - Exe - Properties - HjsonSample - HjsonSample - v4.6 - 512 - - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - ..\packages\Hjson.2.1.2\lib\net40\Hjson.dll - True - - - - - - - - - - - - - - - Always - - - - - - - - - - - - - \ No newline at end of file diff --git a/legacy/sample/packages.config b/legacy/sample/packages.config deleted file mode 100644 index 644acdf..0000000 --- a/legacy/sample/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/legacy/test/Test.csproj b/legacy/test/Test.csproj deleted file mode 100644 index 15bab1c..0000000 --- a/legacy/test/Test.csproj +++ /dev/null @@ -1,67 +0,0 @@ - - - - - Debug - AnyCPU - {FEDD354F-316F-4B07-B622-D788808AB8C2} - Exe - Properties - Test - Test - v4.5 - 512 - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - - {ff9e2637-8bd3-4f8d-b563-d105b10d5354} - Hjson - - - - - - - - - \ No newline at end of file diff --git a/sample/HjsonSample.sln b/sample/HjsonSample.sln deleted file mode 100644 index 1d51c89..0000000 --- a/sample/HjsonSample.sln +++ /dev/null @@ -1,22 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26114.2 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "HjsonSample", "HjsonSample.csproj", "{FEDD354F-316F-4B07-B622-D788808AB8C2}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {FEDD354F-316F-4B07-B622-D788808AB8C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FEDD354F-316F-4B07-B622-D788808AB8C2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FEDD354F-316F-4B07-B622-D788808AB8C2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FEDD354F-316F-4B07-B622-D788808AB8C2}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/sample/HjsonSample.slnx b/sample/HjsonSample.slnx new file mode 100644 index 0000000..1850a90 --- /dev/null +++ b/sample/HjsonSample.slnx @@ -0,0 +1,3 @@ + + + diff --git a/sample/Program.cs b/sample/Program.cs index fd659ac..039a8e1 100644 --- a/sample/Program.cs +++ b/sample/Program.cs @@ -1,41 +1,32 @@ +#nullable enable + using System; -using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Hjson; -namespace HjsonSample -{ - class Program - { - // note: this sample uses the Hjson library directly. - // Normally you would use nuget. - - static void Main(string[] args) - { - var data=HjsonValue.Load("test.hjson").Qo(); - Console.WriteLine(data.Qs("hello")); - - Console.WriteLine("Saving as test-out.json..."); - HjsonValue.Save(data, "test-out.json"); - - Console.WriteLine("Saving as test-out.hjson..."); - HjsonValue.Save(data, "test-out.hjson"); - - // edit (preserve whitespace and comments) - var wdata=(WscJsonObject)HjsonValue.Load("test.hjson", new HjsonOptions { KeepWsc=true }).Qo(); - - // edit like you normally would - wdata["hugo"]="value"; - // optionally set order and comments: - wdata.Order.Insert(2, "hugo"); - wdata.Comments["hugo"]="just another test"; - - var sw=new StringWriter(); - HjsonValue.Save(wdata, sw, new HjsonOptions() { KeepWsc = true }); - Console.WriteLine(sw.ToString()); - } - } -} +// note: this sample uses the Hjson library directly. +// Normally you would use nuget. + +var data = HjsonValue.Load("test.hjson").Qo(); +Console.WriteLine(data.Qs("hello")); + +Console.WriteLine("Saving as test-out.json..."); +HjsonValue.Save(data, "test-out.json"); + +Console.WriteLine("Saving as test-out.hjson..."); +HjsonValue.Save(data, "test-out.hjson"); + +// edit (preserve whitespace and comments) +var wdata = (WscJsonObject)HjsonValue.Load("test.hjson", new HjsonOptions { KeepWsc = true }).Qo(); + +// edit like you normally would +wdata["hugo"] = "value"; +// optionally set order and comments: +wdata.Order.Insert(2, "hugo"); +wdata.Comments["hugo"] = "just another test"; + +using var sw = new StringWriter(); +HjsonValue.Save(wdata, sw, new HjsonOptions { KeepWsc = true }); +Console.WriteLine(sw.ToString()); + + diff --git a/sample/sample.csproj b/sample/sample.csproj index 98d86ba..ea807cf 100755 --- a/sample/sample.csproj +++ b/sample/sample.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.0 + net10.0 sample sample diff --git a/test/HjsonConvertTests.cs b/test/HjsonConvertTests.cs new file mode 100644 index 0000000..c809866 --- /dev/null +++ b/test/HjsonConvertTests.cs @@ -0,0 +1,624 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Hjson; +using Xunit; + +namespace Hjson.Tests; + +#region Test models + +public class SimpleConfig +{ + public string Name { get; set; } = ""; + public int Port { get; set; } + public bool Enabled { get; set; } +} + +public class RenamedProps +{ + [HjsonPropertyName("server_name")] + public string ServerName { get; set; } = ""; + + [HjsonPropertyName("listen_port")] + public int Port { get; set; } +} + +public class JsonRenamedFallback +{ + [JsonPropertyName("server_name")] + public string ServerName { get; set; } = ""; + + [JsonPropertyName("listen_port")] + public int Port { get; set; } +} + +public class HjsonOverridesJson +{ + [HjsonPropertyName("hjson_name")] + [JsonPropertyName("json_name")] + public string Name { get; set; } = ""; +} + +public class WithIgnored +{ + public string Visible { get; set; } = ""; + + [HjsonIgnore] + public string Secret { get; set; } = ""; +} + +public class WithJsonIgnored +{ + public string Visible { get; set; } = ""; + + [JsonIgnore] + public string Secret { get; set; } = ""; +} + +public class WithIncluded +{ + public string Public { get; set; } = ""; + + [HjsonInclude] + internal string Internal { get; set; } = ""; +} + +public class WithJsonIncluded +{ + public string Public { get; set; } = ""; + + [JsonInclude] + internal string Internal { get; set; } = ""; +} + +public class WithComments +{ + [HjsonComment("The server hostname")] + public string Host { get; set; } = ""; + + [HjsonComment("Port number to listen on")] + public int Port { get; set; } + + public bool Debug { get; set; } +} + +public class WithMultilineComment +{ + [HjsonComment("First line\nSecond line")] + public string Value { get; set; } = ""; +} + +public class WithAllAttributes +{ + [HjsonComment("The display name")] + [HjsonPropertyName("display_name")] + public string Name { get; set; } = ""; + + [HjsonIgnore] + public string Hidden { get; set; } = ""; + + [HjsonComment("Is active?")] + public bool Active { get; set; } +} + +public enum Color { Red, Green, Blue } + +public class WithEnum +{ + public Color Favorite { get; set; } +} + +public class WithNullable +{ + public int? MaybeInt { get; set; } + public string? MaybeString { get; set; } +} + +public class WithCollections +{ + public List Numbers { get; set; } = []; + public string[] Tags { get; set; } = []; +} + +public class WithDictionary +{ + public Dictionary Scores { get; set; } = new(); +} + +public class Nested +{ + public string Name { get; set; } = ""; + public InnerObj Inner { get; set; } = new(); +} + +public class InnerObj +{ + public int Value { get; set; } + public string Label { get; set; } = ""; +} + +public class WithNumericTypes +{ + public byte ByteVal { get; set; } + public short ShortVal { get; set; } + public long LongVal { get; set; } + public float FloatVal { get; set; } + public double DoubleVal { get; set; } + public decimal DecimalVal { get; set; } +} + +#endregion + +public class HjsonConvertTests +{ + // ── Basic Serialization ────────────────────────────────────────── + + [Fact] + public void Serialize_SimpleObject() + { + var config = new SimpleConfig { Name = "test", Port = 8080, Enabled = true }; + string hjson = HjsonConvert.Serialize(config); + + Assert.Contains("Name", hjson); + Assert.Contains("test", hjson); + Assert.Contains("Port", hjson); + Assert.Contains("8080", hjson); + Assert.Contains("Enabled", hjson); + Assert.Contains("true", hjson); + } + + [Fact] + public void Serialize_Null_ReturnsNullString() + { + Assert.Equal("null", HjsonConvert.Serialize(null!)); + } + + // ── Basic Deserialization ──────────────────────────────────────── + + [Fact] + public void Deserialize_SimpleObject() + { + string hjson = @" + { + Name: test + Port: 8080 + Enabled: true + }"; + + var config = HjsonConvert.Deserialize(hjson); + + Assert.Equal("test", config.Name); + Assert.Equal(8080, config.Port); + Assert.True(config.Enabled); + } + + // ── Round-trip ─────────────────────────────────────────────────── + + [Fact] + public void Roundtrip_SimpleObject() + { + var original = new SimpleConfig { Name = "hello", Port = 3000, Enabled = false }; + string hjson = HjsonConvert.Serialize(original); + var restored = HjsonConvert.Deserialize(hjson); + + Assert.Equal(original.Name, restored.Name); + Assert.Equal(original.Port, restored.Port); + Assert.Equal(original.Enabled, restored.Enabled); + } + + // ── HjsonPropertyName ─────────────────────────────────────────── + + [Fact] + public void Serialize_HjsonPropertyName_UsesCustomKey() + { + var obj = new RenamedProps { ServerName = "myhost", Port = 443 }; + string hjson = HjsonConvert.Serialize(obj); + + Assert.Contains("server_name", hjson); + Assert.Contains("listen_port", hjson); + Assert.DoesNotContain("ServerName", hjson); + Assert.DoesNotContain("Port", hjson); + } + + [Fact] + public void Deserialize_HjsonPropertyName_ReadsCustomKey() + { + string hjson = @" + { + server_name: myhost + listen_port: 443 + }"; + + var obj = HjsonConvert.Deserialize(hjson); + + Assert.Equal("myhost", obj.ServerName); + Assert.Equal(443, obj.Port); + } + + // ── JsonPropertyName fallback ─────────────────────────────────── + + [Fact] + public void Serialize_JsonPropertyName_Fallback() + { + var obj = new JsonRenamedFallback { ServerName = "host1", Port = 80 }; + string hjson = HjsonConvert.Serialize(obj); + + Assert.Contains("server_name", hjson); + Assert.Contains("listen_port", hjson); + } + + [Fact] + public void Deserialize_JsonPropertyName_Fallback() + { + string hjson = @" + { + server_name: host1 + listen_port: 80 + }"; + + var obj = HjsonConvert.Deserialize(hjson); + + Assert.Equal("host1", obj.ServerName); + Assert.Equal(80, obj.Port); + } + + // ── Hjson attribute takes priority over Json attribute ─────────── + + [Fact] + public void Serialize_HjsonPropertyName_OverridesJsonPropertyName() + { + var obj = new HjsonOverridesJson { Name = "test" }; + string hjson = HjsonConvert.Serialize(obj); + + Assert.Contains("hjson_name", hjson); + // "hjson_name" contains "json_name" as a substring, so check the key isn't standalone + Assert.DoesNotContain("json_name:", hjson.Replace("hjson_name", "")); + } + + // ── HjsonIgnore ───────────────────────────────────────────────── + + [Fact] + public void Serialize_HjsonIgnore_ExcludesProperty() + { + var obj = new WithIgnored { Visible = "yes", Secret = "s3cr3t" }; + string hjson = HjsonConvert.Serialize(obj); + + Assert.Contains("Visible", hjson); + Assert.DoesNotContain("Secret", hjson); + Assert.DoesNotContain("s3cr3t", hjson); + } + + [Fact] + public void Deserialize_HjsonIgnore_SkipsProperty() + { + string hjson = @" + { + Visible: yes + Secret: should_be_ignored + }"; + + var obj = HjsonConvert.Deserialize(hjson); + + Assert.Equal("yes", obj.Visible); + Assert.Equal("", obj.Secret); // default, not deserialized + } + + // ── JsonIgnore fallback ───────────────────────────────────────── + + [Fact] + public void Serialize_JsonIgnore_Fallback() + { + var obj = new WithJsonIgnored { Visible = "yes", Secret = "s3cr3t" }; + string hjson = HjsonConvert.Serialize(obj); + + Assert.Contains("Visible", hjson); + Assert.DoesNotContain("Secret", hjson); + } + + // ── HjsonInclude ──────────────────────────────────────────────── + + [Fact] + public void Serialize_HjsonInclude_IncludesNonPublic() + { + var obj = new WithIncluded { Public = "pub" }; + // Use reflection to set internal property + typeof(WithIncluded).GetProperty("Internal", + System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)! + .SetValue(obj, "internal_val"); + + string hjson = HjsonConvert.Serialize(obj); + + Assert.Contains("Public", hjson); + Assert.Contains("Internal", hjson); + Assert.Contains("internal_val", hjson); + } + + [Fact] + public void Deserialize_HjsonInclude_SetsNonPublic() + { + string hjson = @" + { + Public: pub + Internal: internal_val + }"; + + var obj = HjsonConvert.Deserialize(hjson); + + Assert.Equal("pub", obj.Public); + Assert.Equal("internal_val", obj.Internal); + } + + // ── JsonInclude fallback ──────────────────────────────────────── + + [Fact] + public void Serialize_JsonInclude_Fallback() + { + var obj = new WithJsonIncluded { Public = "pub" }; + typeof(WithJsonIncluded).GetProperty("Internal", + System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)! + .SetValue(obj, "internal_val"); + + string hjson = HjsonConvert.Serialize(obj); + + Assert.Contains("Internal", hjson); + } + + // ── HjsonComment ──────────────────────────────────────────────── + + [Fact] + public void Serialize_HjsonComment_EmitsComments() + { + var obj = new WithComments { Host = "localhost", Port = 8080, Debug = true }; + string hjson = HjsonConvert.Serialize(obj); + + Assert.Contains("# The server hostname", hjson); + Assert.Contains("# Port number to listen on", hjson); + Assert.Contains("Host", hjson); + Assert.Contains("localhost", hjson); + } + + [Fact] + public void Serialize_MultilineComment_EmitsAllLines() + { + var obj = new WithMultilineComment { Value = "test" }; + string hjson = HjsonConvert.Serialize(obj); + + Assert.Contains("# First line", hjson); + Assert.Contains("# Second line", hjson); + } + + [Fact] + public void Deserialize_IgnoresComments() + { + string hjson = @" + { + # This is a comment + Host: myhost + # Another comment + Port: 9090 + Debug: false + }"; + + var obj = HjsonConvert.Deserialize(hjson); + + Assert.Equal("myhost", obj.Host); + Assert.Equal(9090, obj.Port); + Assert.False(obj.Debug); + } + + // ── Combined attributes ───────────────────────────────────────── + + [Fact] + public void Serialize_AllAttributes_Combined() + { + var obj = new WithAllAttributes { Name = "Test", Hidden = "secret", Active = true }; + string hjson = HjsonConvert.Serialize(obj); + + Assert.Contains("# The display name", hjson); + Assert.Contains("display_name", hjson); + Assert.Contains("# Is active?", hjson); + Assert.DoesNotContain("Hidden", hjson); + Assert.DoesNotContain("secret", hjson); + } + + [Fact] + public void Roundtrip_AllAttributes() + { + var original = new WithAllAttributes { Name = "Test", Hidden = "secret", Active = true }; + string hjson = HjsonConvert.Serialize(original); + var restored = HjsonConvert.Deserialize(hjson); + + Assert.Equal("Test", restored.Name); + Assert.Equal("", restored.Hidden); // ignored, uses default + Assert.True(restored.Active); + } + + // ── Enum support ──────────────────────────────────────────────── + + [Fact] + public void Roundtrip_Enum() + { + var obj = new WithEnum { Favorite = Color.Blue }; + string hjson = HjsonConvert.Serialize(obj); + + Assert.Contains("Blue", hjson); + + var restored = HjsonConvert.Deserialize(hjson); + Assert.Equal(Color.Blue, restored.Favorite); + } + + // ── Nullable support ──────────────────────────────────────────── + + [Fact] + public void Roundtrip_Nullable_WithValues() + { + var obj = new WithNullable { MaybeInt = 42, MaybeString = "hello" }; + string hjson = HjsonConvert.Serialize(obj); + var restored = HjsonConvert.Deserialize(hjson); + + Assert.Equal(42, restored.MaybeInt); + Assert.Equal("hello", restored.MaybeString); + } + + [Fact] + public void Roundtrip_Nullable_WithNulls() + { + var obj = new WithNullable { MaybeInt = null, MaybeString = null }; + string hjson = HjsonConvert.Serialize(obj); + var restored = HjsonConvert.Deserialize(hjson); + + Assert.Null(restored.MaybeInt); + Assert.Null(restored.MaybeString); + } + + // ── Collections ───────────────────────────────────────────────── + + [Fact] + public void Roundtrip_List_And_Array() + { + var obj = new WithCollections + { + Numbers = [1, 2, 3], + Tags = ["a", "b", "c"], + }; + + string hjson = HjsonConvert.Serialize(obj); + var restored = HjsonConvert.Deserialize(hjson); + + Assert.Equal([1, 2, 3], restored.Numbers); + Assert.Equal(["a", "b", "c"], restored.Tags); + } + + [Fact] + public void Roundtrip_EmptyCollections() + { + var obj = new WithCollections { Numbers = [], Tags = [] }; + string hjson = HjsonConvert.Serialize(obj); + var restored = HjsonConvert.Deserialize(hjson); + + Assert.Empty(restored.Numbers); + Assert.Empty(restored.Tags); + } + + // ── Dictionary ────────────────────────────────────────────────── + + [Fact] + public void Roundtrip_Dictionary() + { + var obj = new WithDictionary + { + Scores = new Dictionary + { + ["alice"] = 100, + ["bob"] = 85, + }, + }; + + string hjson = HjsonConvert.Serialize(obj); + var restored = HjsonConvert.Deserialize(hjson); + + Assert.Equal(100, restored.Scores["alice"]); + Assert.Equal(85, restored.Scores["bob"]); + } + + // ── Nested objects ────────────────────────────────────────────── + + [Fact] + public void Roundtrip_NestedObject() + { + var obj = new Nested + { + Name = "parent", + Inner = new InnerObj { Value = 42, Label = "nested" }, + }; + + string hjson = HjsonConvert.Serialize(obj); + var restored = HjsonConvert.Deserialize(hjson); + + Assert.Equal("parent", restored.Name); + Assert.Equal(42, restored.Inner.Value); + Assert.Equal("nested", restored.Inner.Label); + } + + // ── Numeric types ─────────────────────────────────────────────── + + [Fact] + public void Roundtrip_NumericTypes() + { + var obj = new WithNumericTypes + { + ByteVal = 255, + ShortVal = 1000, + LongVal = 9999999999L, + FloatVal = 3.14f, + DoubleVal = 2.718281828, + DecimalVal = 123.456m, + }; + + string hjson = HjsonConvert.Serialize(obj); + var restored = HjsonConvert.Deserialize(hjson); + + Assert.Equal(255, restored.ByteVal); + Assert.Equal(1000, restored.ShortVal); + Assert.Equal(9999999999L, restored.LongVal); + Assert.Equal(3.14f, restored.FloatVal); + Assert.Equal(2.718281828, restored.DoubleVal); + Assert.Equal(123.456m, restored.DecimalVal); + } + + // ── Deserialize from real Hjson features ───────────────────────── + + [Fact] + public void Deserialize_UnquotedStrings() + { + string hjson = @" + { + Name: hello world + Port: 3000 + Enabled: true + }"; + + var config = HjsonConvert.Deserialize(hjson); + + Assert.Equal("hello world", config.Name); + Assert.Equal(3000, config.Port); + } + + [Fact] + public void Deserialize_WithHjsonComments() + { + string hjson = @" + { + // C-style comment + Name: test + # Hash comment + Port: 80 + /* Block comment */ + Enabled: true + }"; + + var config = HjsonConvert.Deserialize(hjson); + + Assert.Equal("test", config.Name); + Assert.Equal(80, config.Port); + Assert.True(config.Enabled); + } + + [Fact] + public void Deserialize_MissingProperties_UseDefaults() + { + string hjson = @" + { + Name: partial + }"; + + var config = HjsonConvert.Deserialize(hjson); + + Assert.Equal("partial", config.Name); + Assert.Equal(0, config.Port); // default int + Assert.False(config.Enabled); // default bool + } +} diff --git a/test/ParserTests.cs b/test/ParserTests.cs new file mode 100644 index 0000000..3957238 --- /dev/null +++ b/test/ParserTests.cs @@ -0,0 +1,143 @@ +#nullable enable + +using System; +using System.IO; +using System.Text; +using Hjson; +using Xunit; + +namespace Hjson.Tests; + +public class ParserTests +{ + static readonly string AssetsDir = Path.Combine(AppContext.BaseDirectory, "assets"); + + static string Load(string file, bool cr) + { + string text = File.ReadAllText(file, Encoding.UTF8); + return text.ReplaceLineEndings(cr ? "\r\n" : "\n"); + } + + [Theory] + [InlineData("charset_test.hjson")] + [InlineData("comments_test.hjson")] + [InlineData("empty_test.hjson")] + [InlineData("failCharset1_test.hjson")] + [InlineData("failJSON02_test.json")] + [InlineData("failJSON05_test.json")] + [InlineData("failJSON06_test.json")] + [InlineData("failJSON07_test.json")] + [InlineData("failJSON08_test.json")] + [InlineData("failJSON10_test.json")] + [InlineData("failJSON11_test.json")] + [InlineData("failJSON12_test.json")] + [InlineData("failJSON13_test.json")] + [InlineData("failJSON14_test.json")] + [InlineData("failJSON15_test.json")] + [InlineData("failJSON16_test.json")] + [InlineData("failJSON17_test.json")] + [InlineData("failJSON19_test.json")] + [InlineData("failJSON20_test.json")] + [InlineData("failJSON21_test.json")] + [InlineData("failJSON22_test.json")] + [InlineData("failJSON23_test.json")] + [InlineData("failJSON26_test.json")] + [InlineData("failJSON28_test.json")] + [InlineData("failJSON29_test.json")] + [InlineData("failJSON30_test.json")] + [InlineData("failJSON31_test.json")] + [InlineData("failJSON32_test.json")] + [InlineData("failJSON33_test.json")] + [InlineData("failJSON34_test.json")] + [InlineData("failKey1_test.hjson")] + [InlineData("failKey2_test.hjson")] + [InlineData("failKey3_test.hjson")] + [InlineData("failKey4_test.hjson")] + [InlineData("failKey5_test.hjson")] + [InlineData("failMLStr1_test.hjson")] + [InlineData("failObj1_test.hjson")] + [InlineData("failObj2_test.hjson")] + [InlineData("failObj3_test.hjson")] + [InlineData("failStr1a_test.hjson")] + [InlineData("failStr1b_test.hjson")] + [InlineData("failStr1c_test.hjson")] + [InlineData("failStr1d_test.hjson")] + [InlineData("failStr2a_test.hjson")] + [InlineData("failStr2b_test.hjson")] + [InlineData("failStr2c_test.hjson")] + [InlineData("failStr2d_test.hjson")] + [InlineData("failStr3a_test.hjson")] + [InlineData("failStr3b_test.hjson")] + [InlineData("failStr3c_test.hjson")] + [InlineData("failStr3d_test.hjson")] + [InlineData("failStr4a_test.hjson")] + [InlineData("failStr4b_test.hjson")] + [InlineData("failStr4c_test.hjson")] + [InlineData("failStr4d_test.hjson")] + [InlineData("failStr5a_test.hjson")] + [InlineData("failStr5b_test.hjson")] + [InlineData("failStr5c_test.hjson")] + [InlineData("failStr5d_test.hjson")] + [InlineData("failStr6a_test.hjson")] + [InlineData("failStr6b_test.hjson")] + [InlineData("failStr6c_test.hjson")] + [InlineData("failStr6d_test.hjson")] + [InlineData("failStr7a_test.hjson")] + [InlineData("failStr8a_test.hjson")] + [InlineData("kan_test.hjson")] + [InlineData("keys_test.hjson")] + [InlineData("mltabs_test.json")] + [InlineData("oa_test.hjson")] + [InlineData("pass1_test.json")] + [InlineData("pass2_test.json")] + [InlineData("pass3_test.json")] + [InlineData("pass4_test.json")] + [InlineData("passSingle_test.hjson")] + [InlineData("stringify1_test.hjson")] + [InlineData("strings2_test.hjson")] + [InlineData("strings_test.hjson")] + [InlineData("trail_test.hjson")] + public void ParseAndStringify(string file) + { + // Test all four CR/LF combinations + RunTest(file, inputCr: false, outputCr: false); + RunTest(file, inputCr: true, outputCr: false); + RunTest(file, inputCr: false, outputCr: true); + RunTest(file, inputCr: true, outputCr: true); + } + + void RunTest(string file, bool inputCr, bool outputCr) + { + string name = Path.GetFileNameWithoutExtension(file)[..^5]; + bool isJson = Path.GetExtension(file) is ".json"; + bool shouldFail = name.StartsWith("fail"); + + JsonValue.Eol = outputCr ? "\r\n" : "\n"; + string text = Load(Path.Combine(AssetsDir, file), inputCr); + + if (shouldFail) + { + Assert.ThrowsAny(() => HjsonValue.Parse(text)); + return; + } + + var data = HjsonValue.Parse(text); + + string data1 = data.ToString(Stringify.Formatted); + string hjson1 = data.ToString(Stringify.Hjson); + + var result = JsonValue.Parse(Load(Path.Combine(AssetsDir, $"{name}_result.json"), inputCr)); + string data2 = result.ToString(Stringify.Formatted); + string hjson2 = Load(Path.Combine(AssetsDir, $"{name}_result.hjson"), outputCr); + + Assert.Equal(data2, data1); + Assert.Equal(hjson2, hjson1); + + if (isJson) + { + string json1 = data.ToString(); + string json2 = JsonValue.Parse(text).ToString(); + Assert.Equal(json2, json1); + } + } +} diff --git a/test/Program.cs b/test/Program.cs deleted file mode 100644 index 70b49a0..0000000 --- a/test/Program.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using Hjson; - -namespace Test -{ - class Program - { - static string assetsDir; - - static string load(string file, bool cr) - { - var text=File.ReadAllText(file, Encoding.UTF8); - var std=text.Replace("\r", ""); // make sure we have unix style text regardless of the input - return cr?std.Replace("\n", "\r\n"):std; - } - - static bool test(string name, string file, bool inputCr, bool outputCr) - { - bool isJson=Path.GetExtension(file)==".json"; - bool shouldFail=name.StartsWith("fail"); - - JsonValue.Eol=outputCr?"\r\n":"\n"; - var text=load(Path.Combine(assetsDir, file), inputCr); - - try - { - var data=HjsonValue.Parse(text); - var data1=data.ToString(Stringify.Formatted); - var hjson1=data.ToString(Stringify.Hjson); - - if (!shouldFail) - { - var result=JsonValue.Parse(load(Path.Combine(assetsDir, name+"_result.json"), inputCr)); - var data2=result.ToString(Stringify.Formatted); - var hjson2=load(Path.Combine(assetsDir, name+"_result.hjson"), outputCr); - if (data1!=data2) return failErr(name, "parse", data1, data2); - if (hjson1!=hjson2) return failErr(name, "stringify", hjson1, hjson2); - - if (isJson) - { - string json1=data.ToString(), json2=JsonValue.Parse(text).ToString(); - if (json1!=json2) return failErr(name, "json chk", json1, json2); - } - } - else return failErr(name, "should fail"); - } - catch (Exception e) - { - if (!shouldFail) return failErr(name, "exception", e.ToString(), ""); - } - return true; - } - - static int Main(string[] args) - { - string filter=args.Length==2?args[1]:null; - if (args.Length==0) - { - Console.WriteLine("error: specify the assets directory"); - return 1; - } - - assetsDir=args[0]; - Console.WriteLine("running tests..."); - - var tests=File.ReadAllLines(Path.Combine(assetsDir, "testlist.txt"), Encoding.UTF8); - foreach (var file in tests) - { - if (file.Contains("/")) continue; // skip for now - string name=Path.GetFileNameWithoutExtension(file); - name=name.Substring(0, name.Length-5); - if (filter!=null && !name.Contains(filter)) continue; - - if (!test(name, file, false, false) - || !test(name, file, true, false) - || !test(name, file, false, true) - || !test(name, file, true, true)) return 1; - Console.WriteLine("- "+name+" OK"); - } - Console.WriteLine("ALL OK!"); - return 0; - } - - static bool failErr(string name, string type, string s1=null, string s2=null) - { - Console.WriteLine(name+" "+type+" FAILED!"); - if (s1!=null || s2!=null) - { - Console.WriteLine("--- actual ({0}):", s1.Length); - Console.WriteLine(s1+"---"); - Console.WriteLine("--- expected ({0}):", s2.Length); - Console.WriteLine(s2+"---"); - if (s1.Length==s2.Length) - for (int i=0; i - Exe - netcoreapp2.0 - test - test + net10.0 + false + true + + + + + + + + + PreserveNewest + + + From 1040ee314967f2f5271d0aa72ff83e933f206a37 Mon Sep 17 00:00:00 2001 From: v Date: Sun, 8 Mar 2026 20:09:04 +0100 Subject: [PATCH 2/9] dotnet format --- Hjson/HjsonAttributes.cs | 2 +- Hjson/HjsonConvert.cs | 2 +- Hjson/HjsonDsf.cs | 2 +- Hjson/HjsonOptions.cs | 2 +- Hjson/HjsonReader.cs | 2 +- Hjson/HjsonValue.cs | 2 +- Hjson/HjsonWriter.cs | 2 +- Hjson/HjsonWsc.cs | 2 +- Hjson/IJsonReader.cs | 2 +- Hjson/JsonArray.cs | 2 +- Hjson/JsonObject.cs | 2 +- Hjson/JsonPrimitive.cs | 2 +- Hjson/JsonReader.cs | 2 +- Hjson/JsonType.cs | 2 +- Hjson/JsonUtil.cs | 2 +- Hjson/JsonValue.cs | 2 +- Hjson/JsonWriter.cs | 2 +- test/HjsonConvertTests.cs | 4 +++- test/ParserTests.cs | 4 +++- 19 files changed, 23 insertions(+), 19 deletions(-) diff --git a/Hjson/HjsonAttributes.cs b/Hjson/HjsonAttributes.cs index 581efaf..dbc23f1 100644 --- a/Hjson/HjsonAttributes.cs +++ b/Hjson/HjsonAttributes.cs @@ -24,4 +24,4 @@ public sealed class HjsonCommentAttribute(string comment) : Attribute { /// Gets the comment text. public string Comment { get; } = comment; -} +} \ No newline at end of file diff --git a/Hjson/HjsonConvert.cs b/Hjson/HjsonConvert.cs index fe30f50..e7b445e 100644 --- a/Hjson/HjsonConvert.cs +++ b/Hjson/HjsonConvert.cs @@ -322,4 +322,4 @@ static string GetName(MemberInfo member) => member.Name; static string GetComment(MemberInfo member) => member.GetCustomAttribute()?.Comment; -} +} \ No newline at end of file diff --git a/Hjson/HjsonDsf.cs b/Hjson/HjsonDsf.cs index daae52e..c7cf262 100644 --- a/Hjson/HjsonDsf.cs +++ b/Hjson/HjsonDsf.cs @@ -182,4 +182,4 @@ public string Stringify(JsonValue value) return null; } } -} +} \ No newline at end of file diff --git a/Hjson/HjsonOptions.cs b/Hjson/HjsonOptions.cs index e5bdd43..06c3b07 100644 --- a/Hjson/HjsonOptions.cs +++ b/Hjson/HjsonOptions.cs @@ -27,4 +27,4 @@ public IEnumerable DsfProviders get => dsf ?? []; set => dsf = [.. value]; } -} +} \ No newline at end of file diff --git a/Hjson/HjsonReader.cs b/Hjson/HjsonReader.cs index d6ce98c..4c67f58 100644 --- a/Hjson/HjsonReader.cs +++ b/Hjson/HjsonReader.cs @@ -409,4 +409,4 @@ JsonValue readTfnns(int c) c = PeekChar(); } } -} +} \ No newline at end of file diff --git a/Hjson/HjsonValue.cs b/Hjson/HjsonValue.cs index 6e82775..7b3178c 100644 --- a/Hjson/HjsonValue.cs +++ b/Hjson/HjsonValue.cs @@ -151,4 +151,4 @@ public static JsonValue Parse(string hjsonString, bool preserveComments) } #endregion -} +} \ No newline at end of file diff --git a/Hjson/HjsonWriter.cs b/Hjson/HjsonWriter.cs index c7d0fc0..2460fde 100644 --- a/Hjson/HjsonWriter.cs +++ b/Hjson/HjsonWriter.cs @@ -235,4 +235,4 @@ static bool startsWithKeyword(string text) static bool needsEscape(char c) => c is '\"' or '\\' || needsQuotes(c); static bool needsEscapeML(char c) => c is '\f' or '\b'; -} +} \ No newline at end of file diff --git a/Hjson/HjsonWsc.cs b/Hjson/HjsonWsc.cs index 011e9a7..c12b72a 100644 --- a/Hjson/HjsonWsc.cs +++ b/Hjson/HjsonWsc.cs @@ -22,4 +22,4 @@ public class WscJsonArray : JsonArray { /// Defines a comment for each item. The [0] entry is emitted before any element. public List Comments { get; private set; } = []; -} +} \ No newline at end of file diff --git a/Hjson/IJsonReader.cs b/Hjson/IJsonReader.cs index bb1bf04..0aa5560 100644 --- a/Hjson/IJsonReader.cs +++ b/Hjson/IJsonReader.cs @@ -11,4 +11,4 @@ public interface IJsonReader void Index(int idx); /// Triggered when a value is read. void Value(JsonValue value); -} +} \ No newline at end of file diff --git a/Hjson/JsonArray.cs b/Hjson/JsonArray.cs index 9c89b25..d20e6a7 100644 --- a/Hjson/JsonArray.cs +++ b/Hjson/JsonArray.cs @@ -75,4 +75,4 @@ public void AddRange(IEnumerable items) IEnumerator IEnumerable.GetEnumerator() => list.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => list.GetEnumerator(); -} +} \ No newline at end of file diff --git a/Hjson/JsonObject.cs b/Hjson/JsonObject.cs index 6db8833..ffe3b50 100644 --- a/Hjson/JsonObject.cs +++ b/Hjson/JsonObject.cs @@ -110,4 +110,4 @@ public bool Remove(string key) void ICollection.CopyTo(JsonPair[] array, int arrayIndex) => this.CopyTo(array, arrayIndex); int ICollection.Count => this.Count; -} +} \ No newline at end of file diff --git a/Hjson/JsonPrimitive.cs b/Hjson/JsonPrimitive.cs index 566bd47..ddd2a49 100644 --- a/Hjson/JsonPrimitive.cs +++ b/Hjson/JsonPrimitive.cs @@ -73,4 +73,4 @@ internal string GetRawString() _ => throw new InvalidOperationException(), }; } -} +} \ No newline at end of file diff --git a/Hjson/JsonReader.cs b/Hjson/JsonReader.cs index 320f2e5..e96331c 100644 --- a/Hjson/JsonReader.cs +++ b/Hjson/JsonReader.cs @@ -91,4 +91,4 @@ JsonValue ReadCore() throw ParseError($"Unexpected character '{(char)c}'"); } } -} +} \ No newline at end of file diff --git a/Hjson/JsonType.cs b/Hjson/JsonType.cs index d0d1aa8..db3f15e 100644 --- a/Hjson/JsonType.cs +++ b/Hjson/JsonType.cs @@ -16,4 +16,4 @@ public enum JsonType Boolean, /// Json value of an unknown type. Unknown, -} +} \ No newline at end of file diff --git a/Hjson/JsonUtil.cs b/Hjson/JsonUtil.cs index 6f6739e..fe7d20e 100644 --- a/Hjson/JsonUtil.cs +++ b/Hjson/JsonUtil.cs @@ -176,4 +176,4 @@ public static string ToJson(this TimeSpan ts) if (rct.Length > 0) { rc.Append('T'); rc.Append(rct.ToString()); } return rc.ToString(); } -} +} \ No newline at end of file diff --git a/Hjson/JsonValue.cs b/Hjson/JsonValue.cs index fc984a8..9c14905 100644 --- a/Hjson/JsonValue.cs +++ b/Hjson/JsonValue.cs @@ -238,4 +238,4 @@ public static implicit operator short(JsonValue value) /// Converts to string. Also see . public static implicit operator string(JsonValue value) => (string)((JsonPrimitive)value)?.Value; -} +} \ No newline at end of file diff --git a/Hjson/JsonWriter.cs b/Hjson/JsonWriter.cs index 208277c..ad611cb 100644 --- a/Hjson/JsonWriter.cs +++ b/Hjson/JsonWriter.cs @@ -126,4 +126,4 @@ static string doEscapeString(StringBuilder sb, string src, int cur) '\\' => "\\\\", _ => null, }; -} +} \ No newline at end of file diff --git a/test/HjsonConvertTests.cs b/test/HjsonConvertTests.cs index c809866..70f363c 100644 --- a/test/HjsonConvertTests.cs +++ b/test/HjsonConvertTests.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; + using Hjson; + using Xunit; namespace Hjson.Tests; @@ -621,4 +623,4 @@ public void Deserialize_MissingProperties_UseDefaults() Assert.Equal(0, config.Port); // default int Assert.False(config.Enabled); // default bool } -} +} \ No newline at end of file diff --git a/test/ParserTests.cs b/test/ParserTests.cs index 3957238..479ddb8 100644 --- a/test/ParserTests.cs +++ b/test/ParserTests.cs @@ -3,7 +3,9 @@ using System; using System.IO; using System.Text; + using Hjson; + using Xunit; namespace Hjson.Tests; @@ -140,4 +142,4 @@ void RunTest(string file, bool inputCr, bool outputCr) Assert.Equal(json2, json1); } } -} +} \ No newline at end of file From df4f275d2ae8df4820dcaad9c0bf85c732e3093e Mon Sep 17 00:00:00 2001 From: v Date: Mon, 9 Mar 2026 01:42:02 +0100 Subject: [PATCH 3/9] performance tunning and fixes Fixes #7 Fixes #10 --- Hjson/BaseReader.cs | 80 +++++++++++++++--------------------- Hjson/HjsonDsf.cs | 13 +++--- Hjson/HjsonReader.cs | 93 +++++++++++++++++++++++++++--------------- Hjson/HjsonWriter.cs | 30 ++++++++++++-- Hjson/JsonPrimitive.cs | 4 -- Hjson/JsonWriter.cs | 3 -- test/ParserTests.cs | 1 + 7 files changed, 129 insertions(+), 95 deletions(-) diff --git a/Hjson/BaseReader.cs b/Hjson/BaseReader.cs index 19a2ad7..0d8698b 100644 --- a/Hjson/BaseReader.cs +++ b/Hjson/BaseReader.cs @@ -1,9 +1,6 @@ using System; -using System.Collections; -using System.Collections.Generic; using System.Globalization; using System.IO; -using System.Linq; using System.Text; namespace Hjson; @@ -11,11 +8,9 @@ namespace Hjson; internal abstract class BaseReader { readonly string buffer; - TextReader r; + int pos; readonly StringBuilder sb = new(); readonly StringBuilder white = new(); - // peek could be removed since we now use a buffer - readonly List peek = []; bool prevLf; public int Line { get; private set; } @@ -29,7 +24,6 @@ internal abstract class BaseReader public BaseReader(TextReader reader, IJsonReader jsonReader) { ArgumentNullException.ThrowIfNull(reader); - // use a buffer so we can support reset Reader = jsonReader; buffer = reader.ReadToEnd(); Reset(); @@ -38,22 +32,16 @@ public BaseReader(TextReader reader, IJsonReader jsonReader) public void Reset() { Line = 1; - this.r = new StringReader(buffer); - peek.Clear(); + pos = 0; white.Length = sb.Length = 0; prevLf = false; } public int PeekChar(int idx = 0) { - if (idx < 0) throw new ArgumentOutOfRangeException(); - while (idx >= peek.Count) - { - int c = r.Read(); - if (c < 0) return c; - peek.Add(c); - } - return peek[idx]; + if (idx < 0) throw new ArgumentOutOfRangeException(nameof(idx)); + int p = pos + idx; + return (uint)p < (uint)buffer.Length ? buffer[p] : -1; } public virtual int SkipPeekChar() @@ -64,16 +52,11 @@ public virtual int SkipPeekChar() public int ReadChar() { - int v; - if (peek.Count > 0) - { - // normally peek will only hold not more than one character so this should not matter for performance - v = peek[0]; - peek.RemoveAt(0); - } - else v = r.Read(); + if ((uint)pos >= (uint)buffer.Length) return -1; - if (ReadWsc && v != '\r') white.Append((char)v); + char v = buffer[pos++]; + + if (ReadWsc && v != '\r') white.Append(v); if (prevLf) { @@ -106,28 +89,25 @@ public static bool IsWhite(char c) public void SkipWhite() { - for (; ; ) - { - if (IsWhite((char)PeekChar())) ReadChar(); - else break; - } + while (IsWhite((char)PeekChar())) ReadChar(); } - // It could return either long or double, depending on the parsed value. + // Returns either long or double, depending on the parsed value. public JsonValue ReadNumericLiteral() { int c, leadingZeros = 0; bool testLeading = true; - var numStr = new StringBuilder(); + Span numBuf = stackalloc char[64]; + int numLen = 0; if (PeekChar() == '-') { - numStr.Append('-'); + numBuf[numLen++] = '-'; ReadChar(); if (PeekChar() < 0) throw ParseError("Invalid JSON numeric literal; extra negation"); } - for (int x = 0; ; x++) + for (; ; ) { c = PeekChar(); if (c < '0' || c > '9') break; @@ -136,7 +116,7 @@ public JsonValue ReadNumericLiteral() if (c == '0') leadingZeros++; else testLeading = false; } - numStr.Append((char)c); + numBuf[numLen++] = (char)c; ReadChar(); } if (testLeading) leadingZeros--; // single 0 is allowed @@ -149,14 +129,14 @@ public JsonValue ReadNumericLiteral() { hasFracOrExp = true; int fdigits = 0; - numStr.Append('.'); + numBuf[numLen++] = '.'; ReadChar(); if (PeekChar() < 0) throw ParseError("Invalid JSON numeric literal; extra dot"); for (; ; ) { c = PeekChar(); if (c < '0' || '9' < c) break; - numStr.Append((char)c); + numBuf[numLen++] = (char)c; ReadChar(); fdigits++; } @@ -167,19 +147,19 @@ public JsonValue ReadNumericLiteral() if (c == 'e' || c == 'E') { hasFracOrExp = true; - numStr.Append((char)c); + numBuf[numLen++] = (char)c; ReadChar(); if (PeekChar() < 0) throw new ArgumentException("Invalid JSON numeric literal; incomplete exponent"); c = PeekChar(); if (c == '-') { - numStr.Append('-'); + numBuf[numLen++] = '-'; ReadChar(); } else if (c == '+') { - numStr.Append('+'); + numBuf[numLen++] = '+'; ReadChar(); } @@ -189,20 +169,24 @@ public JsonValue ReadNumericLiteral() { c = PeekChar(); if (c < '0' || c > '9') break; - numStr.Append((char)c); + numBuf[numLen++] = (char)c; ReadChar(); } } - double val = double.Parse(numStr.ToString(), NumberStyles.Float, NumberFormatInfo.InvariantInfo); - - if (val == 0.0 && double.IsNegative(val)) return -0.0; + var numSpan = numBuf[..numLen]; - if (!hasFracOrExp) + // Try parsing as long first to preserve precision for large integers + if (!hasFracOrExp && long.TryParse(numSpan, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out long lval)) { - long lval = (long)val; - if (lval == val) return lval; + // Preserve negative zero as double (-0 as long is just 0) + if (lval == 0 && numSpan[0] == '-') return -0.0; + return lval; } + + double val = double.Parse(numSpan, NumberStyles.Float, NumberFormatInfo.InvariantInfo); + if (val == 0.0 && double.IsNegative(val)) return -0.0; + return val; } diff --git a/Hjson/HjsonDsf.cs b/Hjson/HjsonDsf.cs index c7cf262..34bb3d8 100644 --- a/Hjson/HjsonDsf.cs +++ b/Hjson/HjsonDsf.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.Linq; -using System.Text; using System.Text.RegularExpressions; namespace Hjson; @@ -65,8 +63,13 @@ internal static string Stringify(IEnumerable dsfProviders, Js var text = dsf.Stringify(value); if (text != null) { - if (text.Length == 0 || text.FirstOrDefault() == '"' || text.Any(c => isInvalidDsfChar(c))) - throw new Exception("value may not be empty, start with a quote or contain a punctuator character except colon: " + text); + if (text.Length == 0 || text[0] == '"') + throw new Exception("value may not be empty or start with a quote: " + text); + foreach (char c in text) + { + if (isInvalidDsfChar(c)) + throw new Exception("value may not contain a punctuator character except colon: " + text); + } return text; } } @@ -134,7 +137,7 @@ class DsfHex(bool stringify) : IHjsonDsfProvider public JsonValue Parse(string text) { if (isHex.IsMatch(text)) - return long.Parse(text.Substring(2), NumberStyles.HexNumber); + return long.Parse(text.AsSpan(2), NumberStyles.HexNumber); else return null; } diff --git a/Hjson/HjsonReader.cs b/Hjson/HjsonReader.cs index 4c67f58..87c3198 100644 --- a/Hjson/HjsonReader.cs +++ b/Hjson/HjsonReader.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -280,17 +279,20 @@ internal static bool TryParseNumericLiteral(string text, bool stopAtNext, out Js { int c, leadingZeros = 0, p = 0; bool testLeading = true; - text += '\0'; + int len = text.Length; value = null; + if (len == 0) return false; + if (text[p] == '-') { p++; - if (text[p] == 0) return false; + if (p >= len) return false; } - for (int x = 0; ; x++) + for (; ; ) { + if (p >= len) break; c = text[p]; if (c < '0' || c > '9') break; if (testLeading) @@ -304,14 +306,15 @@ internal static bool TryParseNumericLiteral(string text, bool stopAtNext, out Js if (leadingZeros > 0) return false; // fraction - if (text[p] == '.') + if (p < len && text[p] == '.') { if (leadingZeros < 0) return false; int fdigits = 0; p++; - if (text[p] == 0) return false; + if (p >= len) return false; for (; ; ) { + if (p >= len) break; c = text[p]; if (c < '0' || '9' < c) break; p++; @@ -320,51 +323,58 @@ internal static bool TryParseNumericLiteral(string text, bool stopAtNext, out Js if (fdigits == 0) return false; } - c = text[p]; - if (c == 'e' || c == 'E') + if (p < len) { - p++; - if (text[p] == 0) return false; - c = text[p]; - if (c == '-') + if (c == 'e' || c == 'E') { p++; - } - else if (c == '+') p++; - - if (text[p] == 0) return false; + if (p >= len) return false; - for (; ; ) - { c = text[p]; - if (c < '0' || c > '9') break; - p++; + if (c == '-') p++; + else if (c == '+') p++; + + if (p >= len) return false; + + for (; ; ) + { + if (p >= len) break; + c = text[p]; + if (c < '0' || c > '9') break; + p++; + } } } int numEnd = p; - while (p < text.Length && IsWhite(text[p])) p++; + while (p < len && IsWhite(text[p])) p++; bool foundStop = false; - if (p < text.Length && stopAtNext) + if (p < len && stopAtNext) { // end scan if we find a control character like ,}] or a comment char ch = text[p]; - if (ch == ',' || ch == '}' || ch == ']' || ch == '#' || ch == '/' && (text.Length > p + 1 && (text[p + 1] == '/' || text[p + 1] == '*'))) + if (ch == ',' || ch == '}' || ch == ']' || ch == '#' || ch == '/' && (len > p + 1 && (text[p + 1] == '/' || text[p + 1] == '*'))) foundStop = true; } - if (p + 1 != text.Length && !foundStop) return false; + if (p != len && !foundStop) return false; - string numStr = text.Substring(0, numEnd); - double val = double.Parse(numStr, NumberStyles.Float, NumberFormatInfo.InvariantInfo); + var numSpan = text.AsSpan(0, numEnd); - if (val == 0.0 && double.IsNegative(val)) { value = -0.0; return true; } + // Try parsing as long first to preserve precision for large integers + if (long.TryParse(numSpan, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out long lval)) + { + // Preserve negative zero as double (-0 as long is just 0) + if (lval == 0 && numSpan[0] == '-') { value = -0.0; return true; } + value = lval; + return true; + } - long lval = (long)val; - value = lval == val ? lval : val; + double val = double.Parse(numSpan, NumberStyles.Float, NumberFormatInfo.InvariantInfo); + value = val; return true; } @@ -387,9 +397,15 @@ JsonValue readTfnns(int c) char ch = sb[0]; switch (ch) { - case 'f': if (sb.ToString().Trim() == "false") return false; break; - case 'n': if (sb.ToString().Trim() == "null") return null; break; - case 't': if (sb.ToString().Trim() == "true") return true; break; + case 'f': + if (SbTrimEquals(sb, "false")) return false; + break; + case 'n': + if (SbTrimEquals(sb, "null")) return null; + break; + case 't': + if (SbTrimEquals(sb, "true")) return true; + break; default: if (ch is '-' || (ch is >= '0' and <= '9')) { @@ -409,4 +425,17 @@ JsonValue readTfnns(int c) c = PeekChar(); } } + + /// Checks if the trimmed content of a StringBuilder equals a target string, without allocating. + static bool SbTrimEquals(StringBuilder sb, string target) + { + int start = 0, end = sb.Length - 1; + while (start <= end && sb[start] <= ' ') start++; + while (end >= start && sb[end] <= ' ') end--; + int len = end - start + 1; + if (len != target.Length) return false; + for (int i = 0; i < len; i++) + if (sb[start + i] != target[i]) return false; + return true; + } } \ No newline at end of file diff --git a/Hjson/HjsonWriter.cs b/Hjson/HjsonWriter.cs index 2460fde..a3e8909 100644 --- a/Hjson/HjsonWriter.cs +++ b/Hjson/HjsonWriter.cs @@ -1,7 +1,5 @@ using System; -using System.Collections; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; using System.Text; @@ -46,13 +44,39 @@ string getWsc(string str) c == '/' && i + 1 < str.Length && (str[i + 1] == '/' || str[i + 1] == '*')) break; if (c > ' ') return $" # {str}"; } - return str; + return normalizeEol(str); } string getWsc(Dictionary white, string key) => white.TryGetValue(key, out string value) ? getWsc(value) : ""; string getWsc(List white, int index) => white.Count > index ? getWsc(white[index]) : ""; bool testWsc(string str) => str.Length > 0 && str[str[0] == '\r' && str.Length > 1 ? 1 : 0] != '\n'; + static string normalizeEol(string str) + { + if (string.IsNullOrEmpty(str) || JsonValue.eol == "\n") return str; + // Quick scan: if no newline chars exist, return as-is + bool hasNewline = false; + foreach (char ch in str) + { + if (ch == '\n' || ch == '\r') { hasNewline = true; break; } + } + if (!hasNewline) return str; + // WSC strings from the reader contain bare \n; normalize to the current eol + var sb = new StringBuilder(str.Length); + for (int i = 0; i < str.Length; i++) + { + char c = str[i]; + if (c == '\r') + { + sb.Append(JsonValue.eol); + if (i + 1 < str.Length && str[i + 1] == '\n') i++; + } + else if (c == '\n') sb.Append(JsonValue.eol); + else sb.Append(c); + } + return sb.ToString(); + } + public void Save(JsonValue value, TextWriter tw, int level, bool hasComment, string separator, bool noIndent = false, bool isRootObject = false) { if (value == null) diff --git a/Hjson/JsonPrimitive.cs b/Hjson/JsonPrimitive.cs index ddd2a49..8095cf7 100644 --- a/Hjson/JsonPrimitive.cs +++ b/Hjson/JsonPrimitive.cs @@ -1,9 +1,5 @@ using System; -using System.Collections; -using System.Collections.Generic; using System.Globalization; -using System.IO; -using System.Text; namespace Hjson; diff --git a/Hjson/JsonWriter.cs b/Hjson/JsonWriter.cs index ad611cb..5466c03 100644 --- a/Hjson/JsonWriter.cs +++ b/Hjson/JsonWriter.cs @@ -1,9 +1,6 @@ using System; -using System.Collections; using System.Collections.Generic; -using System.Globalization; using System.IO; -using System.Linq; using System.Text; namespace Hjson; diff --git a/test/ParserTests.cs b/test/ParserTests.cs index 479ddb8..815284f 100644 --- a/test/ParserTests.cs +++ b/test/ParserTests.cs @@ -10,6 +10,7 @@ namespace Hjson.Tests; +[Collection("EolTests")] public class ParserTests { static readonly string AssetsDir = Path.Combine(AppContext.BaseDirectory, "assets"); From e3ac07d75b24689fe81aca834bea375678dd9653 Mon Sep 17 00:00:00 2001 From: v Date: Mon, 9 Mar 2026 01:42:43 +0100 Subject: [PATCH 4/9] missing tests --- test/EdgeCaseTests.cs | 339 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 test/EdgeCaseTests.cs diff --git a/test/EdgeCaseTests.cs b/test/EdgeCaseTests.cs new file mode 100644 index 0000000..25466ce --- /dev/null +++ b/test/EdgeCaseTests.cs @@ -0,0 +1,339 @@ +#nullable enable + +using System; +using System.IO; + +using Hjson; + +using Xunit; + +namespace Hjson.Tests; + +[Collection("EolTests")] +public class EdgeCaseTests +{ + // ── Issue: Mixed line endings when saving with KeepWsc ─────────── + + [Fact] + public void KeepWsc_Save_ConsistentLineEndings_LF() + { + JsonValue.Eol = "\n"; + string input = "# comment\n\na: 0\nb: 0\n"; + + var options = new HjsonOptions { EmitRootBraces = false, KeepWsc = true }; + var config = HjsonValue.Parse(input, options); + config["a"] = 1; + + var sw = new StringWriter(); + HjsonValue.Save(config, sw, options); + string output = sw.ToString(); + + // No \r should be present when eol is LF + Assert.DoesNotContain("\r", output); + // Values should be updated + Assert.Contains("a: 1", output); + Assert.Contains("b: 0", output); + } + + [Fact] + public void KeepWsc_Save_ConsistentLineEndings_CRLF() + { + JsonValue.Eol = "\r\n"; + string input = "# comment\n\na: 0\nb: 0\n"; + + var options = new HjsonOptions { EmitRootBraces = false, KeepWsc = true }; + var config = HjsonValue.Parse(input, options); + config["a"] = 1; + + var sw = new StringWriter(); + HjsonValue.Save(config, sw, options); + string output = sw.ToString(); + + // All newlines should be CRLF, not bare LF + string withoutCr = output.Replace("\r\n", "\n"); + Assert.DoesNotContain("\r", withoutCr); // no stray \r left + // And there should be no bare LF (each \n should be preceded by \r) + for (int i = 0; i < output.Length; i++) + { + if (output[i] == '\n') + Assert.True(i > 0 && output[i - 1] == '\r', + $"Found bare LF at position {i} in output: {output[..Math.Min(i + 20, output.Length)]}"); + } + + Assert.Contains("a: 1", output); + Assert.Contains("b: 0", output); + } + + [Fact] + public void KeepWsc_Save_CommentAndValueLines_SameLineEnding() + { + JsonValue.Eol = "\r\n"; + // Input with LF-only endings (as might come from a Unix-style file) + string input = "# this is a comment\n\na: 0\nb: 0\n"; + + var options = new HjsonOptions { EmitRootBraces = false, KeepWsc = true }; + var config = HjsonValue.Parse(input, options); + + var sw = new StringWriter(); + HjsonValue.Save(config, sw, options); + string output = sw.ToString(); + + // Split on CRLF; there should be no elements containing bare LF + string[] lines = output.Split("\r\n"); + foreach (string line in lines) + { + Assert.DoesNotContain("\n", line); + Assert.DoesNotContain("\r", line); + } + } + + [Fact] + public void KeepWsc_Roundtrip_PreservesComments() + { + JsonValue.Eol = "\n"; + string input = "# header comment\n\na: 0\n# inline comment\nb: 0\n"; + + var options = new HjsonOptions { EmitRootBraces = false, KeepWsc = true }; + var config = HjsonValue.Parse(input, options); + + var sw = new StringWriter(); + HjsonValue.Save(config, sw, options); + string output = sw.ToString(); + + Assert.Contains("# header comment", output); + Assert.Contains("# inline comment", output); + } + + [Fact] + public void KeepWsc_Save_WithBraces_ConsistentLineEndings() + { + JsonValue.Eol = "\r\n"; + string input = "{\n # comment\n a: 0\n b: 0\n}\n"; + + var options = new HjsonOptions { KeepWsc = true }; + var config = HjsonValue.Parse(input, options); + config["a"] = 42; + + var sw = new StringWriter(); + HjsonValue.Save(config, sw, options); + string output = sw.ToString(); + + // Every newline should be CRLF + for (int i = 0; i < output.Length; i++) + { + if (output[i] == '\n') + Assert.True(i > 0 && output[i - 1] == '\r', + $"Found bare LF at position {i}"); + } + } + + [Fact] + public void KeepWsc_Save_Array_ConsistentLineEndings() + { + JsonValue.Eol = "\r\n"; + string input = "{\n items:\n [\n # first item\n 1\n 2\n ]\n}\n"; + + var options = new HjsonOptions { KeepWsc = true }; + var config = HjsonValue.Parse(input, options); + + var sw = new StringWriter(); + HjsonValue.Save(config, sw, options); + string output = sw.ToString(); + + for (int i = 0; i < output.Length; i++) + { + if (output[i] == '\n') + Assert.True(i > 0 && output[i - 1] == '\r', + $"Found bare LF at position {i}"); + } + } + + // ── Issue: ulong/long precision loss ──────────────────────────── + + [Fact] + public void Parse_LargeNumber_PreservesPrecision() + { + string hjson = @" + { + ChannelId: 943453428129071119 + }"; + + var config = HjsonValue.Parse(hjson); + + long channelId = config["ChannelId"]; + Assert.Equal(943453428129071119L, channelId); + } + + [Fact] + public void Parse_LargeNumber_QuotelessValue_PreservesPrecision() + { + // Hjson allows quoteless values; this tests the readTfnns path + string hjson = "ChannelId: 943453428129071119"; + + var options = new HjsonOptions { EmitRootBraces = false }; + var config = HjsonValue.Parse(hjson, options); + + long channelId = config["ChannelId"]; + Assert.Equal(943453428129071119L, channelId); + } + + [Fact] + public void Parse_LargeNegativeNumber_PreservesPrecision() + { + string hjson = @" + { + Value: -943453428129071119 + }"; + + var config = HjsonValue.Parse(hjson); + + long value = config["Value"]; + Assert.Equal(-943453428129071119L, value); + } + + [Fact] + public void Parse_LongMaxValue_PreservesPrecision() + { + string hjson = $@" + {{ + Max: {long.MaxValue} + }}"; + + var config = HjsonValue.Parse(hjson); + + long value = config["Max"]; + Assert.Equal(long.MaxValue, value); + } + + [Fact] + public void Parse_LongMinValue_PreservesPrecision() + { + string hjson = $@" + {{ + Min: {long.MinValue} + }}"; + + var config = HjsonValue.Parse(hjson); + + long value = config["Min"]; + Assert.Equal(long.MinValue, value); + } + + [Fact] + public void Parse_SmallNumber_StillWorksAsLong() + { + string hjson = @" + { + Value: 42 + }"; + + var config = HjsonValue.Parse(hjson); + + int value = config["Value"]; + Assert.Equal(42, value); + } + + [Fact] + public void Parse_FloatingPoint_StillWorksAsDouble() + { + string hjson = @" + { + Value: 3.14159 + }"; + + var config = HjsonValue.Parse(hjson); + + double value = config["Value"]; + Assert.Equal(3.14159, value); + } + + [Fact] + public void Parse_ScientificNotation_StillWorksAsDouble() + { + string hjson = @" + { + Value: 1.5e10 + }"; + + var config = HjsonValue.Parse(hjson); + + double value = config["Value"]; + Assert.Equal(1.5e10, value); + } + + [Fact] + public void Parse_Zero_StillWorks() + { + string hjson = @" + { + Value: 0 + }"; + + var config = HjsonValue.Parse(hjson); + + int value = config["Value"]; + Assert.Equal(0, value); + } + + [Fact] + public void Parse_NegativeZero_ReturnsMinus0() + { + string hjson = @" + { + Value: -0.0 + }"; + + var config = HjsonValue.Parse(hjson); + + double value = config["Value"]; + Assert.True(double.IsNegative(value)); + Assert.Equal(0.0, Math.Abs(value)); + } + + [Fact] + public void Parse_LargeNumber_JsonPath_PreservesPrecision() + { + // Test the JSON (strict) parser path as well + string json = "{\"ChannelId\": 943453428129071119}"; + + var config = JsonValue.Parse(json); + + long channelId = config["ChannelId"]; + Assert.Equal(943453428129071119L, channelId); + } + + [Fact] + public void Roundtrip_LargeNumber_PreservesPrecision() + { + string hjson = @" + { + ChannelId: 943453428129071119 + }"; + + var config = HjsonValue.Parse(hjson); + + // Serialize back and re-parse + string output = config.ToString(Stringify.Hjson); + var config2 = HjsonValue.Parse(output); + + long channelId = config2["ChannelId"]; + Assert.Equal(943453428129071119L, channelId); + } + + [Theory] + [InlineData("999999999999999999")] + [InlineData("100000000000000000")] + [InlineData("123456789012345678")] + public void Parse_Various18DigitNumbers_PreservesPrecision(string numStr) + { + string hjson = $@" + {{ + Value: {numStr} + }}"; + + var config = HjsonValue.Parse(hjson); + + long value = config["Value"]; + Assert.Equal(long.Parse(numStr), value); + } +} From dab558b4521b328522c09e0e45ff1cca1ba28872 Mon Sep 17 00:00:00 2001 From: v Date: Mon, 9 Mar 2026 01:44:45 +0100 Subject: [PATCH 5/9] formatting --- test/EdgeCaseTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/EdgeCaseTests.cs b/test/EdgeCaseTests.cs index 25466ce..8db8ee9 100644 --- a/test/EdgeCaseTests.cs +++ b/test/EdgeCaseTests.cs @@ -336,4 +336,4 @@ public void Parse_Various18DigitNumbers_PreservesPrecision(string numStr) long value = config["Value"]; Assert.Equal(long.Parse(numStr), value); } -} +} \ No newline at end of file From 76faac1f339ace81a8ee9a9146e04fe127daf940 Mon Sep 17 00:00:00 2001 From: v Date: Mon, 9 Mar 2026 01:51:11 +0100 Subject: [PATCH 6/9] Fix zero length tripple quotes strings Fixes #13 --- Hjson/HjsonReader.cs | 2 +- test/EdgeCaseTests.cs | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/Hjson/HjsonReader.cs b/Hjson/HjsonReader.cs index 87c3198..2a0da8c 100644 --- a/Hjson/HjsonReader.cs +++ b/Hjson/HjsonReader.cs @@ -248,7 +248,7 @@ string readMlString() ReadChar(); if (triple == 3) { - if (sb[sb.Length - 1] == '\n') sb.Length--; + if (sb.Length > 0 && sb[sb.Length - 1] == '\n') sb.Length--; return sb.ToString(); } else continue; diff --git a/test/EdgeCaseTests.cs b/test/EdgeCaseTests.cs index 8db8ee9..6e7865d 100644 --- a/test/EdgeCaseTests.cs +++ b/test/EdgeCaseTests.cs @@ -336,4 +336,41 @@ public void Parse_Various18DigitNumbers_PreservesPrecision(string numStr) long value = config["Value"]; Assert.Equal(long.Parse(numStr), value); } + + // ── Issue: Empty multiline string DoS (IndexOutOfRangeException) ── + + [Fact] + public void Parse_EmptyMultilineString_DoesNotThrow() + { + // Six single quotes = open + close of an empty multiline string + string hjson = "{ value: '''''' }"; + + var config = HjsonValue.Parse(hjson); + + string value = config["value"]; + Assert.Equal("", value); + } + + [Fact] + public void Parse_EmptyMultilineString_Quoteless_DoesNotThrow() + { + string hjson = "value: ''''''"; + + var options = new HjsonOptions { EmitRootBraces = false }; + var config = HjsonValue.Parse(hjson, options); + + string value = config["value"]; + Assert.Equal("", value); + } + + [Fact] + public void Parse_MultilineString_WithContent_StillWorks() + { + string hjson = "{ value: '''\nhello\nworld\n''' }"; + + var config = HjsonValue.Parse(hjson); + + string value = config["value"]; + Assert.Equal("hello\nworld", value); + } } \ No newline at end of file From 7b7cb2995ac74d346e1870956e0560c0cd883475 Mon Sep 17 00:00:00 2001 From: v Date: Mon, 9 Mar 2026 01:52:47 +0100 Subject: [PATCH 7/9] removed travis badge --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 527adae..56fda00 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # hjson-cs -[![Build Status](https://img.shields.io/travis/hjson/hjson-cs.svg?style=flat-square)](http://travis-ci.org/hjson/hjson-cs) [![nuget version](https://img.shields.io/nuget/v/Hjson.svg?style=flat-square)](https://www.nuget.org/packages/Hjson/) [![License](https://img.shields.io/github/license/hjson/hjson-cs.svg?style=flat-square)](https://github.com/hjson/hjson-cs/blob/master/LICENSE) From 4f6712a6dd18ed82566895d85d0ebd4faf3321b3 Mon Sep 17 00:00:00 2001 From: v Date: Mon, 9 Mar 2026 09:22:37 +0100 Subject: [PATCH 8/9] optimizations --- Hjson/BaseReader.cs | 140 +++++++++++++++++++++++++++---------------- Hjson/HjsonReader.cs | 76 ++++++++++++++++++----- Hjson/HjsonValue.cs | 18 ++++-- 3 files changed, 160 insertions(+), 74 deletions(-) diff --git a/Hjson/BaseReader.cs b/Hjson/BaseReader.cs index 0d8698b..96e4220 100644 --- a/Hjson/BaseReader.cs +++ b/Hjson/BaseReader.cs @@ -21,6 +21,14 @@ internal abstract class BaseReader public bool ReadWsc { get; set; } + public BaseReader(string input, IJsonReader jsonReader) + { + ArgumentNullException.ThrowIfNull(input); + Reader = jsonReader; + buffer = input; + Reset(); + } + public BaseReader(TextReader reader, IJsonReader jsonReader) { ArgumentNullException.ThrowIfNull(reader); @@ -44,6 +52,8 @@ public int PeekChar(int idx = 0) return (uint)p < (uint)buffer.Length ? buffer[p] : -1; } + public bool RemainingContains(char c) => buffer.AsSpan(pos).Contains(c); + public virtual int SkipPeekChar() { SkipWhite(); @@ -193,66 +203,90 @@ public JsonValue ReadNumericLiteral() public string ReadStringLiteral(Func allowML) { // callers make sure that (exitCh == '"' || exitCh == "'") - int exitCh = ReadChar(); - sb.Length = 0; - for (; ; ) + + // Check for multiline ''' string + if (allowML != null && exitCh == '\'' + && (uint)pos < (uint)buffer.Length && buffer[pos] == '\'' + && (uint)(pos + 1) < (uint)buffer.Length && buffer[pos + 1] == '\'') + { + ReadChar(); ReadChar(); + return allowML(); + } + + // Scan to find closing quote and detect escapes + int start = pos; + bool hasEscapes = false; + int i = start; + int bufLen = buffer.Length; + while (i < bufLen) + { + char c = buffer[i]; + if (c == exitCh) break; + if (c == '\\') { hasEscapes = true; i++; } // skip escaped char + else if (c == '\n' || c == '\r') break; + i++; + } + if (i >= bufLen) throw ParseError("JSON string is not closed"); + if (buffer[i] != exitCh) throw ParseError("Bad string containing newline"); + + int end = i; // position of closing quote + + string result; + if (!hasEscapes) + { + result = buffer.Substring(start, end - start); + } + else { - int c = ReadChar(); - if (c < 0) throw ParseError("JSON string is not closed"); - if (c == exitCh) + sb.Length = 0; + for (i = start; i < end; i++) { - if (allowML != null && exitCh == '\'' && PeekChar() == '\'' && sb.Length == 0) + char c = buffer[i]; + if (c != '\\') { sb.Append(c); continue; } + + if (++i >= end) + throw ParseError("Invalid JSON string literal; incomplete escape sequence"); + c = buffer[i]; + switch (c) { - // ''' indicates a multiline string - ReadChar(); - return allowML(); + case '"': + case '\'': + case '\\': + case '/': sb.Append(c); break; + case 'b': sb.Append('\x8'); break; + case 'f': sb.Append('\f'); break; + case 'n': sb.Append('\n'); break; + case 'r': sb.Append('\r'); break; + case 't': sb.Append('\t'); break; + case 'u': + ushort cp = 0; + for (int j = 0; j < 4; j++) + { + if (++i >= end) + throw ParseError("Incomplete unicode character escape literal"); + cp <<= 4; + c = buffer[i]; + if (c >= '0' && c <= '9') cp += (ushort)(c - '0'); + else if (c >= 'A' && c <= 'F') cp += (ushort)(c - 'A' + 10); + else if (c >= 'a' && c <= 'f') cp += (ushort)(c - 'a' + 10); + else throw ParseError("Bad \\u char " + c); + } + sb.Append((char)cp); + break; + default: + throw ParseError("Invalid JSON string literal; unexpected escape character"); } - else return sb.ToString(); - } - else if (c == '\n' || c == '\r') - { - throw ParseError("Bad string containing newline"); - } - else if (c != '\\') - { - sb.Append((char)c); - continue; - } - - // escaped expression - c = ReadChar(); - if (c < 0) - throw ParseError("Invalid JSON string literal; incomplete escape sequence"); - switch (c) - { - case '"': - case '\'': - case '\\': - case '/': sb.Append((char)c); break; - case 'b': sb.Append('\x8'); break; - case 'f': sb.Append('\f'); break; - case 'n': sb.Append('\n'); break; - case 'r': sb.Append('\r'); break; - case 't': sb.Append('\t'); break; - case 'u': - ushort cp = 0; - for (int i = 0; i < 4; i++) - { - cp <<= 4; - if ((c = ReadChar()) < 0) - throw ParseError("Incomplete unicode character escape literal"); - if (c >= '0' && c <= '9') cp += (ushort)(c - '0'); - else if (c >= 'A' && c <= 'F') cp += (ushort)(c - 'A' + 10); - else if (c >= 'a' && c <= 'f') cp += (ushort)(c - 'a' + 10); - else throw ParseError("Bad \\u char " + (char)c); - } - sb.Append((char)cp); - break; - default: - throw ParseError("Invalid JSON string literal; unexpected escape character"); } + result = sb.ToString(); } + + // Update reader state (no newlines in valid strings) + if (ReadWsc) white.Append(buffer, start, end + 1 - start); + pos = end + 1; + Column += end - start + 1; + + return result; } public void Expect(char expected) diff --git a/Hjson/HjsonReader.cs b/Hjson/HjsonReader.cs index 2a0da8c..68529f4 100644 --- a/Hjson/HjsonReader.cs +++ b/Hjson/HjsonReader.cs @@ -13,6 +13,18 @@ internal class HjsonReader : BaseReader { readonly StringBuilder sb = new(); readonly IEnumerable dsfProviders = []; + readonly bool hasDsfProviders; + + public HjsonReader(string input, IJsonReader jsonReader, HjsonOptions options) + : base(input, jsonReader) + { + if (options != null) + { + ReadWsc = options.KeepWsc; + dsfProviders = options.DsfProviders; + } + hasDsfProviders = dsfProviders.Any(); + } public HjsonReader(TextReader reader, IJsonReader jsonReader, HjsonOptions options) : base(reader, jsonReader) @@ -22,6 +34,7 @@ public HjsonReader(TextReader reader, IJsonReader jsonReader, HjsonOptions optio ReadWsc = options.KeepWsc; dsfProviders = options.DsfProviders; } + hasDsfProviders = dsfProviders.Any(); } public JsonValue Read() @@ -35,18 +48,25 @@ public JsonValue Read() case '{': return checkTrailing(ReadCore()); default: - try + if (RemainingContains(':')) { - // assume we have a root object without braces - return checkTrailing(ReadCore(true)); + try + { + // assume we have a root object without braces + return checkTrailing(ReadCore(true)); + } + catch (Exception) + { + // test if we are dealing with a single JSON value instead (true/false/null/num/"") + Reset(); + try { return checkTrailing(ReadCore()); } + catch (Exception) { } + throw; // throw original error + } } - catch (Exception) + else { - // test if we are dealing with a single JSON value instead (true/false/null/num/"") - Reset(); - try { return checkTrailing(ReadCore()); } - catch (Exception) { } - throw; // throw original error + return checkTrailing(ReadCore()); } } } @@ -276,6 +296,18 @@ string readMlString() } internal static bool TryParseNumericLiteral(string text, bool stopAtNext, out JsonValue value) + => TryParseNumericLiteral(text.AsSpan(), stopAtNext, out value); + + internal static bool TryParseNumericLiteral(StringBuilder sb, bool stopAtNext, out JsonValue value) + { + int len = sb.Length; + if (len == 0) { value = null; return false; } + Span buf = len <= 64 ? stackalloc char[len] : new char[len]; + for (int i = 0; i < len; i++) buf[i] = sb[i]; + return TryParseNumericLiteral(buf, stopAtNext, out value); + } + + internal static bool TryParseNumericLiteral(ReadOnlySpan text, bool stopAtNext, out JsonValue value) { int c, leadingZeros = 0, p = 0; bool testLeading = true; @@ -305,9 +337,11 @@ internal static bool TryParseNumericLiteral(string text, bool stopAtNext, out Js if (testLeading) leadingZeros--; // single 0 is allowed if (leadingZeros > 0) return false; - // fraction + bool hasFracOrExp = false; + if (p < len && text[p] == '.') { + hasFracOrExp = true; if (leadingZeros < 0) return false; int fdigits = 0; p++; @@ -328,6 +362,7 @@ internal static bool TryParseNumericLiteral(string text, bool stopAtNext, out Js c = text[p]; if (c == 'e' || c == 'E') { + hasFracOrExp = true; p++; if (p >= len) return false; @@ -362,10 +397,9 @@ internal static bool TryParseNumericLiteral(string text, bool stopAtNext, out Js if (p != len && !foundStop) return false; - var numSpan = text.AsSpan(0, numEnd); + var numSpan = text[..numEnd]; - // Try parsing as long first to preserve precision for large integers - if (long.TryParse(numSpan, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out long lval)) + if (!hasFracOrExp && long.TryParse(numSpan, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out long lval)) { // Preserve negative zero as double (-0 as long is just 0) if (lval == 0 && numSpan[0] == '-') { value = -0.0; return true; } @@ -409,7 +443,7 @@ JsonValue readTfnns(int c) default: if (ch is '-' || (ch is >= '0' and <= '9')) { - if (TryParseNumericLiteral(sb.ToString(), false, out var res)) return res; + if (TryParseNumericLiteral(sb, false, out var res)) return res; } break; } @@ -417,7 +451,9 @@ JsonValue readTfnns(int c) if (isEol) { // remove any whitespace at the end (ignored in quoteless strings) - return HjsonDsf.Parse(dsfProviders, sb.ToString().Trim()); + var str = SbTrimEnd(sb); + if (!hasDsfProviders) return str; + return HjsonDsf.Parse(dsfProviders, str); } } ReadChar(); @@ -426,6 +462,16 @@ JsonValue readTfnns(int c) } } + /// Returns the content of the StringBuilder with trailing whitespace removed, without allocating extra. + static string SbTrimEnd(StringBuilder sb) + { + int end = sb.Length - 1; + while (end >= 0 && sb[end] <= ' ') end--; + if (end < 0) return ""; + if (end == sb.Length - 1) return sb.ToString(); + return sb.ToString(0, end + 1); + } + /// Checks if the trimmed content of a StringBuilder equals a target string, without allocating. static bool SbTrimEquals(StringBuilder sb, string target) { diff --git a/Hjson/HjsonValue.cs b/Hjson/HjsonValue.cs index 7b3178c..6041188 100644 --- a/Hjson/HjsonValue.cs +++ b/Hjson/HjsonValue.cs @@ -14,13 +14,13 @@ public static class HjsonValue /// Loads Hjson/JSON from a file. public static JsonValue Load(string path) { - return load(path, null, null); + return loadFile(path, null, null); } /// Loads Hjson/JSON from a file, optionally preserving whitespace and comments. public static JsonValue Load(string path, HjsonOptions options) { - return load(path, null, options); + return loadFile(path, null, options); } /// Loads Hjson/JSON from a stream. @@ -54,7 +54,7 @@ public static JsonValue LoadWsc(TextReader textReader) return load(textReader, null, new HjsonOptions { KeepWsc = true }); } - static JsonValue load(string path, IJsonReader jsonReader, HjsonOptions options) + static JsonValue loadFile(string path, IJsonReader jsonReader, HjsonOptions options) { if (Path.GetExtension(path).ToLower() == ".json") return JsonValue.Load(path); try @@ -77,18 +77,24 @@ static JsonValue load(TextReader textReader, IJsonReader jsonReader, HjsonOption return new HjsonReader(textReader, jsonReader, options).Read(); } + static JsonValue load(string input, IJsonReader jsonReader, HjsonOptions options) + { + ArgumentNullException.ThrowIfNull(input); + return new HjsonReader(input, jsonReader, options).Read(); + } + /// Parses the specified Hjson/JSON string. public static JsonValue Parse(string hjsonString) { ArgumentNullException.ThrowIfNull(hjsonString); - return Load(new StringReader(hjsonString)); + return load(hjsonString, null, null); } /// Parses the specified Hjson/JSON string, optionally preserving whitespace and comments. public static JsonValue Parse(string hjsonString, HjsonOptions options) { ArgumentNullException.ThrowIfNull(hjsonString); - return Load(new StringReader(hjsonString), options); + return load(hjsonString, null, options); } /// Saves Hjson to a file. @@ -125,7 +131,7 @@ internal static bool IsPunctuatorChar(char ch) [Obsolete("Use HjsonOptions for preserveComments")] public static JsonValue Load(string path, bool preserveComments) { - return load(path, null, new HjsonOptions { KeepWsc = preserveComments }); + return loadFile(path, null, new HjsonOptions { KeepWsc = preserveComments }); } /// Loads Hjson/JSON from a stream, optionally preserving whitespace and comments. From d29f5d2533900c379835b32cf63110a84aab813a Mon Sep 17 00:00:00 2001 From: v Date: Mon, 9 Mar 2026 09:35:11 +0100 Subject: [PATCH 9/9] prepare nuget release --- .github/workflows/release.yml | 13 ++++++++----- Hjson.nuspec | 22 ---------------------- Hjson/Hjson.csproj | 22 +++++++++++++++------- Hjson/Properties/AssemblyInfo.cs | 6 ------ build-core | 28 ---------------------------- history.md | 4 ++++ 6 files changed, 27 insertions(+), 68 deletions(-) delete mode 100644 Hjson.nuspec delete mode 100755 build-core diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9e8c016..d4d3e63 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,7 @@ name: Release on: push: tags: - - 'v*' # Trigger on tags like v1.0.0 + - 'v*' # Trigger on tags like v4.0.0 or v4.0.0-rc.1 jobs: publish: @@ -17,12 +17,15 @@ jobs: with: dotnet-version: '10.0.x' + - name: Extract version from tag + id: version + run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + - name: Restore dependencies run: dotnet restore Hjson/Hjson.csproj - name: Pack Hjson - run: dotnet pack Hjson/Hjson.csproj --configuration Release --no-restore -o ./artifacts + run: dotnet pack Hjson/Hjson.csproj --configuration Release --no-restore -o ./artifacts /p:Version=${{ steps.version.outputs.VERSION }} - # Uncomment this when you are ready to publish! - # - name: Push to NuGet - # run: dotnet nuget push ./artifacts/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate + - name: Push to NuGet + run: dotnet nuget push ./artifacts/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/Hjson.nuspec b/Hjson.nuspec deleted file mode 100644 index e3b6b4c..0000000 --- a/Hjson.nuspec +++ /dev/null @@ -1,22 +0,0 @@ - - - - Hjson - $version$ - Hjson - Christian Zangl - Christian Zangl - https://github.com/laktak/hjson-cs/blob/master/LICENSE - http://hjson.github.io - false - Hjson, a user interface for JSON. Relaxed syntax, fewer mistakes, more comments. Supports .NET Core, .NET 4.x and Mono. For details go to http://hjson.github.io. - Copyright Christian Zangl - JSON comments config hjson parser serializer - - - - - - - - diff --git a/Hjson/Hjson.csproj b/Hjson/Hjson.csproj index 9e65514..348c7d6 100755 --- a/Hjson/Hjson.csproj +++ b/Hjson/Hjson.csproj @@ -1,17 +1,25 @@  - 3.0.1 net10.0 true Hjson + + Hjson - false - false - false - false - false - false + 4.0.0 + Christian Zangl;V + Hjson, a user interface for JSON. Relaxed syntax, fewer mistakes, more comments. For details go to http://hjson.github.io. + Copyright Christian Zangl + JSON;comments;config;hjson;parser;serializer + MIT + https://hjson.github.io + https://github.com/hjson/hjson-cs + README.md + + + + diff --git a/Hjson/Properties/AssemblyInfo.cs b/Hjson/Properties/AssemblyInfo.cs index ff59006..0829af8 100644 --- a/Hjson/Properties/AssemblyInfo.cs +++ b/Hjson/Properties/AssemblyInfo.cs @@ -1,7 +1 @@ -[assembly: System.Reflection.AssemblyVersion("1.0.0.0")] -[assembly: System.Reflection.AssemblyDescription("Hjson.dll")] -[assembly: System.Reflection.AssemblyProduct("Hjson - https://github.com/hjson/hjson-cs")] -[assembly: System.Reflection.AssemblyCopyright("See LICENSE")] -[assembly: System.Reflection.AssemblyFileVersion("3.0.1.0")] -[assembly: System.Reflection.AssemblyTitle("Hjson.dll")] [assembly: System.CLSCompliant(true)] \ No newline at end of file diff --git a/build-core b/build-core deleted file mode 100755 index c718d52..0000000 --- a/build-core +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash -set -e # exit script on error -BASE=`dirname "$(readlink -f "$0")"` -cd $BASE - -VERSION=`cat Hjson/Hjson.csproj | sed -rn 's/.*VersionPrefix>(.*)<\/.*/\1/p'` -VERSION2=`cat Hjson/Properties/AssemblyInfo.cs | sed -rn 's/.*AssemblyFileVersion\("(.*).0"\).*/\1/p'` -if [[ "$VERSION" != "$VERSION2" ]]; then echo error: version mismatch $VERSION:$VERSION2; exit; fi - -echo ------------------ -echo build core - -#dotnet restore Hjson -#dotnet restore cli -#dotnet restore test -#dotnet restore sample/sample.csproj -dotnet build Hjson -c Release -dotnet build cli -c Release -dotnet build test -c Release - -# test -dotnet run --project test/test.csproj -- test/assets - -echo ------------------ -echo pkg - -dotnet pack Hjson -c Release - diff --git a/history.md b/history.md index c2856f7..d993b4c 100644 --- a/history.md +++ b/history.md @@ -1,5 +1,9 @@ # hjson-cs History +- v4.0.0 + - Updated to .NET 10 and latest C# features + - Performance tuning + - Added HjsonConvert with attribute support - v3.0.0 - add support for single quoted strings - v2.1.3