[
  {
    "path": ".claude/settings.local.json",
    "content": "{\n  \"permissions\": {\n    \"allow\": [\n      \"Bash(dotnet build:*)\",\n      \"Bash(dotnet test:*)\"\n    ]\n  }\n}\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nindent_style = space\nend_of_line = lf\ninsert_final_newline = true\n\n[*.cs]\nindent_size = 4\ncharset = utf-8\n\n# Redundant accessor body\nresharper_redundant_accessor_body_highlighting = error\n\n# Replace with field keyword\nresharper_replace_with_field_keyword_highlighting = error\n\n# Replace with single call to Single(..)\nresharper_replace_with_single_call_to_single_highlighting = error\n\n# Replace with single call to SingleOrDefault(..)\nresharper_replace_with_single_call_to_single_or_default_highlighting = error\n\n# Replace with single call to LastOrDefault(..)\nresharper_replace_with_single_call_to_last_or_default_highlighting = error\n\n# Element is localizable\nresharper_localizable_element_highlighting = none\n\n# Replace with single call to Last(..)\nresharper_replace_with_single_call_to_last_highlighting = error\n\n# Replace with single call to First(..)\nresharper_replace_with_single_call_to_first_highlighting = error\n\n# Replace with single call to FirstOrDefault(..)\nresharper_replace_with_single_call_to_first_or_default_highlighting = error\n\n# Replace with single call to Any(..)\nresharper_replace_with_single_call_to_any_highlighting = error\n\n# Simplify negative equality expression\nresharper_negative_equality_expression_highlighting = error\n\n# Replace with single call to Count(..)\nresharper_replace_with_single_call_to_count_highlighting = error\n\n# Declare types in namespaces\ndotnet_diagnostic.CA1050.severity = none\n\n# Use Literals Where Appropriate\ndotnet_diagnostic.CA1802.severity = error\n\n# Template should be a static expression\ndotnet_diagnostic.CA2254.severity = error\n\n# Potentially misleading parameter name in lambda or local function\nresharper_all_underscore_local_parameter_name_highlighting = none\n\n# Redundant explicit collection creation in argument of 'params' parameter\nresharper_redundant_explicit_params_array_creation_highlighting = error\n\n# Do not initialize unnecessarily\ndotnet_diagnostic.CA1805.severity = error\n\n# Avoid unsealed attributes\ndotnet_diagnostic.CA1813.severity = error\n\n# Test for empty strings using string length\ndotnet_diagnostic.CA1820.severity = none\n\n# Remove empty finalizers\ndotnet_diagnostic.CA1821.severity = error\n\n# Mark members as static\ndotnet_diagnostic.CA1822.severity = error\n\n# Avoid unused private fields\ndotnet_diagnostic.CA1823.severity = error\n\n# Avoid zero-length array allocations\ndotnet_diagnostic.CA1825.severity = error\n\n# Use property instead of Linq Enumerable method\ndotnet_diagnostic.CA1826.severity = error\n\n# Do not use Count()/LongCount() when Any() can be used\ndotnet_diagnostic.CA1827.severity = error\ndotnet_diagnostic.CA1828.severity = error\n\n# Use Length/Count property instead of Enumerable.Count method\ndotnet_diagnostic.CA1829.severity = error\n\n# Prefer strongly-typed Append and Insert method overloads on StringBuilder\ndotnet_diagnostic.CA1830.severity = error\n\n# Use AsSpan instead of Range-based indexers for string when appropriate\ndotnet_diagnostic.CA1831.severity = error\n\n# Use AsSpan instead of Range-based indexers for string when appropriate\ndotnet_diagnostic.CA1831.severity = error\ndotnet_diagnostic.CA1832.severity = error\ndotnet_diagnostic.CA1833.severity = error\n\n# Use StringBuilder.Append(char) for single character strings\ndotnet_diagnostic.CA1834.severity = error\n\n# Prefer IsEmpty over Count when available\ndotnet_diagnostic.CA1836.severity = error\n\n# Prefer IsEmpty over Count when available\ndotnet_diagnostic.CA1836.severity = error\n\n# Use Environment.ProcessId instead of Process.GetCurrentProcess().Id\ndotnet_diagnostic.CA1837.severity = error\n\n# Use Environment.ProcessPath instead of Process.GetCurrentProcess().MainModule.FileName\ndotnet_diagnostic.CA1839.severity = error\n\n# Use Environment.CurrentManagedThreadId instead of Thread.CurrentThread.ManagedThreadId\ndotnet_diagnostic.CA1840.severity = error\n\n# Prefer Dictionary Contains methods\ndotnet_diagnostic.CA1841.severity = error\n\n# Do not use WhenAll with a single task\ndotnet_diagnostic.CA1842.severity = error\n\n# Do not use WhenAll/WaitAll with a single task\ndotnet_diagnostic.CA1842.severity = error\ndotnet_diagnostic.CA1843.severity = error\n\n# Use span-based 'string.Concat'\ndotnet_diagnostic.CA1845.severity = error\n\n# Prefer AsSpan over Substring\ndotnet_diagnostic.CA1846.severity = error\n\n# Use string.Contains(char) instead of string.Contains(string) with single characters\ndotnet_diagnostic.CA1847.severity = error\n\n# Prefer static HashData method over ComputeHash\ndotnet_diagnostic.CA1850.severity = error\n\n# Possible multiple enumerations of IEnumerable collection\ndotnet_diagnostic.CA1851.severity = error\n\n# Unnecessary call to Dictionary.ContainsKey(key)\ndotnet_diagnostic.CA1853.severity = error\n\n# Prefer the IDictionary.TryGetValue(TKey, out TValue) method\ndotnet_diagnostic.CA1854.severity = error\n\n# Use Span<T>.Clear() instead of Span<T>.Fill()\ndotnet_diagnostic.CA1855.severity = error\n\n# Incorrect usage of ConstantExpected attribute\ndotnet_diagnostic.CA1856.severity = error\n\n# The parameter expects a constant for optimal performance\ndotnet_diagnostic.CA1857.severity = error\n\n# Use StartsWith instead of IndexOf\ndotnet_diagnostic.CA1858.severity = error\n\n# Avoid using Enumerable.Any() extension method\ndotnet_diagnostic.CA1860.severity = error\n\n# Avoid constant arrays as arguments\ndotnet_diagnostic.CA1861.severity = error\n\n# Use the StringComparison method overloads to perform case-insensitive string comparisons\ndotnet_diagnostic.CA1862.severity = error\n\n# Prefer the IDictionary.TryAdd(TKey, TValue) method\ndotnet_diagnostic.CA1864.severity = error\n\n# Use string.Method(char) instead of string.Method(string) for string with single char\ndotnet_diagnostic.CA1865.severity = error\ndotnet_diagnostic.CA1866.severity = error\ndotnet_diagnostic.CA1867.severity = error\n\n# Unnecessary call to 'Contains' for sets\ndotnet_diagnostic.CA1868.severity = error\n\n# Cache and reuse 'JsonSerializerOptions' instances\ndotnet_diagnostic.CA1869.severity = error\n\n# Use a cached 'SearchValues' instance\ndotnet_diagnostic.CA1870.severity = error\n\n# Microsoft .NET properties\ntrim_trailing_whitespace = true\ncsharp_preferred_modifier_order = public, private, protected, internal, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async:suggestion\nresharper_namespace_body = file_scoped\ndotnet_naming_rule.private_constants_rule.severity = warning\ndotnet_naming_rule.private_constants_rule.style = lower_camel_case_style\ndotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols\ndotnet_naming_rule.private_instance_fields_rule.severity = warning\ndotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style\ndotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols\ndotnet_naming_rule.private_static_fields_rule.severity = warning\ndotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style\ndotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols\ndotnet_naming_rule.private_static_readonly_rule.severity = warning\ndotnet_naming_rule.private_static_readonly_rule.style = lower_camel_case_style\ndotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols\ndotnet_naming_style.lower_camel_case_style.capitalization = camel_case\ndotnet_naming_style.upper_camel_case_style.capitalization = pascal_case\ndotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private\ndotnet_naming_symbols.private_constants_symbols.applicable_kinds = field\ndotnet_naming_symbols.private_constants_symbols.required_modifiers = const\ndotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities = private\ndotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds = field\ndotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private\ndotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field\ndotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static\ndotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private\ndotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field\ndotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static, readonly\ndotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none\ndotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none\ndotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none\n\n# ReSharper properties\nresharper_object_creation_when_type_not_evident = target_typed\n\n# ReSharper inspection severities\nresharper_arrange_object_creation_when_type_evident_highlighting = error\nresharper_arrange_object_creation_when_type_not_evident_highlighting = error\nresharper_arrange_redundant_parentheses_highlighting = error\nresharper_arrange_static_member_qualifier_highlighting = error\nresharper_arrange_this_qualifier_highlighting = error\nresharper_arrange_type_member_modifiers_highlighting = none\nresharper_built_in_type_reference_style_for_member_access_highlighting = hint\nresharper_built_in_type_reference_style_highlighting = hint\nresharper_check_namespace_highlighting = none\nresharper_convert_to_using_declaration_highlighting = error\nresharper_field_can_be_made_read_only_local_highlighting = none\nresharper_merge_into_logical_pattern_highlighting = warning\nresharper_merge_into_pattern_highlighting = error\nresharper_method_has_async_overload_highlighting = warning\n# because stop rider giving errors before source generators have run\nresharper_partial_type_with_single_part_highlighting = warning\nresharper_redundant_base_qualifier_highlighting = warning\nresharper_redundant_cast_highlighting = error\nresharper_redundant_empty_object_creation_argument_list_highlighting = error\nresharper_redundant_empty_object_or_collection_initializer_highlighting = error\nresharper_redundant_name_qualifier_highlighting = error\nresharper_redundant_suppress_nullable_warning_expression_highlighting = error\nresharper_redundant_using_directive_highlighting = error\nresharper_redundant_verbatim_string_prefix_highlighting = error\nresharper_redundant_lambda_signature_parentheses_highlighting = error\nresharper_replace_substring_with_range_indexer_highlighting = warning\nresharper_suggest_var_or_type_built_in_types_highlighting = error\nresharper_suggest_var_or_type_elsewhere_highlighting = error\nresharper_suggest_var_or_type_simple_types_highlighting = error\nresharper_unnecessary_whitespace_highlighting = error\nresharper_use_await_using_highlighting = warning\nresharper_use_deconstruction_highlighting = warning\n\n# Sort using and Import directives with System.* appearing first\ndotnet_sort_system_directives_first = true\n\n# Avoid \"this.\" and \"Me.\" if not necessary\ndotnet_style_qualification_for_field = false:error\ndotnet_style_qualification_for_property = false:error\ndotnet_style_qualification_for_method = false:error\ndotnet_style_qualification_for_event = false:error\n\n# Use language keywords instead of framework type names for type references\ndotnet_style_predefined_type_for_locals_parameters_members = true:error\ndotnet_style_predefined_type_for_member_access = true:error\n\n# Suggest more modern language features when available\ndotnet_style_object_initializer = true:error\ndotnet_style_collection_initializer = true:error\ndotnet_style_coalesce_expression = false:error\ndotnet_style_null_propagation = true:error\ndotnet_style_explicit_tuple_names = true:error\n\n# Use collection expression syntax\nresharper_use_collection_expression_highlighting = error\n\n# Prefer \"var\" everywhere\ncsharp_style_var_for_built_in_types = true:error\ncsharp_style_var_when_type_is_apparent = true:error\ncsharp_style_var_elsewhere = true:error\n\n# Prefer method-like constructs to have a block body\ncsharp_style_expression_bodied_methods = true:error\ncsharp_style_expression_bodied_local_functions = true:error\ncsharp_style_expression_bodied_constructors = true:error\ncsharp_style_expression_bodied_operators = true:error\nresharper_place_expr_method_on_single_line = false\n\n# Prefer property-like constructs to have an expression-body\ncsharp_style_expression_bodied_properties = true:error\ncsharp_style_expression_bodied_indexers = true:error\ncsharp_style_expression_bodied_accessors = true:error\n\n# Suggest more modern language features when available\ncsharp_style_pattern_matching_over_is_with_cast_check = true:error\ncsharp_style_pattern_matching_over_as_with_null_check = true:error\ncsharp_style_inlined_variable_declaration = true:suggestion\ncsharp_style_throw_expression = true:suggestion\ncsharp_style_conditional_delegate_call = true:suggestion\n\n# Newline settings\n#csharp_new_line_before_open_brace = all:error\nresharper_max_array_initializer_elements_on_line = 1\ncsharp_new_line_before_else = true\ncsharp_new_line_before_catch = true\ncsharp_new_line_before_finally = true\ncsharp_new_line_before_members_in_object_initializers = true\ncsharp_new_line_before_members_in_anonymous_types = true\nresharper_wrap_before_first_type_parameter_constraint = true\nresharper_wrap_extends_list_style = chop_always\nresharper_wrap_after_dot_in_method_calls = false\nresharper_wrap_before_binary_pattern_op = false\nresharper_wrap_object_and_collection_initializer_style = chop_always\nresharper_place_simple_initializer_on_single_line = false\n\n# space\nresharper_space_around_lambda_arrow = true\n\ndotnet_style_require_accessibility_modifiers = never:error\nresharper_place_type_constraints_on_same_line = false\nresharper_blank_lines_inside_namespace = 0\nresharper_blank_lines_after_file_scoped_namespace_directive = 1\nresharper_blank_lines_inside_type = 0\n\nresharper_place_attribute_on_same_line = false\n\n#braces https://www.jetbrains.com/help/resharper/EditorConfig_CSHARP_CSharpCodeStylePageImplSchema.html#Braces\nresharper_braces_for_ifelse = required\nresharper_braces_for_foreach = required\nresharper_braces_for_while = required\nresharper_braces_for_dowhile = required\nresharper_braces_for_lock = required\nresharper_braces_for_fixed = required\nresharper_braces_for_for = required\n\nresharper_return_value_of_pure_method_is_not_used_highlighting = error\n\nresharper_member_hides_interface_member_with_default_implementation_highlighting = error\n\nresharper_misleading_body_like_statement_highlighting = error\n\nresharper_redundant_record_class_keyword_highlighting = error\n\nresharper_redundant_extends_list_entry_highlighting = error\n\nresharper_redundant_type_arguments_inside_nameof_highlighting = error\n\n# Xml files\n[*.{xml,config,nuspec,resx,vsixmanifest,csproj,targets,props,fsproj}]\nindent_size = 2\n# https://www.jetbrains.com/help/resharper/EditorConfig_XML_XmlCodeStylePageSchema.html#resharper_xml_blank_line_after_pi\nresharper_blank_line_after_pi = false\nresharper_space_before_self_closing = true\nij_xml_space_inside_empty_tag = true\n\n[*.json]\nindent_size = 2\n\n# Verify settings\n[*.{received,verified}.{txt,xml,json,md,sql,csv,html,htm,nuspec,rels}]\ncharset = utf-8-bom\nend_of_line = lf\nindent_size = unset\nindent_style = unset\ninsert_final_newline = false\ntab_width = unset\ntrim_trailing_whitespace = false"
  },
  {
    "path": ".gitattributes",
    "content": "# Auto detect text files and normalize line endings to LF\n* text=auto eol=lf\n*.png binary\n*.snk binary\n\n*.verified.txt text eol=lf working-tree-encoding=UTF-8\n*.verified.xml text eol=lf working-tree-encoding=UTF-8\n*.verified.json text eol=lf working-tree-encoding=UTF-8\n\n.editorconfig text eol=lf working-tree-encoding=UTF-8\n*.sln.DotSettings text eol=lf working-tree-encoding=UTF-8\n*.slnx.DotSettings text eol=lf working-tree-encoding=UTF-8"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: SimonCropp\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug fix\nabout: Create a bug fix to help us improve\n---\n\nNote: New issues raised, where it is clear the submitter has not read the issue template, are likely to be closed with \"please read the issue template\". Please don't take offense at this. It is simply a time management decision. If someone raises an issue, and can't be bothered to spend the time to read the issue template, then the project maintainers should not be expected to spend the time to read the submitted issue. Often too much time is spent going back and forth in issue comments asking for information that is outlined in the issue template.\n\n\n#### Preamble\n\nWhere relevant, ensure you are using the current stable versions on your development stack. For example:\n\n * Visual Studio\n * [.NET SDK or .NET Core SDK](https://www.microsoft.com/net/download)\n * Any related NuGet packages\n\nAny code or stack traces must be properly formatted with [GitHub markdown](https://guides.github.com/features/mastering-markdown/).\n\n\n#### Describe the bug\n\nA clear and concise description of what the bug is. Include any relevant version information.\n\nA clear and concise description of what you expected to happen.\n\nAdd any other context about the problem here.\n\n\n#### Minimal Repro\n\nEnsure you have replicated the bug in a minimal solution with the fewest moving parts. Often this will help point to the true cause of the problem. Upload this repro as part of the issue, preferably a public GitHub repository or a downloadable zip. The repro will allow the maintainers of this project to smoke test the any fix.\n\n#### Submit a PR that fixes the bug\n\nSubmit a [Pull Request (PR)](https://help.github.com/articles/about-pull-requests/) that fixes the bug. Include in this PR a test that verifies the fix. If you were not able to fix the bug, a PR that illustrates your partial progress will suffice."
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: How to raise feature requests\n---\n\n\nNote: New issues raised, where it is clear the submitter has not read the issue template, are likely to be closed with \"please read the issue template\". Please don't take offense at this. It is simply a time management decision. If someone raises an issue, and can't be bothered to spend the time to read the issue template, then the project maintainers should not be expected to spend the time to read the submitted issue. Often too much time is spent going back and forth in issue comments asking for information that is outlined in the issue template.\n\nIf you are certain the feature will be accepted, it is better to raise a [Pull Request (PR)](https://help.github.com/articles/about-pull-requests/).\n\nIf you are uncertain if the feature will be accepted, outline the proposal below to confirm it is viable, prior to raising a PR that implements the feature.\n\nNote that even if the feature is a good idea and viable, it may not be accepted since the ongoing effort in maintaining the feature may outweigh the benefit it delivers.\n\n\n#### Is the feature request related to a problem\n\nA clear and concise description of what the problem is.\n\n\n#### Describe the solution\n\nA clear and concise proposal of how you intend to implement the feature.\n\n\n#### Describe alternatives considered\n\nA clear and concise description of any alternative solutions or features you've considered.\n\n\n#### Additional context\n\nAdd any other context about the feature request here.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n- package-ecosystem: nuget\n  directory: \"/src\"\n  schedule:\n    interval: daily\n  open-pull-requests-limit: 10\n"
  },
  {
    "path": ".github/stale.yml",
    "content": "# Number of days of inactivity before an issue becomes stale\ndaysUntilStale: 7\n# Number of days of inactivity before a stale issue is closed\ndaysUntilClose: 7\n# Set to true to ignore issues in a milestone (defaults to false)\nexemptMilestones: true\n# Comment to post when marking an issue as stale. Set to `false` to disable\nmarkComment: >\n  This issue has been automatically marked as stale because it has not had\n  recent activity. It will be closed if no further activity occurs. Thank you\n  for your contributions.\n# Comment to post when closing a stale issue. Set to `false` to disable\ncloseComment: false\n# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':\npulls:\n  daysUntilStale: 30\nexemptLabels:\n  - Question\n  - Bug\n  - Feature\n  - Improvement"
  },
  {
    "path": ".github/workflows/merge-dependabot.yml",
    "content": "name: merge-dependabot\non:\n  pull_request:\njobs:\n  automerge:\n    runs-on: ubuntu-latest\n    if: github.actor == 'dependabot[bot]'\n    steps:\n      - name: Dependabot Auto Merge\n        uses: ahmadnassri/action-dependabot-auto-merge@v2.6.6\n        with:\n          target: minor\n          github-token: ${{ secrets.dependabot }}\n          command: squash and merge"
  },
  {
    "path": ".github/workflows/milestone-release.yml",
    "content": "name: milestone-release\n\non:\n  push:\n    tags:\n      - '*'\n  milestone:\n    types: [created, edited, closed, opened]\n  issues:\n    types: [opened, edited, closed, reopened, deleted, milestoned, demilestoned]\n  pull_request:\n    types: [opened, edited, closed, reopened, milestoned, demilestoned]\n  workflow_dispatch:\n    inputs:\n      milestone:\n        description: 'Milestone title to rebuild (leave empty to rebuild all)'\n        required: false\n        type: string\n\npermissions:\n  contents: write\n\njobs:\n  sync-release:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Sync Release with Milestone\n        uses: actions/github-script@v7\n        with:\n          script: |\n            const { owner, repo } = context.repo;\n\n            // Helper: Find release by tag name\n            async function findRelease(tagName) {\n              for await (const response of github.paginate.iterator(\n                github.rest.repos.listReleases,\n                { owner, repo, per_page: 100 }\n              )) {\n                const release = response.data.find(r => r.tag_name === tagName);\n                if (release) return release;\n              }\n              return null;\n            }\n\n            // Helper: Find milestone by title\n            async function findMilestone(title) {\n              for await (const response of github.paginate.iterator(\n                github.rest.issues.listMilestones,\n                { owner, repo, state: 'all', per_page: 100 }\n              )) {\n                const milestone = response.data.find(m => m.title === title);\n                if (milestone) return milestone;\n              }\n              return null;\n            }\n\n            // Helper: Generate release body from milestone\n            async function generateBody(milestoneNumber) {\n              const items = [];\n              for await (const response of github.paginate.iterator(\n                github.rest.issues.listForRepo,\n                { owner, repo, milestone: milestoneNumber, state: 'all', per_page: 100 }\n              )) {\n                items.push(...response.data);\n              }\n              items.sort((a, b) => a.number - b.number);\n              return items.map(item => {\n                const checkbox = item.state === 'closed' ? '[x]' : '[ ]';\n                return `- ${checkbox} [#${item.number}](${item.html_url}) ${item.title}`;\n              }).join('\\n') || 'No issues in this milestone yet.';\n            }\n\n            // Helper: Update existing release only\n            async function updateReleaseIfExists(milestone) {\n              const release = await findRelease(milestone.title);\n              if (!release) {\n                console.log(`No release found for ${milestone.title}, skipping`);\n                return;\n              }\n              const body = await generateBody(milestone.number);\n              await github.rest.repos.updateRelease({\n                owner, repo,\n                release_id: release.id,\n                body: body\n              });\n              console.log(`Updated release: ${milestone.title}`);\n            }\n\n            // Handle tag push events\n            if (context.eventName === 'push' && context.ref.startsWith('refs/tags/')) {\n              const tagName = context.ref.replace('refs/tags/', '');\n\n              // Tag deleted\n              if (context.payload.deleted) {\n                const release = await findRelease(tagName);\n                if (release) {\n                  await github.rest.repos.deleteRelease({\n                    owner, repo, release_id: release.id\n                  });\n                  console.log(`Deleted release for tag: ${tagName}`);\n                }\n                return;\n              }\n\n              // Tag created - create release\n              const milestone = await findMilestone(tagName);\n              const body = milestone\n                ? await generateBody(milestone.number)\n                : '';\n\n              await github.rest.repos.createRelease({\n                owner, repo,\n                tag_name: tagName,\n                name: tagName,\n                body: body,\n                draft: false\n              });\n              console.log(`Created release for tag: ${tagName}`);\n              return;\n            }\n\n            // Handle workflow_dispatch - update only\n            if (context.eventName === 'workflow_dispatch') {\n              const inputMilestone = context.payload.inputs?.milestone;\n              const milestones = [];\n              for await (const response of github.paginate.iterator(\n                github.rest.issues.listMilestones,\n                { owner, repo, state: 'all', per_page: 100 }\n              )) {\n                milestones.push(...response.data);\n              }\n              for (const ms of milestones) {\n                if (!inputMilestone || ms.title === inputMilestone) {\n                  await updateReleaseIfExists(ms);\n                }\n              }\n              return;\n            }\n\n            // Handle milestone/issue/PR events - update only\n            let milestone = context.payload.milestone;\n            if (!milestone && context.payload.issue?.milestone) {\n              milestone = context.payload.issue.milestone;\n            }\n            if (!milestone && context.payload.pull_request?.milestone) {\n              milestone = context.payload.pull_request.milestone;\n            }\n            if (!milestone) {\n              console.log('No milestone associated with this event');\n              return;\n            }\n\n            await updateReleaseIfExists(milestone);\n"
  },
  {
    "path": ".gitignore",
    "content": "*.suo\n*.user\nbin/\nobj/\n.vs/\n*.DotSettings.user\n.idea/\n*.received.*\nnugets/\nnul\n/src/Benchmarks/BenchmarkDotNet.Artifacts\n"
  },
  {
    "path": "claude.md",
    "content": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Project Overview\n\nMarkdownSnippets is a .NET tool/library that extracts code snippets from source files (via `begin-snippet`/`end-snippet` markers and C# regions) and merges them into markdown documents. Distributed as:\n- **dotnet global tool** (`MarkdownSnippets.Tool`, command: `mdsnippets`)\n- **MSBuild task** (`MarkdownSnippets.MsBuild`)\n- **Library** (`MarkdownSnippets`)\n\n## Build Commands\n\nAll commands must be run from the repo root directory.\n\n```bash\n# Build everything\ndotnet build src\n\n# Run all tests\ndotnet test src\n\n# Run a single test project\ndotnet test src/Tests/Tests.csproj\ndotnet test src/ConfigReader.Tests/ConfigReader.Tests.csproj\ndotnet test src/MarkdownSnippets.Tool.Tests/MarkdownSnippets.Tool.Tests.csproj\n\n# Run a specific test by name\ndotnet test src/Tests/Tests.csproj --filter \"FullyQualifiedName~TestClassName.TestMethodName\"\n\n# Pack the tool\ndotnet pack src/MarkdownSnippets.Tool/MarkdownSnippets.Tool.csproj\n```\n\nRequires .NET SDK 10.0 (preview). See `src/global.json` for exact version.\n\n## Architecture\n\nAll source is under `src/`. There is no `.sln` file; build by targeting `src/` or individual `.csproj` files.\n\n### Core Library (`src/MarkdownSnippets/`)\n\nTwo main subsystems:\n\n- **Reading** (`Reading/`): Extracts snippets from source files. `FileSnippetExtractor` scans files for `begin-snippet`/`end-snippet` markers. `StartEndTester` handles marker detection including C# regions. `Snippet` is the data model.\n- **Processing** (`Processing/`): Transforms markdown files. `DirectoryMarkdownProcessor` is the top-level orchestrator that scans directories, collects snippets, and processes markdown. `MarkdownProcessor` handles individual file transformation. `IncludeProcessor` handles include directives.\n\n### Tool (`src/MarkdownSnippets.Tool/`)\n\nCLI entry point. `Program.cs` uses top-level statements. `CommandRunner` parses args via `CommandLineParser` and delegates to `DirectoryMarkdownProcessor`. Shares `ConfigReader` source files via `<Compile Include>` (not a project reference).\n\n### ConfigReader (`src/ConfigReader/`)\n\nReads `mdsnippets.json` configuration files. Source files are compiled directly into the Tool project (not referenced as a library).\n\n### MsBuild Task (`src/MarkdownSnippets.MsBuild/`)\n\nWraps the library as an MSBuild task. Uses `PackageShader.MsBuild` for dependency isolation. Targets netstandard2.0 and net10.0.\n\n## Testing\n\n- Framework: **xunit.v3** with **Verify** (snapshot testing)\n- Snapshots are `.verified.txt` files alongside tests. When a test fails due to output changes, review the `.received.txt` diff and accept with the Verify tooling if correct.\n- Main test project (`src/Tests/`) targets net10.0 (and net48 on Windows)\n- Test data directories (e.g., `DirectoryMarkdownProcessor/`, `SnippetExtractor/`) are copied to output via csproj settings\n- The markdown files in this repo are regenerated by running tests, not as part of the build\n\n## Build Configuration\n\n- `src/Directory.Build.props`: Version (28.0.1), LangVersion preview, TreatWarningsAsErrors, EnforceCodeStyleInBuild\n- `src/Directory.Packages.props`: Central package management with transitive pinning\n- Global type alias: `CharSpan` = `System.ReadOnlySpan<System.Char>` (defined in Directory.Build.props)\n- Multi-targeting: Library targets netstandard2.0/2.1, net48, net8.0, net9.0, net10.0\n\n## Document Conventions\n\nThe tool supports two modes for processing markdown:\n- **SourceTransform** (default): Reads `*.source.md`, writes output to `*.md`\n- **InPlaceOverwrite**: Modifies `*.md` files directly\n"
  },
  {
    "path": "code_of_conduct.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, sex characteristics, gender identity and expression,\nlevel of experience, education, socio-economic status, nationality, personal\nappearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at simon.cropp@gmail.com. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see\nhttps://www.contributor-covenant.org/faq\n"
  },
  {
    "path": "docs/api.md",
    "content": "<!--\nGENERATED FILE - DO NOT EDIT\nThis file was generated by [MarkdownSnippets](https://github.com/SimonCropp/MarkdownSnippets).\nSource File: /docs/mdsource/api.source.md\nTo change this file edit the source file and then run MarkdownSnippets.\n-->\n\n# Library Usage\n\n\n## NuGet package\n\nhttps://nuget.org/packages/MarkdownSnippets/ [![NuGet Status](https://img.shields.io/nuget/v/MarkdownSnippets.svg)](https://www.nuget.org/packages/MarkdownSnippets/)\n\n\n## Reading snippets from files\n\n<!-- snippet: ReadingFilesSimple -->\n<a id='snippet-ReadingFilesSimple'></a>\n```cs\nvar files = Directory.EnumerateFiles(@\"C:\\path\", \"*.cs\", SearchOption.AllDirectories);\n\nvar snippets = FileSnippetExtractor.Read(files);\n```\n<sup><a href='/src/Tests/Snippets/Usage.cs#L8-L14' title='Snippet source file'>snippet source</a> | <a href='#snippet-ReadingFilesSimple' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\n\n## Ignored paths\n\nTo change conventions manipulate lists `MarkdownSnippets.Exclusions.NoAcceptCommentsExtensions` and `MarkdownSnippets.Exclusions.BinaryFileExtensions`.\n"
  },
  {
    "path": "docs/config-file.md",
    "content": "<!--\nGENERATED FILE - DO NOT EDIT\nThis file was generated by [MarkdownSnippets](https://github.com/SimonCropp/MarkdownSnippets).\nSource File: /docs/mdsource/config-file.source.md\nTo change this file edit the source file and then run MarkdownSnippets.\n-->\n\n# Config File\n\nThe [dotnet tool](/readme.md#installation) and the [MSBuild Task](msbuild.md) support a config file convention.\n\nAdd a file named `mdsnippets.json` at the target directory with the following content:\n\n\n## For [InPlaceOverwrite](https://github.com/SimonCropp/MarkdownSnippets#inplaceoverwrite)\n\n<!-- snippet: InPlaceOverwrite.json -->\n<a id='snippet-InPlaceOverwrite.json'></a>\n```json\n{\n  \"$schema\": \"https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/schema.json\",\n  \"Convention\": \"InPlaceOverwrite\"\n}\n```\n<sup><a href='/src/ConfigReader.Tests/InPlaceOverwrite.json#L1-L4' title='Snippet source file'>snippet source</a> | <a href='#snippet-InPlaceOverwrite.json' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\n\n## For [SourceTransform](https://github.com/SimonCropp/MarkdownSnippets#sourcetransform)\n\n<!-- snippet: SourceTransform.json -->\n<a id='snippet-SourceTransform.json'></a>\n```json\n{\n  \"$schema\": \"https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/schema.json\",\n  \"Convention\": \"SourceTransform\"\n}\n```\n<sup><a href='/src/ConfigReader.Tests/SourceTransform.json#L1-L4' title='Snippet source file'>snippet source</a> | <a href='#snippet-SourceTransform.json' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\n\n## All Settings\n\n<!-- snippet: allConfig.json -->\n<a id='snippet-allConfig.json'></a>\n```json\n{\n  \"$schema\": \"https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/schema.json\",\n  \"ReadOnly\": false,\n  \"LinkFormat\": \"Tfs\",\n  \"TocLevel\": 3,\n  \"ExcludeDirectories\": [ \"Dir1\", \"Dir2\" ],\n  \"ExcludeMarkdownDirectories\": [ \"Dir2\", \"Dir3\" ],\n  \"ExcludeSnippetDirectories\": [ \"Dir4\", \"Dir5\" ],\n  \"ExcludeSnippetFiles\": [ \"*.verified.txt\", \"*.received.txt\" ],\n  \"UrlsAsSnippets\": [ \"Url1\", \"Url2\" ],\n  \"TocExcludes\": [ \"Exclude Heading1\", \"Exclude Heading2\" ],\n  \"Convention\": \"InPlaceOverwrite\",\n  \"WriteHeader\": true,\n  \"MaxWidth\": 80,\n  \"Header\": \"GENERATED FILE - Source File: {relativePath}\",\n  \"UrlPrefix\": \"TheUrlPrefix\",\n  \"TreatMissingAsWarning\": true,\n  \"ValidateContent\": true,\n  \"OmitSnippetLinks\": true\n}\n```\n<sup><a href='/src/ConfigReader.Tests/allConfig.json#L1-L20' title='Snippet source file'>snippet source</a> | <a href='#snippet-allConfig.json' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\n\n## JSON Schema\n\nEditor help is available by adding the `$schema` field to the `mdsnippets.json` file.\n\n```json\n{\n  \"$schema\": \"https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/schema.json\"\n}\n```\n\nIn the screenshot, [JetBrains Rider](https://jetbrains.com/rider), is able to offer code completion support.\n\n![IDE schema code completion](/docs/code-completion.png)\n\nThe schema also includes `enum` values for constrained value types.\n\n![IDE schema code completion](/docs/code-completion-values.png)\n\n\n## More Info\n\n * [ReadOnly: Mark resulting files as read only](/readme.md#mark-resulting-files-as-read-only)\n * [LinkFormat](/readme.md#linkformat).\n * [Convention](/readme.md#document-convention).\n * [TocLevel: Heading level](/docs/toc.md#heading-level).\n * [TocExcludes: Ignore headings](/docs/toc.md#ignore-headings).\n * [Exclude: Exclude directories from discovery](/docs/exclusion.md).\n * [WriteHeader: Disable Header](/docs/header.md#disable-header).\n * [UrlPrefix](/readme.md#urlprefix).\n * [UrlsAsSnippets: Urls to files to be included as snippets](/readme.md#urlsassnippets).\n * TreatMissingAsWarning: The default behavior for a missing snippet/include is to log an error (or throw an exception). To change that behavior to a warning set TreatMissingAsWarning to true.\n"
  },
  {
    "path": "docs/exclusion.md",
    "content": "<!--\nGENERATED FILE - DO NOT EDIT\nThis file was generated by [MarkdownSnippets](https://github.com/SimonCropp/MarkdownSnippets).\nSource File: /docs/mdsource/exclusion.source.md\nTo change this file edit the source file and then run MarkdownSnippets.\n-->\n\n# Exclusions\n\n\n## Exclude directories from snippet and markdown discovery\n\nTo exclude directories use `-e` or `--exclude-directories`.\n\nFor example the following will exclude any directory containing 'foo' or 'bar'\n\n```ps\nmdsnippets -e foo:bar\n```\n\n\n## Exclude snippets from directories\n\nTo exclude directories from snippet discovery use `--exclude-snippet-directories`.\n\nFor example the following will exclude any directory containing 'foo' or 'bar'\n\n```ps\nmdsnippets --exclude-snippet-directories foo:bar\n```\n\n## Exclude markdown from directories\n\nTo exclude directories from markdown discovery use `--exclude-markdown-directories`.\n\nFor example the following will exclude any directory containing 'foo' or 'bar'\n\n```ps\nmdsnippets --exclude-markdown-directories foo:bar\n```\n\n\n## Exclude files from snippet discovery\n\nTo exclude specific files from snippet discovery, add an `ExcludeSnippetFiles` array to [`mdsnippets.json`](/docs/config-file.md). Each entry is a glob pattern matched (case-insensitively) against the file *name* — not the full path. Patterns support `*` (any sequence of characters) and `?` (a single character).\n\nFor example, the following excludes all Verify snapshot files so that `<!-- begin-snippet -->` markers hand-added to a `*.verified.txt` are not picked up as snippet sources:\n\n```json\n{\n  \"ExcludeSnippetFiles\": [\n    \"*.verified.txt\",\n    \"*.received.txt\"\n  ]\n}\n```\n\nMatched files are still visible to the include/all-files enumeration — only snippet scanning skips them.\n\n\n## Ignored paths\n\n### Directory exclusion rules:\n\n<!-- snippet: DefaultDirectoryExclusions.cs -->\n<a id='snippet-DefaultDirectoryExclusions.cs'></a>\n```cs\nnamespace MarkdownSnippets;\n\npublic static class DefaultDirectoryExclusions\n{\n    public static bool ShouldExcludeDirectory(string path)\n    {\n        var suffix = Path\n            .GetFileName(path)\n            .ToLowerInvariant();\n        if (suffix is\n            // source control\n            \".git\" or\n\n            // ide temp files\n            \".vs\" or\n            \".vscode\" or\n            \".idea\" or\n\n            // package cache\n            \"packages\" or\n            \"node_modules\" or\n\n            // build output\n            \"dist\" or\n            \".angular\" or\n            \"bin\" or\n            \"obj\")\n        {\n            return true;\n        }\n\n        var directory = new DirectoryInfo(path);\n        return directory.Attributes.HasFlag(FileAttributes.Hidden);\n    }\n}\n```\n<sup><a href='/src/MarkdownSnippets/Reading/Exclusions/DefaultDirectoryExclusions.cs#L1-L35' title='Snippet source file'>snippet source</a> | <a href='#snippet-DefaultDirectoryExclusions.cs' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\n\n### File exclusion rules\n\nAll binary files as defined by https://github.com/sindresorhus/binary-extensions/:\n\n<!-- snippet: BinaryFileExtensions -->\n<a id='snippet-BinaryFileExtensions'></a>\n```cs\n\"user\",\n// extra binary\n\"mdb\",\n\"binlog\",\n\"shp\",\n\"dbf\",\n\"shx\",\n\"pbf\",\n\"map\",\n\"sbn\",\n\n//from https://github.com/sindresorhus/binary-extensions/blob/master/binary-extensions.json\n\"3dm\",\n\"3ds\",\n\"3g2\",\n\"3gp\",\n\"7z\",\n\"a\",\n\"aac\",\n\"adp\",\n\"ai\",\n\"aif\",\n\"aiff\",\n\"alz\",\n\"ape\",\n\"apk\",\n\"appimage\",\n\"ar\",\n\"arj\",\n\"asf\",\n\"au\",\n\"avi\",\n\"bak\",\n\"baml\",\n\"bh\",\n\"bin\",\n\"bk\",\n\"bmp\",\n\"btif\",\n\"bz2\",\n\"bzip2\",\n\"cab\",\n\"caf\",\n\"cgm\",\n\"class\",\n\"cmx\",\n\"cpio\",\n\"cr2\",\n\"cur\",\n\"dat\",\n\"dcm\",\n\"deb\",\n\"dex\",\n\"djvu\",\n\"dll\",\n\"dmg\",\n\"dng\",\n\"doc\",\n\"docm\",\n\"docx\",\n\"dot\",\n\"dotm\",\n\"dra\",\n\"DS_Store\",\n\"dsk\",\n\"dts\",\n\"dtshd\",\n\"dvb\",\n\"dwg\",\n\"dxf\",\n\"ecelp4800\",\n\"ecelp7470\",\n\"ecelp9600\",\n\"egg\",\n\"eol\",\n\"eot\",\n\"epub\",\n\"exe\",\n\"f4v\",\n\"fbs\",\n\"fh\",\n\"fla\",\n\"flac\",\n\"flatpak\",\n\"fli\",\n\"flv\",\n\"fpx\",\n\"fst\",\n\"fvt\",\n\"g3\",\n\"gh\",\n\"gif\",\n\"graffle\",\n\"gz\",\n\"gzip\",\n\"h261\",\n\"h263\",\n\"h264\",\n\"icns\",\n\"ico\",\n\"ief\",\n\"img\",\n\"ipa\",\n\"iso\",\n\"jar\",\n\"jpeg\",\n\"jpg\",\n\"jpgv\",\n\"jpm\",\n\"jxr\",\n\"key\",\n\"ktx\",\n\"lha\",\n\"lib\",\n\"lvp\",\n\"lz\",\n\"lzh\",\n\"lzma\",\n\"lzo\",\n\"m3u\",\n\"m4a\",\n\"m4v\",\n\"mar\",\n\"mdi\",\n\"mht\",\n\"mid\",\n\"midi\",\n\"mj2\",\n\"mka\",\n\"mkv\",\n\"mmr\",\n\"mng\",\n\"mobi\",\n\"mov\",\n\"movie\",\n\"mp3\",\n\"mp4\",\n\"mp4a\",\n\"mpeg\",\n\"mpg\",\n\"mpga\",\n\"mxu\",\n\"nef\",\n\"npx\",\n\"numbers\",\n\"nupkg\",\n\"o\",\n\"oga\",\n\"ogg\",\n\"ogv\",\n\"otf\",\n\"pages\",\n\"pbm\",\n\"pcx\",\n\"pdb\",\n\"pdf\",\n\"pea\",\n\"pgm\",\n\"pic\",\n\"png\",\n\"pnm\",\n\"pot\",\n\"potm\",\n\"potx\",\n\"ppa\",\n\"ppam\",\n\"ppm\",\n\"pps\",\n\"ppsm\",\n\"ppsx\",\n\"ppt\",\n\"pptm\",\n\"pptx\",\n\"psd\",\n\"pya\",\n\"pyc\",\n\"pyo\",\n\"pyv\",\n\"qt\",\n\"rar\",\n\"ras\",\n\"raw\",\n\"resources\",\n\"rgb\",\n\"rip\",\n\"rlc\",\n\"rmf\",\n\"rmvb\",\n\"rpm\",\n\"rtf\",\n\"rz\",\n\"s3m\",\n\"s7z\",\n\"scpt\",\n\"sgi\",\n\"shar\",\n\"snap\",\n\"sil\",\n\"sketch\",\n\"slk\",\n\"smv\",\n\"snk\",\n\"so\",\n\"stl\",\n\"suo\",\n\"sub\",\n\"swf\",\n\"tar\",\n\"tbz\",\n\"tbz2\",\n\"tga\",\n\"tgz\",\n\"thmx\",\n\"tif\",\n\"tiff\",\n\"tlz\",\n\"ttc\",\n\"ttf\",\n\"txz\",\n\"udf\",\n\"uvh\",\n\"uvi\",\n\"uvm\",\n\"uvp\",\n\"uvs\",\n\"uvu\",\n\"viv\",\n\"vob\",\n\"war\",\n\"wav\",\n\"wax\",\n\"wbmp\",\n\"wdp\",\n\"weba\",\n\"webm\",\n\"webp\",\n\"whl\",\n\"wim\",\n\"wm\",\n\"wma\",\n\"wmv\",\n\"wmx\",\n\"woff\",\n\"woff2\",\n\"wrm\",\n\"wvx\",\n\"xbm\",\n\"xif\",\n\"xla\",\n\"xlam\",\n\"xls\",\n\"xlsb\",\n\"xlsm\",\n\"xlsx\",\n\"xlt\",\n\"xltm\",\n\"xltx\",\n\"xm\",\n\"xmind\",\n\"xpi\",\n\"xpm\",\n\"xwd\",\n\"xz\",\n\"z\",\n\"zip\",\n\"zipx\"\n```\n<sup><a href='/src/MarkdownSnippets/Reading/Exclusions/SnippetFileExclusions.cs#L54-L323' title='Snippet source file'>snippet source</a> | <a href='#snippet-BinaryFileExtensions' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\n\n### No comment files\n\nFiles that cannot contain comments are excluded.\n\n<!-- snippet: NoAcceptCommentsExtensions -->\n<a id='snippet-NoAcceptCommentsExtensions'></a>\n```cs\n\"DotSettings\",\n\"csv\",\n\"json\",\n\"geojson\",\n\"sln\"\n```\n<sup><a href='/src/MarkdownSnippets/Reading/Exclusions/SnippetFileExclusions.cs#L17-L25' title='Snippet source file'>snippet source</a> | <a href='#snippet-NoAcceptCommentsExtensions' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n"
  },
  {
    "path": "docs/github-action.md",
    "content": "<!--\nGENERATED FILE - DO NOT EDIT\nThis file was generated by [MarkdownSnippets](https://github.com/SimonCropp/MarkdownSnippets).\nSource File: /docs/mdsource/github-action.source.md\nTo change this file edit the source file and then run MarkdownSnippets.\n-->\n\n# GitHub Actions\n\nMarkdown snippets can be run inside a [GitHub Action](https://help.github.com/en/actions) by installing and using [MarkdownSnippets.Tool](/readme.md#installation). This can be useful to ensure md docs are in sync when .source files are edited online, and without needing to re-generate the docs locally.\n\nAdd the following to `.github\\workflows\\on-push-do-doco.yml` in the target repository.\n\n<!-- snippet: on-push-do-docs.yml -->\n<a id='snippet-on-push-do-docs.yml'></a>\n```yml\nname: on-push-do-docs\non:\n  push:\njobs:\n  docs:\n    runs-on: windows-latest\n    steps:\n    - uses: actions/checkout@v4\n    - name: Run MarkdownSnippets\n      run: |\n        dotnet tool install --global MarkdownSnippets.Tool\n        mdsnippets ${GITHUB_WORKSPACE}\n      shell: bash\n    - name: Push changes\n      run: |\n        git config --local user.email \"action@github.com\"\n        git config --local user.name \"GitHub Action\"\n        git commit -m \"Docs changes\" -a || echo \"nothing to commit\"\n        remote=\"https://${GITHUB_ACTOR}:${{secrets.GITHUB_TOKEN}}@github.com/${GITHUB_REPOSITORY}.git\"\n        branch=\"${GITHUB_REF:11}\"\n        git push \"${remote}\" ${branch} || echo \"nothing to push\"\n      shell: bash\n```\n<sup><a href='/docs/on-push-do-docs.yml#L1-L22' title='Snippet source file'>snippet source</a> | <a href='#snippet-on-push-do-docs.yml' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\nThis action performs the following tasks:\n\n * Use the [Checkout Action](https://github.com/marketplace/actions/checkout) to pull down the source\n * Install the MarkdownSnippets dotnet tool\n * Run MarkdownSnippets against the current directory\n * Push any changes back to GitHub\n\n\n## More Info\n\n * [Software installed on GitHub-hosted runners](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/software-installed-on-github-hosted-runners)\n"
  },
  {
    "path": "docs/header.md",
    "content": "<!--\nGENERATED FILE - DO NOT EDIT\nThis file was generated by [MarkdownSnippets](https://github.com/SimonCropp/MarkdownSnippets).\nSource File: /docs/mdsource/header.source.md\nTo change this file edit the source file and then run MarkdownSnippets.\n-->\n\n# Header\n\nWhen a .md file is written, a header is include. The default header is:\n\n<!-- snippet: HeaderWriterTests.DefaultHeader.verified.txt -->\n<a id='snippet-HeaderWriterTests.DefaultHeader.verified.txt'></a>\n```txt\nGENERATED FILE - DO NOT EDIT\nThis file was generated by [MarkdownSnippets](https://github.com/SimonCropp/MarkdownSnippets).\nSource File: {relativePath}\nTo change this file edit the source file and then run MarkdownSnippets.\n```\n<sup><a href='/src/Tests/HeaderWriterTests.DefaultHeader.verified.txt#L1-L4' title='Snippet source file'>snippet source</a> | <a href='#snippet-HeaderWriterTests.DefaultHeader.verified.txt' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\n\n## Disable Header\n\nTo disable the header use `--write-header`\n\n```ps\nmdsnippets --write-header false\n```\n\n\n## Custom Header\n\nTo apply a custom header use `--header`. `{relativePath}` will be replaced with the relative path of the `.source.md` file.\n\n```ps\nmdsnippets --header \"GENERATED FILE - Source File: {relativePath}\"\n```\n\n\n## Newlines in Header\n\nTo insert a newline use `\\n`\n\n```ps\nmdsnippets --header \"GENERATED FILE\\nSource File: {relativePath}\"\n```\n"
  },
  {
    "path": "docs/includes.md",
    "content": "<!--\nGENERATED FILE - DO NOT EDIT\nThis file was generated by [MarkdownSnippets](https://github.com/SimonCropp/MarkdownSnippets).\nSource File: /docs/mdsource/includes.source.md\nTo change this file edit the source file and then run MarkdownSnippets.\n-->\n\n# Includes\n\n\n## Including full code files\n\nWhen snippets are read all source files are stored in a list. When searching for a snippet with a specified key, and that key is not found, the list of files are used as a secondary lookup. The lookup is done by finding all files that have a suffix matching the key. This results in the ability to include full files as snippets using the following syntax:\n\n<pre>\nsnippet&#58; directory/FileToInclude.txt\n</pre>\n\nThe path syntax uses forward slashes `/`.\n\n\n## Including urls\n\n\n<pre>\nsnippet&#58; http://myurl\n</pre>\n\n## Including a snippet from an external URL\n\nTo include a specific named snippet from a file using an external URL, use the `web-snippet:` keyword followed by the URL and the snippet key separated by a `#`:\n\n<pre>\nweb-snippet&#58;https://raw.githubusercontent.com/owner/repo/branch/path/to/file.cs#snippetKey\n</pre>\n\nThis will fetch the file from the URL, extract the snippet with the given key, and embed it in your Markdown.\n\n\n## Markdown includes\n\nMarkdown includes are pulled into the document before passing the content through the snippet insertion.\n\n\n### Defining an include\n\nAdd a file anywhere in the target directory that is suffixed with `.include.md`. For example, the file might be named `theKey.include.md`.\n\n\n### Using an include\n\nAdd the following to the markdown:\n\n   ```\n   include: theKey\n   ```\n"
  },
  {
    "path": "docs/indentation.md",
    "content": "<!--\nGENERATED FILE - DO NOT EDIT\nThis file was generated by [MarkdownSnippets](https://github.com/SimonCropp/MarkdownSnippets).\nSource File: /docs/mdsource/indentation.source.md\nTo change this file edit the source file and then run MarkdownSnippets.\n-->\n\n# Code indentation\n\nThe code snippets will do smart trimming of snippet indentation.\n\nFor example given this snippet:\n\n<pre>\n&#8226;&#8226;// begin-snippet MySnippetName\n&#8226;&#8226;Line one of the snippet\n&#8226;&#8226;&#8226;&#8226;Line two of the snippet\n&#8226;&#8226;// end-snippet\n</pre>\n\nThe leading two spaces (&#8226;&#8226;) will be trimmed and the result will be:\n\n```\nLine one of the snippet\n••Line two of the snippet\n```\n\nThe same behavior will apply to leading tabs.\n\n\n## Do not mix tabs and spaces\n\nIf tabs and spaces are mixed there is no way for the snippets to work out what to trim.\n\nSo given this snippet:\n\n<pre>\n&#8226;&#8226;// begin-snippet MySnippetNamea\n&#8226;&#8226;Line one of the snippet\n&#10137;&#10137;Line one of the snippet\n&#8226;&#8226;// end-snippet\n</pre>\n\nWhere &#10137; is a tab.\n\nThe resulting markdown will be will be\n\n<pre>\nLine one of the snippet\n&#10137;&#10137;Line one of the snippet\n</pre>\n\nNote that none of the tabs have been trimmed.\n"
  },
  {
    "path": "docs/max-width.md",
    "content": "<!--\nGENERATED FILE - DO NOT EDIT\nThis file was generated by [MarkdownSnippets](https://github.com/SimonCropp/MarkdownSnippets).\nSource File: /docs/mdsource/max-width.source.md\nTo change this file edit the source file and then run MarkdownSnippets.\n-->\n\n# Max Width\n\nThe Max Width setting is used to control the maximum characters per line of a snippet. If any snippet has a line that exceeds the maximum an error will be thrown.\n\n\n## Usage\n\n\n### Config File\n\n```\n{\n  \"MaxWidth\": 80\n}\n```\n\n\n### Command Line\n\n```\n--max-width 80\n```\n\n\n### Code Api\n\n<!-- snippet: DirectoryMarkdownProcessorRunMaxWidth -->\n<a id='snippet-DirectoryMarkdownProcessorRunMaxWidth'></a>\n```cs\nvar processor = new DirectoryMarkdownProcessor(\n    \"targetDirectory\",\n    maxWidth: 80,\n    directoryIncludes: _ => true,\n    markdownDirectoryIncludes: _ => true,\n    snippetDirectoryIncludes: _ => true);\nprocessor.Run();\n```\n<sup><a href='/src/Tests/Snippets/Usage.cs#L33-L43' title='Snippet source file'>snippet source</a> | <a href='#snippet-DirectoryMarkdownProcessorRunMaxWidth' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\n\n## References\n\nhttps://en.wikipedia.org/wiki/Line_length#Electronic_text\n\n> Legibility research specific to digital text has shown that, like with printed text, line length can affect reading speed. If lines are too long it is difficult for the reader to quickly return to the start of the next line (saccade) whereas if lines are too short more scrolling or paging will be required. Researchers have suggested that longer lines are better for quick scanning, while shorter lines are better for accuracy. Longer lines would then be better suited for cases when the information will likely be scanned, while shorter lines would be appropriate when the information is meant to be read thoroughly. One proposal advanced that, in order for on-screen text to have the best compromise between reading speed and comprehension, about 55 cpl should be used. On the other hand, there have been studies indicating that digital text at 100 cpl can be read faster than text with lines of 25 characters, while retaining the same level of comprehension.\n"
  },
  {
    "path": "docs/mdsource/api.source.md",
    "content": "# Library Usage\n\n\n## NuGet package\n\nhttps://nuget.org/packages/MarkdownSnippets/ [![NuGet Status](https://img.shields.io/nuget/v/MarkdownSnippets.svg)](https://www.nuget.org/packages/MarkdownSnippets/)\n\n\n## Reading snippets from files\n\nsnippet: ReadingFilesSimple\n\n\n## Ignored paths\n\nTo change conventions manipulate lists `MarkdownSnippets.Exclusions.NoAcceptCommentsExtensions` and `MarkdownSnippets.Exclusions.BinaryFileExtensions`."
  },
  {
    "path": "docs/mdsource/config-file.source.md",
    "content": "# Config File\n\nThe [dotnet tool](/readme.md#installation) and the [MSBuild Task](msbuild.md) support a config file convention.\n\nAdd a file named `mdsnippets.json` at the target directory with the following content:\n\n\n## For [InPlaceOverwrite](https://github.com/SimonCropp/MarkdownSnippets#inplaceoverwrite)\n\nsnippet: InPlaceOverwrite.json\n\n\n## For [SourceTransform](https://github.com/SimonCropp/MarkdownSnippets#sourcetransform)\n\nsnippet: SourceTransform.json\n\n\n## All Settings\n\nsnippet: allConfig.json\n\n\n## JSON Schema\n\nEditor help is available by adding the `$schema` field to the `mdsnippets.json` file.\n\n```json\n{\n  \"$schema\": \"https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/schema.json\"\n}\n```\n\nIn the screenshot, [JetBrains Rider](https://jetbrains.com/rider), is able to offer code completion support.\n\n![IDE schema code completion](/docs/code-completion.png)\n\nThe schema also includes `enum` values for constrained value types.\n\n![IDE schema code completion](/docs/code-completion-values.png)\n\n\n## More Info\n\n * [ReadOnly: Mark resulting files as read only](/readme.md#mark-resulting-files-as-read-only)\n * [LinkFormat](/readme.md#linkformat).\n * [Convention](/readme.md#document-convention).\n * [TocLevel: Heading level](/docs/toc.md#heading-level).\n * [TocExcludes: Ignore headings](/docs/toc.md#ignore-headings).\n * [Exclude: Exclude directories from discovery](/docs/exclusion.md).\n * [WriteHeader: Disable Header](/docs/header.md#disable-header).\n * [UrlPrefix](/readme.md#urlprefix).\n * [UrlsAsSnippets: Urls to files to be included as snippets](/readme.md#urlsassnippets).\n * TreatMissingAsWarning: The default behavior for a missing snippet/include is to log an error (or throw an exception). To change that behavior to a warning set TreatMissingAsWarning to true."
  },
  {
    "path": "docs/mdsource/doc-index.include.md",
    "content": "  * Developer Information\n    * [.net API](/docs/api.md)\n    * [MsBuild Task](/docs/msbuild.md)\n    * [Github Action](/docs/github-action.md)\n  * Customisation\n    * [Config file convention](/docs/config-file.md)\n    * [Max Width](/docs/max-width.md)\n    * [Includes](/docs/includes.md)\n    * [Directory Exclusion](/docs/exclusion.md)\n    * [Header](/docs/header.md)\n  * Writing Documentation\n    * [Indentation](/docs/indentation.md)\n    * [Table of contents](/docs/toc.md)"
  },
  {
    "path": "docs/mdsource/exclusion.source.md",
    "content": "# Exclusions\n\n\n## Exclude directories from snippet and markdown discovery\n\nTo exclude directories use `-e` or `--exclude-directories`.\n\nFor example the following will exclude any directory containing 'foo' or 'bar'\n\n```ps\nmdsnippets -e foo:bar\n```\n\n\n## Exclude snippets from directories\n\nTo exclude directories from snippet discovery use `--exclude-snippet-directories`.\n\nFor example the following will exclude any directory containing 'foo' or 'bar'\n\n```ps\nmdsnippets --exclude-snippet-directories foo:bar\n```\n\n## Exclude markdown from directories\n\nTo exclude directories from markdown discovery use `--exclude-markdown-directories`.\n\nFor example the following will exclude any directory containing 'foo' or 'bar'\n\n```ps\nmdsnippets --exclude-markdown-directories foo:bar\n```\n\n\n## Exclude files from snippet discovery\n\nTo exclude specific files from snippet discovery, add an `ExcludeSnippetFiles` array to [`mdsnippets.json`](/docs/config-file.md). Each entry is a glob pattern matched (case-insensitively) against the file *name* — not the full path. Patterns support `*` (any sequence of characters) and `?` (a single character).\n\nFor example, the following excludes all Verify snapshot files so that `<!-- begin-snippet -->` markers hand-added to a `*.verified.txt` are not picked up as snippet sources:\n\n```json\n{\n  \"ExcludeSnippetFiles\": [\n    \"*.verified.txt\",\n    \"*.received.txt\"\n  ]\n}\n```\n\nMatched files are still visible to the include/all-files enumeration — only snippet scanning skips them.\n\n\n## Ignored paths\n\n### Directory exclusion rules:\n\nsnippet: DefaultDirectoryExclusions.cs\n\n\n### File exclusion rules\n\nAll binary files as defined by https://github.com/sindresorhus/binary-extensions/:\n\nsnippet: BinaryFileExtensions\n\n\n### No comment files\n\nFiles that cannot contain comments are excluded.\n\nsnippet: NoAcceptCommentsExtensions"
  },
  {
    "path": "docs/mdsource/github-action.source.md",
    "content": "# GitHub Actions\n\nMarkdown snippets can be run inside a [GitHub Action](https://help.github.com/en/actions) by installing and using [MarkdownSnippets.Tool](/readme.md#installation). This can be useful to ensure md docs are in sync when .source files are edited online, and without needing to re-generate the docs locally.\n\nAdd the following to `.github\\workflows\\on-push-do-doco.yml` in the target repository.\n\nsnippet: on-push-do-docs.yml\n\nThis action performs the following tasks:\n\n * Use the [Checkout Action](https://github.com/marketplace/actions/checkout) to pull down the source\n * Install the MarkdownSnippets dotnet tool\n * Run MarkdownSnippets against the current directory\n * Push any changes back to GitHub\n\n\n## More Info\n\n * [Software installed on GitHub-hosted runners](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/software-installed-on-github-hosted-runners)"
  },
  {
    "path": "docs/mdsource/header.source.md",
    "content": "# Header\n\nWhen a .md file is written, a header is include. The default header is:\n\nsnippet: HeaderWriterTests.DefaultHeader.verified.txt\n\n\n## Disable Header\n\nTo disable the header use `--write-header`\n\n```ps\nmdsnippets --write-header false\n```\n\n\n## Custom Header\n\nTo apply a custom header use `--header`. `{relativePath}` will be replaced with the relative path of the `.source.md` file.\n\n```ps\nmdsnippets --header \"GENERATED FILE - Source File: {relativePath}\"\n```\n\n\n## Newlines in Header\n\nTo insert a newline use `\\n`\n\n```ps\nmdsnippets --header \"GENERATED FILE\\nSource File: {relativePath}\"\n```"
  },
  {
    "path": "docs/mdsource/includes.source.md",
    "content": "# Includes\n\n\n## Including full code files\n\nWhen snippets are read all source files are stored in a list. When searching for a snippet with a specified key, and that key is not found, the list of files are used as a secondary lookup. The lookup is done by finding all files that have a suffix matching the key. This results in the ability to include full files as snippets using the following syntax:\n\n<pre>\nsnippet&#58; directory/FileToInclude.txt\n</pre>\n\nThe path syntax uses forward slashes `/`.\n\n\n## Including urls\n\n\n<pre>\nsnippet&#58; http://myurl\n</pre>\n\n## Including a snippet from an external URL\n\nTo include a specific named snippet from a file using an external URL, use the `web-snippet:` keyword followed by the URL and the snippet key separated by a `#`:\n\n<pre>\nweb-snippet&#58;https://raw.githubusercontent.com/owner/repo/branch/path/to/file.cs#snippetKey\n</pre>\n\nThis will fetch the file from the URL, extract the snippet with the given key, and embed it in your Markdown.\n\n\n## Markdown includes\n\nMarkdown includes are pulled into the document before passing the content through the snippet insertion.\n\n\n### Defining an include\n\nAdd a file anywhere in the target directory that is suffixed with `.include.md`. For example, the file might be named `theKey.include.md`.\n\n\n### Using an include\n\nAdd the following to the markdown:\n\n   ```\n   include: theKey\n   ```"
  },
  {
    "path": "docs/mdsource/indentation.source.md",
    "content": "# Code indentation\n\nThe code snippets will do smart trimming of snippet indentation.\n\nFor example given this snippet:\n\n<pre>\n&#8226;&#8226;// begin-snippet MySnippetName\n&#8226;&#8226;Line one of the snippet\n&#8226;&#8226;&#8226;&#8226;Line two of the snippet\n&#8226;&#8226;// end-snippet\n</pre>\n\nThe leading two spaces (&#8226;&#8226;) will be trimmed and the result will be:\n\n```\nLine one of the snippet\n••Line two of the snippet\n```\n\nThe same behavior will apply to leading tabs.\n\n\n## Do not mix tabs and spaces\n\nIf tabs and spaces are mixed there is no way for the snippets to work out what to trim.\n\nSo given this snippet:\n\n<pre>\n&#8226;&#8226;// begin-snippet MySnippetNamea\n&#8226;&#8226;Line one of the snippet\n&#10137;&#10137;Line one of the snippet\n&#8226;&#8226;// end-snippet\n</pre>\n\nWhere &#10137; is a tab.\n\nThe resulting markdown will be will be\n\n<pre>\nLine one of the snippet\n&#10137;&#10137;Line one of the snippet\n</pre>\n\nNote that none of the tabs have been trimmed."
  },
  {
    "path": "docs/mdsource/max-width.source.md",
    "content": "# Max Width\n\nThe Max Width setting is used to control the maximum characters per line of a snippet. If any snippet has a line that exceeds the maximum an error will be thrown.\n\n\n## Usage\n\n\n### Config File\n\n```\n{\n  \"MaxWidth\": 80\n}\n```\n\n\n### Command Line\n\n```\n--max-width 80\n```\n\n\n### Code Api\n\nsnippet: DirectoryMarkdownProcessorRunMaxWidth\n\n\n## References\n\nhttps://en.wikipedia.org/wiki/Line_length#Electronic_text\n\n> Legibility research specific to digital text has shown that, like with printed text, line length can affect reading speed. If lines are too long it is difficult for the reader to quickly return to the start of the next line (saccade) whereas if lines are too short more scrolling or paging will be required. Researchers have suggested that longer lines are better for quick scanning, while shorter lines are better for accuracy. Longer lines would then be better suited for cases when the information will likely be scanned, while shorter lines would be appropriate when the information is meant to be read thoroughly. One proposal advanced that, in order for on-screen text to have the best compromise between reading speed and comprehension, about 55 cpl should be used. On the other hand, there have been studies indicating that digital text at 100 cpl can be read faster than text with lines of 25 characters, while retaining the same level of comprehension."
  },
  {
    "path": "docs/mdsource/msbuild.source.md",
    "content": "# MsBuild\n\nAn [MsBuild task](https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild-task) for merging snippets into markdown documents.\n\nMsBuild has a [convention](https://docs.microsoft.com/en-us/nuget/create-packages/creating-a-package#from-a-convention-based-working-directory) to automatically run build tasks from included NuGet packages. This package takes advantage of that hook to run markdownsnippets on build.\n\nThis package only need to be included in one project of the solution. A logical choice is the test project.\n\n\n## NuGet package\n\nhttps://nuget.org/packages/MarkdownSnippets.MsBuild/ [![NuGet Status](https://img.shields.io/nuget/v/MarkdownSnippets.MsBuild.svg)](https://www.nuget.org/packages/MarkdownSnippets.MsBuild/)\n\n\n## More Info\n\n * [Config file convention](/docs/config-file.md)."
  },
  {
    "path": "docs/mdsource/readme.source.md",
    "content": "# Documentation\n\ninclude: doc-index"
  },
  {
    "path": "docs/mdsource/toc/tocAfter.txt",
    "content": "# Title\n\n<!-- toc -->\n## Contents\n\n * [Heading 1](#heading-1)\n * [Heading 2](#heading-2)\n<!-- endToc -->\n\n## Heading 1\n\nText1\n\n## Heading 2\n\nText2"
  },
  {
    "path": "docs/mdsource/toc/tocBefore.txt",
    "content": "# Title\n\ntoc\n\n## Heading 1\n\nText1\n\n## Heading 1\n\nText2"
  },
  {
    "path": "docs/mdsource/toc.source.md",
    "content": "# Table of contents\n\nIf a line is `toc` it will be replaced with a table of contents\n\nSo if a markdown document contains the following:\n\nsnippet: tocBefore.txt\n\nThe result will be rendered:\n\nsnippet: tocAfter.txt\n\n\n## Heading Level\n\nHeadings with level 2 (`##`) or greater can be rendered. By default all level 2 and level 3 headings are included.\n\nTo include more levels use the `--toc-level` argument. So for example to include headings levels 2 though level 6 use:\n\n```ps\nmdsnippets --toc-level 5\n```\n\n\n## Ignore Headings\n\nTo exclude headings use the `--toc-excludes` argument. So for example to exclude `heading1` and `heading2` use:\n\n```ps\nmdsnippets --toc-excludes heading1:heading2\n```"
  },
  {
    "path": "docs/msbuild.md",
    "content": "<!--\nGENERATED FILE - DO NOT EDIT\nThis file was generated by [MarkdownSnippets](https://github.com/SimonCropp/MarkdownSnippets).\nSource File: /docs/mdsource/msbuild.source.md\nTo change this file edit the source file and then run MarkdownSnippets.\n-->\n\n# MsBuild\n\nAn [MsBuild task](https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild-task) for merging snippets into markdown documents.\n\nMsBuild has a [convention](https://docs.microsoft.com/en-us/nuget/create-packages/creating-a-package#from-a-convention-based-working-directory) to automatically run build tasks from included NuGet packages. This package takes advantage of that hook to run markdownsnippets on build.\n\nThis package only need to be included in one project of the solution. A logical choice is the test project.\n\n\n## NuGet package\n\nhttps://nuget.org/packages/MarkdownSnippets.MsBuild/ [![NuGet Status](https://img.shields.io/nuget/v/MarkdownSnippets.MsBuild.svg)](https://www.nuget.org/packages/MarkdownSnippets.MsBuild/)\n\n\n## More Info\n\n * [Config file convention](/docs/config-file.md).\n"
  },
  {
    "path": "docs/on-push-do-docs.yml",
    "content": "name: on-push-do-docs\non:\n  push:\njobs:\n  docs:\n    runs-on: windows-latest\n    steps:\n    - uses: actions/checkout@v4\n    - name: Run MarkdownSnippets\n      run: |\n        dotnet tool install --global MarkdownSnippets.Tool\n        mdsnippets ${GITHUB_WORKSPACE}\n      shell: bash\n    - name: Push changes\n      run: |\n        git config --local user.email \"action@github.com\"\n        git config --local user.name \"GitHub Action\"\n        git commit -m \"Docs changes\" -a || echo \"nothing to commit\"\n        remote=\"https://${GITHUB_ACTOR}:${{secrets.GITHUB_TOKEN}}@github.com/${GITHUB_REPOSITORY}.git\"\n        branch=\"${GITHUB_REF:11}\"\n        git push \"${remote}\" ${branch} || echo \"nothing to push\"\n      shell: bash"
  },
  {
    "path": "docs/readme.md",
    "content": "<!--\nGENERATED FILE - DO NOT EDIT\nThis file was generated by [MarkdownSnippets](https://github.com/SimonCropp/MarkdownSnippets).\nSource File: /docs/mdsource/readme.source.md\nTo change this file edit the source file and then run MarkdownSnippets.\n-->\n\n# Documentation\n\n  * Developer Information<!-- include: doc-index. path: /docs/mdsource/doc-index.include.md -->\n    * [.net API](/docs/api.md)\n    * [MsBuild Task](/docs/msbuild.md)\n    * [Github Action](/docs/github-action.md)\n  * Customisation\n    * [Config file convention](/docs/config-file.md)\n    * [Max Width](/docs/max-width.md)\n    * [Includes](/docs/includes.md)\n    * [Directory Exclusion](/docs/exclusion.md)\n    * [Header](/docs/header.md)\n  * Writing Documentation\n    * [Indentation](/docs/indentation.md)\n    * [Table of contents](/docs/toc.md)<!-- endInclude -->\n"
  },
  {
    "path": "docs/toc.md",
    "content": "<!--\nGENERATED FILE - DO NOT EDIT\nThis file was generated by [MarkdownSnippets](https://github.com/SimonCropp/MarkdownSnippets).\nSource File: /docs/mdsource/toc.source.md\nTo change this file edit the source file and then run MarkdownSnippets.\n-->\n\n# Table of contents\n\nIf a line is `toc` it will be replaced with a table of contents\n\nSo if a markdown document contains the following:\n\n<!-- snippet: tocBefore.txt -->\n<a id='snippet-tocBefore.txt'></a>\n```txt\n# Title\n\ntoc\n\n## Heading 1\n\nText1\n\n## Heading 1\n\nText2\n```\n<sup><a href='/docs/mdsource/toc/tocBefore.txt#L1-L11' title='Snippet source file'>snippet source</a> | <a href='#snippet-tocBefore.txt' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\nThe result will be rendered:\n\n<!-- snippet: tocAfter.txt -->\n<a id='snippet-tocAfter.txt'></a>\n```txt\n# Title\n\n<!-- toc -->\n## Contents\n\n * [Heading 1](#heading-1)\n * [Heading 2](#heading-2)\n<!-- endToc -->\n\n## Heading 1\n\nText1\n\n## Heading 2\n\nText2\n```\n<sup><a href='/docs/mdsource/toc/tocAfter.txt#L1-L16' title='Snippet source file'>snippet source</a> | <a href='#snippet-tocAfter.txt' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\n\n## Heading Level\n\nHeadings with level 2 (`##`) or greater can be rendered. By default all level 2 and level 3 headings are included.\n\nTo include more levels use the `--toc-level` argument. So for example to include headings levels 2 though level 6 use:\n\n```ps\nmdsnippets --toc-level 5\n```\n\n\n## Ignore Headings\n\nTo exclude headings use the `--toc-excludes` argument. So for example to exclude `heading1` and `heading2` use:\n\n```ps\nmdsnippets --toc-excludes heading1:heading2\n```\n"
  },
  {
    "path": "license.txt",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2013 Simon Cropp\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "readme.md",
    "content": "<!--\nGENERATED FILE - DO NOT EDIT\nThis file was generated by [MarkdownSnippets](https://github.com/SimonCropp/MarkdownSnippets).\nSource File: /readme.source.md\nTo change this file edit the source file and then run MarkdownSnippets.\n-->\n\n# <img src=\"/src/icon.png\" height=\"30px\"> MarkdownSnippets\n\n[![Build status](https://img.shields.io/appveyor/build/SimonCropp/MarkdownSnippets)](https://ci.appveyor.com/project/SimonCropp/MarkdownSnippets)\n[![NuGet Status](https://img.shields.io/nuget/v/MarkdownSnippets.Tool.svg?label=dotnet%20tool)](https://www.nuget.org/packages/MarkdownSnippets.Tool/)\n[![NuGet Status](https://img.shields.io/nuget/v/MarkdownSnippets.MsBuild.svg?label=MsBuild%20Task)](https://www.nuget.org/packages/MarkdownSnippets.MsBuild/)\n[![NuGet Status](https://img.shields.io/nuget/v/MarkdownSnippets.svg?label=.net%20API)](https://www.nuget.org/packages/MarkdownSnippets/)\n\nA [dotnet tool](https://docs.microsoft.com/en-us/dotnet/core/tools/global-tools) or [MsBuild Task](/docs/msbuild.md) that extract snippets from code files and merges them into markdown documents.\n\n**See [Milestones](../../milestones?state=closed) for release notes.**\n\n**[.net 10](https://dotnet.microsoft.com/download/dotnet/10.0) or higher is required to run the dotnet tool.**\n\n\n## Value Proposition\n\nAutomatically extract snippets from code and injecting them into markdown documents has several benefits:\n\n * Snippets can be verified by a compiler or parser.\n * Tests can be run on snippets, or snippets can be pulled from existing tests.\n * Changes in code are automatically reflected in documentation.\n * Snippets are less likely to get out of sync with the main code-base.\n * Snippets in markdown is easier to create and maintain since any preferred editor can be used to edit them.\n\n\n## Behavior\n\n * Recursively scan the target directory for code files containing snippets. (See [exclusion](/docs/exclusion.md)).\n * Recursively scan the target directory for markdown (`.md` or `mdx`) files. (See [Document Scanning](#document-convention)).\n * Merge the snippets into those markdown files.\n\n\n## Installation\n\nEnsure [dotnet CLI is installed](https://docs.microsoft.com/en-us/dotnet/core/tools/).\n\nInstall [MarkdownSnippets.Tool](https://nuget.org/packages/MarkdownSnippets.Tool/)\n\n```ps\ndotnet tool install -g MarkdownSnippets.Tool\n```\n\nSee also: [MsBuild Task usage](/docs/msbuild.md)\n\n\n## Usage\n\n```ps\nmdsnippets C:\\Code\\TargetDirectory\n```\n\nIf no directory is passed the current directory will be used, but only if it exists with a git repository directory tree. If not an error is returned.\n\n\n### Document Convention\n\nThere are two approaches scanning and modifying markdown files.\n\n\n#### SourceTransform\n\nThis is the default.\n\n\n##### source.md file\n\nThe file convention recursively scans the target directory for all `*.source.md` files. Once snippets are merged the `.source.md` to produce `.md` files. So for example `readme.source.md` would be merged with snippets to produce `readme.md`. Note that this process will overwrite any existing `.md` files that have matching `.source.md` files.\n\n\n#### mdsource directory\n\nThere is a secondary convention that leverages the use of a directory named `mdsource`. Where `.source.md` files are placed in a `mdsource` sub-directory, the `mdsource` part of the file path will be removed when calculating the target path. This allows the `.source.md` to be grouped in a sub directory and avoid cluttering up the main documentation directory.\n\nWhen using the `mdsource` convention, all references to other files, such as links and images, should specify the full path from the root of the repository. This will allow those links to work correctly in both the source and generated markdown files. Relative paths cannot work for both the source and the target file.\n\n\n#### InPlaceOverwrite\n\nRecursively scans the target directory for all `*.md` files and merges snippets into those files.\n\n\n##### Command line\n\n```ps\nmdsnippets -c InPlaceOverwrite\n```\n\n```ps\nmdsnippets --convention InPlaceOverwrite\n```\n\n\n##### Config file\n\nCan be enabled in [mdsnippets.json config file](/docs/config-file.md).\n\n```json\n{\n  \"Convention\": \"InPlaceOverwrite\"\n}\n```\n\n\n#### Moving from SourceTransform to InPlaceOverwrite\n\n * Ensure `\"WriteHeader\": false` is used in `mdsnippets.json`.\n * Ensure `\"ReadOnly\": false` is used in `mdsnippets.json`.\n * Ensure using the current stable version and a docs generation has run.\n * Delete all `.source.md` files.\n * Modify `mdsnippets.json` to add `\"Convention\": \"InPlaceOverwrite\"`.\n * Run the docs generation.\n\n\n### Mark resulting files as read only\n\nTo mark the resulting documents files as read only use `-r` or `--read-only`.\n\nThis can be helpful in preventing incorrectly editing the documents file instead of the `.source.` file conventions.\n\n```ps\nmdsnippets -r true\n```\n\n```ps\nmdsnippets --read-only true\n```\n\n\n## Defining Snippets\n\nAny code wrapped in a convention based comment will be picked up. The comment needs to start with `begin-snippet:` which is followed by the key. The snippet is then terminated by `end-snippet`.\n\n```\n// begin-snippet: MySnippetName\nMy Snippet Code\n// end-snippet\n```\n\nNamed [C# regions](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-region) will also be picked up, with the name of the region used as the key.\n\n```\n#region MySnippetName\nMy Snippet Code\n#endregion\n```\n\nTo stop regions collapsing in Visual Studio [disable 'enter outlining mode when files open'](/docs/stop-regions-collapsing.png). See [Visual Studio outlining](https://docs.microsoft.com/en-us/visualstudio/ide/outlining).\n\n\n### UrlsAsSnippets\n\nUrls to files to be included as snippets. Space ` ` separated for multiple values.\n\nEach url will be accessible using the file name as a key. Any snippets within the files will be extracted and accessible as individual keyed snippets.\n\n```ps\nmdsnippets --urls-as-snippets \"https://github.com/SimonCropp/MarkdownSnippets/snippet.cs\"\n```\n\n```ps\nmdsnippets -u \"https://github.com/SimonCropp/MarkdownSnippets/snippet.cs\"\n```\n\n\n## Using Snippets\n\nThe keyed snippets can be used in any documentation `.md` file by adding the text `snippet: KEY`.\n\nThen snippets with that key.\n\nFor example\n\n<pre>\nSome blurb about the below snippet\nsnippet&#58; MySnippetName\n</pre>\n\nThe resulting markdown will be:\n\n    Some blurb about the below snippet\n    <!-- snippet: MySnippetName -->\n    <a id='snippet-MySnippetName'></a>\n    ```\n    My Snippet Code\n    ```\n    <sup><a href='/relativeUrlToFile#L1-L11' title='Snippet source file'>snippet source</a> | <a href='#snippet-MySnippetName' title='Start of snippet'>anchor</a></sup>\n    <!-- endSnippet -->\n\nNotes:\n\n * The vertical bar ( | ) is used to separate adjacent links as per web accessibility recommendations: https://webaim.org/techniques/hypertext/hypertext_links#groups\n * [H33: Supplementing link text with the title attribute](https://www.w3.org/TR/WCAG20-TECHS/H33.html)\n\n\n### Including a snippet from the web\n\nSnippets that start with `http` will be downloaded and the contents rendered. For example:\n\n`snippet: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/license.txt`\n\nWill render:\n\n\t<!-- snippet: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/license.txt -->\n\t<a id='snippet-https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/license.txt'></a>\n\t```txt\n\tThe MIT License (MIT)\n\t...\n\t```\n\t<sup><a href='#snippet-https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/license.txt' title='Snippet source file'>anchor</a></sup>\n\t<!-- endSnippet -->\n\nFiles are downloaded to `%temp%MarkdownSnippets` with a maximum of 100 files kept.\n\n`web-snippet:` can be used to reference remote content where a specific snippet is defined in that content.\n\n`web-snippet: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet`\n\nWill render:\n\n\t<!-- web-snippet: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet -->\n\t<a id='snippet-https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt%23snipPet'></a>\n\t```txt\n\tSome code\n\t```\n\t<sup><a href='https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet' title='Snippet source file'>anchor</a></sup>\n\t<!-- endSnippet -->\n\nYou can optionally provide a second URL that will be used for the source link. This is useful when the raw content URL is different from the view URL. For example:\n\n`web-snippet: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet https://github.com/SimonCropp/MarkdownSnippets/blob/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet`\n\nWill render:\n\n\t<!-- web-snippet: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet https://github.com/SimonCropp/MarkdownSnippets/blob/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet -->\n\t<a id='snippet-https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt%23snipPet'></a>\n\t```txt\n\tSome code\n\t```\n\t<sup><a href='https://github.com/SimonCropp/MarkdownSnippets/blob/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet#L1-L3' title='Snippet source file'>anchor</a></sup>\n\t<!-- endSnippet -->\n\n\n### Including a full file\n\nIf no snippet is found matching the defined name. The target directory will be searched for a file matching that name. For example:\n\n`snippet: license.txt`\n\nWill render:\n\n\t<!-- snippet: license.txt -->\n\t<a id='snippet-license.txt'></a>\n\t```txt\n\tThe MIT License (MIT)\n\t\n\tCopyright (c) 2013 Simon Cropp\n\t\n\tPermission is hereby granted, free of charge, to any person obtaining a copy of\n\tthis software and associated documentation files (the \"Software\"), to deal in\n\tthe Software without restriction, including without limitation the rights to\n\tuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\n\tthe Software, and to permit persons to whom the Software is furnished to do so,\n\tsubject to the following conditions:\n\t\n\tThe above copyright notice and this permission notice shall be included in all\n\tcopies or substantial portions of the Software.\n\t\n\tTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n\tIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\n\tFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n\tCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\n\tIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n\tCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\t```\n\t<sup><a href='/license.txt#L1-L20' title='Snippet source file'>snippet source</a> | <a href='#snippet-license.txt' title='Start of snippet'>anchor</a></sup>\n\t<!-- endSnippet -->\n\n\n### LinkFormat\n\nDefines the format of `snippet source` links that appear under each snippet.\n\n\n#### Command line\n\n```ps\nmdsnippets --link-format Bitbucket\n```\n\n```ps\nmdsnippets -l Bitbucket\n```\n\n\n#### Values\n\n<!-- snippet: LinkFormat.cs -->\n<a id='snippet-LinkFormat.cs'></a>\n```cs\nnamespace MarkdownSnippets;\n\npublic enum LinkFormat\n{\n    GitHub,\n    Tfs,\n    Bitbucket,\n    GitLab,\n    DevOps,\n    None\n}\n```\n<sup><a href='/src/MarkdownSnippets/Processing/LinkFormat.cs#L1-L11' title='Snippet source file'>snippet source</a> | <a href='#snippet-LinkFormat.cs' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\nLink format `None` will omit the source link but still keep the snippet anchor.\n\n\n### OmitSnippetLinks\n\nThe links below a snippet can be omitted.\n\n\n#### Command line\n\n```ps\nmdsnippets --omit-snippet-links true\n```\n\n\n#### Config file\n\n```\n{\n  \"OmitSnippetLinks\": true\n}\n```\n\n\n#### How links are constructed\n\n<!-- snippet: BuildLink -->\n<a id='snippet-BuildLink'></a>\n```cs\nswitch (linkFormat)\n{\n    case LinkFormat.GitHub:\n        Polyfill.Append(builder, $\"{path}#L{snippet.StartLine}-L{snippet.EndLine}\");\n        return;\n    case LinkFormat.Tfs:\n        Polyfill.Append(builder, $\"{path}&line={snippet.StartLine}&lineEnd={snippet.EndLine}\");\n        return;\n    case LinkFormat.Bitbucket:\n        Polyfill.Append(builder, $\"{path}#lines={snippet.StartLine}:{snippet.EndLine}\");\n        return;\n    case LinkFormat.GitLab:\n        Polyfill.Append(builder, $\"{path}#L{snippet.StartLine}-{snippet.EndLine}\");\n        return;\n    case LinkFormat.DevOps:\n        Polyfill.Append(builder, $\"?path={path}&line={snippet.StartLine}&lineEnd={snippet.EndLine}&lineStartColumn=1&lineEndColumn=999\");\n        return;\n    case LinkFormat.None:\n        throw new($\"Unknown LinkFormat: {linkFormat}\");\n}\n```\n<sup><a href='/src/MarkdownSnippets/Processing/SnippetMarkdownHandling.cs#L133-L154' title='Snippet source file'>snippet source</a> | <a href='#snippet-BuildLink' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\n\n### UrlPrefix\n\nUrlPrefix allows a string to be defined that will prefix all snippet links. This is helpful when the markdown file are being hosted on a site that is not co-located with the source code files. It can be defined in the [config file](/docs/config-file.md), the [MsBuild task](/docs/msbuild.md), and the dotnet tool.\n\n\n#### Command line\n\n```ps\nmdsnippets --url-prefix \"TheUrlPrefix\"\n```\n\n\n#### Config file\n\n```\n{\n  \"UrlPrefix\": \"TheUrlPrefix\"\n}\n```\n\n\n## Add to Windows Explorer\n\nUse [src/context-menu.reg](context-menu.reg) to add MarkdownSnippets to the Windows Explorer context menu.\n\n<!-- snippet: context-menu.reg -->\n<a id='snippet-context-menu.reg'></a>\n```reg\nWindows Registry Editor Version 5.00\n[HKEY_CLASSES_ROOT\\Directory\\Shell]\n@=\"none\"\n[HKEY_CLASSES_ROOT\\Directory\\shell\\mdsnippets]\n\"MUIVerb\"=\"run mdsnippets\"\n\"Position\"=\"bottom\"\n[HKEY_CLASSES_ROOT\\Directory\\Background\\shell\\mdsnippets]\n\"MUIVerb\"=\"run mdsnippets\"\n\"Position\"=\"bottom\"\n[HKEY_CLASSES_ROOT\\Directory\\shell\\mdsnippets\\command]\n@=\"cmd.exe /c mdsnippets \\\"%V\\\"\"\n[HKEY_CLASSES_ROOT\\Directory\\Background\\shell\\mdsnippets\\command]\n@=\"cmd.exe /c mdsnippets \\\"%V\\\"\"\n```\n<sup><a href='/src/context-menu.reg#L1-L13' title='Snippet source file'>snippet source</a> | <a href='#snippet-context-menu.reg' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\n\n## Expressive Code / Snippet Metadata\n\nWhen defining snippets, additional metadata can be added at the source to the rendered snippet using the following syntax.\n\n```csharp\n// begin-snippet: HelloWorld(title=Program.cs {1})\nConsole.WriteLine(\"Hello, World\");\n// end-snippet\n```\n\nNote the text within the parenthesis; this is metadata we want to add to the rendered Markdown block immediately after the language declaration. The metadata is stripped and the key remains `HelloWorld`. The feature produces the following output destination (will vary based on configuration):\n\n````markdown\n<-- begin-snippet: HelloWorld -->\n```csharp title=Program.cs {1}\nConsole.WriteLine(\"Hello, World\");\n```\n<-- end-snippet -->\n````\n\nThis syntax is known as [Expressive Code](https://expressive-code.com/) and is supported in documentation systems such as [Astro Starlight](https://github.com/withastro/starlight/) but can be installed in any Markdown-powered tool that supports [reHype](https://github.com/rehypejs/rehype).\n\nIt is important to note, the metadata is not explicitly limited to Expressive code. Any text within the `()` will be rendered after the language block. This can be useful for adding additional information based on specific rendering engine. For example, if a presentation tool such as [Sli.dev](https://sli.dev/), then this feature to apply [magic-move animations](https://sli.dev/features/shiki-magic-move).\n\n```csharp\n// begin-snippet: EncapsulateVariable({*|2})\nConsole.WriteLine(\"Hello, World\");\n// end-snippet\n```\n\nThe above snippet will render as follows:\n\n````markdown\n<-- begin-snippet: EncapsulateVariable -->\n```csharp {*|2}\nConsole.WriteLine(\"Hello, World\");\n```\n<-- end-snippet -->\n````\n\n\n## Language Override\n\nBy default the language of a rendered fenced code block is derived from the source file extension (e.g. a snippet extracted from a `.cs` file renders as `csharp`). The language can be overridden per snippet by adding a `lang=` token as the first item inside the parenthesised metadata:\n\n```csharp\n// begin-snippet: SampleJson(lang=json)\n{\"hello\": \"world\"}\n// end-snippet\n```\n\nRenders as:\n\n````markdown\n<-- begin-snippet: SampleJson -->\n```json\n{\"hello\": \"world\"}\n```\n<-- end-snippet -->\n````\n\n`lang=` can be combined with Expressive Code metadata — the language token must come first, followed by a space, then the remaining metadata:\n\n```csharp\n// begin-snippet: SampleJson(lang=json title=config.json)\n{\"hello\": \"world\"}\n// end-snippet\n```\n\nThe value must be lowercase alphanumeric.\n\n\n## More Documentation\n\n  * Developer Information<!-- include: doc-index. path: /docs/mdsource/doc-index.include.md -->\n    * [.net API](/docs/api.md)\n    * [MsBuild Task](/docs/msbuild.md)\n    * [Github Action](/docs/github-action.md)\n  * Customisation\n    * [Config file convention](/docs/config-file.md)\n    * [Max Width](/docs/max-width.md)\n    * [Includes](/docs/includes.md)\n    * [Directory Exclusion](/docs/exclusion.md)\n    * [Header](/docs/header.md)\n  * Writing Documentation\n    * [Indentation](/docs/indentation.md)\n    * [Table of contents](/docs/toc.md)<!-- endInclude -->\n\n\n## Credits\n\nLoosely based on some code from https://github.com/shiftkey/scribble.\n\n\n## Icon\n\n[Down](https://thenounproject.com/AlfredoCreates/collection/arrows-5-glyph/) by [Alfredo Creates](https://thenounproject.com/AlfredoCreates) from [The Noun Project](https://thenounproject.com/).\n"
  },
  {
    "path": "readme.source.md",
    "content": "# <img src=\"/src/icon.png\" height=\"30px\"> MarkdownSnippets\n\n[![Build status](https://img.shields.io/appveyor/build/SimonCropp/MarkdownSnippets)](https://ci.appveyor.com/project/SimonCropp/MarkdownSnippets)\n[![NuGet Status](https://img.shields.io/nuget/v/MarkdownSnippets.Tool.svg?label=dotnet%20tool)](https://www.nuget.org/packages/MarkdownSnippets.Tool/)\n[![NuGet Status](https://img.shields.io/nuget/v/MarkdownSnippets.MsBuild.svg?label=MsBuild%20Task)](https://www.nuget.org/packages/MarkdownSnippets.MsBuild/)\n[![NuGet Status](https://img.shields.io/nuget/v/MarkdownSnippets.svg?label=.net%20API)](https://www.nuget.org/packages/MarkdownSnippets/)\n\nA [dotnet tool](https://docs.microsoft.com/en-us/dotnet/core/tools/global-tools) or [MsBuild Task](/docs/msbuild.md) that extract snippets from code files and merges them into markdown documents.\n\n**See [Milestones](../../milestones?state=closed) for release notes.**\n\n**[.net 10](https://dotnet.microsoft.com/download/dotnet/10.0) or higher is required to run the dotnet tool.**\n\n\n## Value Proposition\n\nAutomatically extract snippets from code and injecting them into markdown documents has several benefits:\n\n * Snippets can be verified by a compiler or parser.\n * Tests can be run on snippets, or snippets can be pulled from existing tests.\n * Changes in code are automatically reflected in documentation.\n * Snippets are less likely to get out of sync with the main code-base.\n * Snippets in markdown is easier to create and maintain since any preferred editor can be used to edit them.\n\n\n## Behavior\n\n * Recursively scan the target directory for code files containing snippets. (See [exclusion](/docs/exclusion.md)).\n * Recursively scan the target directory for markdown (`.md` or `mdx`) files. (See [Document Scanning](#document-convention)).\n * Merge the snippets into those markdown files.\n\n\n## Installation\n\nEnsure [dotnet CLI is installed](https://docs.microsoft.com/en-us/dotnet/core/tools/).\n\nInstall [MarkdownSnippets.Tool](https://nuget.org/packages/MarkdownSnippets.Tool/)\n\n```ps\ndotnet tool install -g MarkdownSnippets.Tool\n```\n\nSee also: [MsBuild Task usage](/docs/msbuild.md)\n\n\n## Usage\n\n```ps\nmdsnippets C:\\Code\\TargetDirectory\n```\n\nIf no directory is passed the current directory will be used, but only if it exists with a git repository directory tree. If not an error is returned.\n\n\n### Document Convention\n\nThere are two approaches scanning and modifying markdown files.\n\n\n#### SourceTransform\n\nThis is the default.\n\n\n##### source.md file\n\nThe file convention recursively scans the target directory for all `*.source.md` files. Once snippets are merged the `.source.md` to produce `.md` files. So for example `readme.source.md` would be merged with snippets to produce `readme.md`. Note that this process will overwrite any existing `.md` files that have matching `.source.md` files.\n\n\n#### mdsource directory\n\nThere is a secondary convention that leverages the use of a directory named `mdsource`. Where `.source.md` files are placed in a `mdsource` sub-directory, the `mdsource` part of the file path will be removed when calculating the target path. This allows the `.source.md` to be grouped in a sub directory and avoid cluttering up the main documentation directory.\n\nWhen using the `mdsource` convention, all references to other files, such as links and images, should specify the full path from the root of the repository. This will allow those links to work correctly in both the source and generated markdown files. Relative paths cannot work for both the source and the target file.\n\n\n#### InPlaceOverwrite\n\nRecursively scans the target directory for all `*.md` files and merges snippets into those files.\n\n\n##### Command line\n\n```ps\nmdsnippets -c InPlaceOverwrite\n```\n\n```ps\nmdsnippets --convention InPlaceOverwrite\n```\n\n\n##### Config file\n\nCan be enabled in [mdsnippets.json config file](/docs/config-file.md).\n\n```json\n{\n  \"Convention\": \"InPlaceOverwrite\"\n}\n```\n\n\n#### Moving from SourceTransform to InPlaceOverwrite\n\n * Ensure `\"WriteHeader\": false` is used in `mdsnippets.json`.\n * Ensure `\"ReadOnly\": false` is used in `mdsnippets.json`.\n * Ensure using the current stable version and a docs generation has run.\n * Delete all `.source.md` files.\n * Modify `mdsnippets.json` to add `\"Convention\": \"InPlaceOverwrite\"`.\n * Run the docs generation.\n\n\n### Mark resulting files as read only\n\nTo mark the resulting documents files as read only use `-r` or `--read-only`.\n\nThis can be helpful in preventing incorrectly editing the documents file instead of the `.source.` file conventions.\n\n```ps\nmdsnippets -r true\n```\n\n```ps\nmdsnippets --read-only true\n```\n\n\n## Defining Snippets\n\nAny code wrapped in a convention based comment will be picked up. The comment needs to start with `begin-snippet:` which is followed by the key. The snippet is then terminated by `end-snippet`.\n\n```\n// begin-snippet: MySnippetName\nMy Snippet Code\n// end-snippet\n```\n\nNamed [C# regions](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-region) will also be picked up, with the name of the region used as the key.\n\n```\n#region MySnippetName\nMy Snippet Code\n#endregion\n```\n\nTo stop regions collapsing in Visual Studio [disable 'enter outlining mode when files open'](/docs/stop-regions-collapsing.png). See [Visual Studio outlining](https://docs.microsoft.com/en-us/visualstudio/ide/outlining).\n\n\n### UrlsAsSnippets\n\nUrls to files to be included as snippets. Space ` ` separated for multiple values.\n\nEach url will be accessible using the file name as a key. Any snippets within the files will be extracted and accessible as individual keyed snippets.\n\n```ps\nmdsnippets --urls-as-snippets \"https://github.com/SimonCropp/MarkdownSnippets/snippet.cs\"\n```\n\n```ps\nmdsnippets -u \"https://github.com/SimonCropp/MarkdownSnippets/snippet.cs\"\n```\n\n\n## Using Snippets\n\nThe keyed snippets can be used in any documentation `.md` file by adding the text `snippet: KEY`.\n\nThen snippets with that key.\n\nFor example\n\n<pre>\nSome blurb about the below snippet\nsnippet&#58; MySnippetName\n</pre>\n\nThe resulting markdown will be:\n\n    Some blurb about the below snippet\n    <!-- snippet: MySnippetName -->\n    <a id='snippet-MySnippetName'></a>\n    ```\n    My Snippet Code\n    ```\n    <sup><a href='/relativeUrlToFile#L1-L11' title='Snippet source file'>snippet source</a> | <a href='#snippet-MySnippetName' title='Start of snippet'>anchor</a></sup>\n    <!-- endSnippet -->\n\nNotes:\n\n * The vertical bar ( | ) is used to separate adjacent links as per web accessibility recommendations: https://webaim.org/techniques/hypertext/hypertext_links#groups\n * [H33: Supplementing link text with the title attribute](https://www.w3.org/TR/WCAG20-TECHS/H33.html)\n\n\n### Including a snippet from the web\n\nSnippets that start with `http` will be downloaded and the contents rendered. For example:\n\n`snippet: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/license.txt`\n\nWill render:\n\n\t<!-- snippet: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/license.txt -->\n\t<a id='snippet-https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/license.txt'></a>\n\t```txt\n\tThe MIT License (MIT)\n\t...\n\t```\n\t<sup><a href='#snippet-https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/license.txt' title='Snippet source file'>anchor</a></sup>\n\t<!-- endSnippet -->\n\nFiles are downloaded to `%temp%MarkdownSnippets` with a maximum of 100 files kept.\n\n`web-snippet:` can be used to reference remote content where a specific snippet is defined in that content.\n\n`web-snippet: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet`\n\nWill render:\n\n\t<!-- web-snippet: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet -->\n\t<a id='snippet-https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt%23snipPet'></a>\n\t```txt\n\tSome code\n\t```\n\t<sup><a href='https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet' title='Snippet source file'>anchor</a></sup>\n\t<!-- endSnippet -->\n\nYou can optionally provide a second URL that will be used for the source link. This is useful when the raw content URL is different from the view URL. For example:\n\n`web-snippet: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet https://github.com/SimonCropp/MarkdownSnippets/blob/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet`\n\nWill render:\n\n\t<!-- web-snippet: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet https://github.com/SimonCropp/MarkdownSnippets/blob/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet -->\n\t<a id='snippet-https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt%23snipPet'></a>\n\t```txt\n\tSome code\n\t```\n\t<sup><a href='https://github.com/SimonCropp/MarkdownSnippets/blob/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet#L1-L3' title='Snippet source file'>anchor</a></sup>\n\t<!-- endSnippet -->\n\n\n### Including a full file\n\nIf no snippet is found matching the defined name. The target directory will be searched for a file matching that name. For example:\n\n`snippet: license.txt`\n\nWill render:\n\n\tsnippet: license.txt\n\n\n### LinkFormat\n\nDefines the format of `snippet source` links that appear under each snippet.\n\n\n#### Command line\n\n```ps\nmdsnippets --link-format Bitbucket\n```\n\n```ps\nmdsnippets -l Bitbucket\n```\n\n\n#### Values\n\nsnippet: LinkFormat.cs\n\nLink format `None` will omit the source link but still keep the snippet anchor.\n\n\n### OmitSnippetLinks\n\nThe links below a snippet can be omitted.\n\n\n#### Command line\n\n```ps\nmdsnippets --omit-snippet-links true\n```\n\n\n#### Config file\n\n```\n{\n  \"OmitSnippetLinks\": true\n}\n```\n\n\n#### How links are constructed\n\nsnippet: BuildLink\n\n\n### UrlPrefix\n\nUrlPrefix allows a string to be defined that will prefix all snippet links. This is helpful when the markdown file are being hosted on a site that is not co-located with the source code files. It can be defined in the [config file](/docs/config-file.md), the [MsBuild task](/docs/msbuild.md), and the dotnet tool.\n\n\n#### Command line\n\n```ps\nmdsnippets --url-prefix \"TheUrlPrefix\"\n```\n\n\n#### Config file\n\n```\n{\n  \"UrlPrefix\": \"TheUrlPrefix\"\n}\n```\n\n\n## Add to Windows Explorer\n\nUse [src/context-menu.reg](context-menu.reg) to add MarkdownSnippets to the Windows Explorer context menu.\n\nsnippet: context-menu.reg\n\n\n## Expressive Code / Snippet Metadata\n\nWhen defining snippets, additional metadata can be added at the source to the rendered snippet using the following syntax.\n\n```csharp\n// begin-snippet: HelloWorld(title=Program.cs {1})\nConsole.WriteLine(\"Hello, World\");\n// end-snippet\n```\n\nNote the text within the parenthesis; this is metadata we want to add to the rendered Markdown block immediately after the language declaration. The metadata is stripped and the key remains `HelloWorld`. The feature produces the following output destination (will vary based on configuration):\n\n````markdown\n<-- begin-snippet: HelloWorld -->\n```csharp title=Program.cs {1}\nConsole.WriteLine(\"Hello, World\");\n```\n<-- end-snippet -->\n````\n\nThis syntax is known as [Expressive Code](https://expressive-code.com/) and is supported in documentation systems such as [Astro Starlight](https://github.com/withastro/starlight/) but can be installed in any Markdown-powered tool that supports [reHype](https://github.com/rehypejs/rehype).\n\nIt is important to note, the metadata is not explicitly limited to Expressive code. Any text within the `()` will be rendered after the language block. This can be useful for adding additional information based on specific rendering engine. For example, if a presentation tool such as [Sli.dev](https://sli.dev/), then this feature to apply [magic-move animations](https://sli.dev/features/shiki-magic-move).\n\n```csharp\n// begin-snippet: EncapsulateVariable({*|2})\nConsole.WriteLine(\"Hello, World\");\n// end-snippet\n```\n\nThe above snippet will render as follows:\n\n````markdown\n<-- begin-snippet: EncapsulateVariable -->\n```csharp {*|2}\nConsole.WriteLine(\"Hello, World\");\n```\n<-- end-snippet -->\n````\n\n\n## Language Override\n\nBy default the language of a rendered fenced code block is derived from the source file extension (e.g. a snippet extracted from a `.cs` file renders as `csharp`). The language can be overridden per snippet by adding a `lang=` token as the first item inside the parenthesised metadata:\n\n```csharp\n// begin-snippet: SampleJson(lang=json)\n{\"hello\": \"world\"}\n// end-snippet\n```\n\nRenders as:\n\n````markdown\n<-- begin-snippet: SampleJson -->\n```json\n{\"hello\": \"world\"}\n```\n<-- end-snippet -->\n````\n\n`lang=` can be combined with Expressive Code metadata — the language token must come first, followed by a space, then the remaining metadata:\n\n```csharp\n// begin-snippet: SampleJson(lang=json title=config.json)\n{\"hello\": \"world\"}\n// end-snippet\n```\n\nThe value must be lowercase alphanumeric.\n\n\n## More Documentation\n\ninclude: doc-index\n\n\n## Credits\n\nLoosely based on some code from https://github.com/shiftkey/scribble.\n\n\n## Icon\n\n[Down](https://thenounproject.com/AlfredoCreates/collection/arrows-5-glyph/) by [Alfredo Creates](https://thenounproject.com/AlfredoCreates) from [The Noun Project](https://thenounproject.com/)."
  },
  {
    "path": "schema.json",
    "content": "{\n  \"definitions\": {},\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$id\": \"MarkdownSnippets\",\n  \"title\": \"Root\",\n  \"type\": \"object\",\n  \"required\": [\n  ],\n  \"properties\": {\n    \"OmitSnippetLinks\": {\n      \"$id\": \"#root/OmitSnippetLinks\",\n      \"title\": \"OmitSnippetLinks\",\n      \"type\": \"boolean\",\n      \"examples\": [\n        true\n      ],\n      \"default\": false\n    },\n    \"ReadOnly\": {\n      \"$id\": \"#root/ReadOnly\",\n      \"title\": \"Readonly\",\n      \"type\": \"boolean\",\n      \"examples\": [\n        true\n      ],\n      \"default\": false\n    },\n    \"LinkFormat\": {\n      \"$id\": \"#root/LinkFormat\",\n      \"title\": \"Linkformat\",\n      \"type\": \"string\",\n      \"default\": \"GitHub\",\n      \"enum\": [\n        \"Tfs\",\n        \"GitHub\",\n        \"Bitbucket\",\n        \"GitLab\",\n        \"DevOps\",\n        \"None\"\n      ],\n      \"pattern\": \"^.*$\"\n    },\n    \"TocLevel\": {\n      \"$id\": \"#root/TocLevel\",\n      \"title\": \"Toclevel\",\n      \"type\": \"integer\",\n      \"examples\": [\n        3\n      ],\n      \"default\": 1\n    },\n    \"UrlsAsSnippets\": {\n      \"$id\": \"#root/UrlsAsSnippets\",\n      \"title\": \"Urlsassnippets\",\n      \"type\": \"array\",\n      \"default\": [],\n      \"items\": {\n        \"$id\": \"#root/UrlsAsSnippets/items\",\n        \"title\": \"Items\",\n        \"type\": \"string\",\n        \"default\": \"\",\n        \"examples\": [\n          \"https://github.com/SimonCropp/MarkdownSnippets/snippet.cs\"\n        ],\n        \"pattern\": \"^.*$\"\n      }\n    },\n    \"TocExcludes\": {\n      \"$id\": \"#root/TocExcludes\",\n      \"title\": \"Tocexcludes\",\n      \"type\": \"array\",\n      \"default\": [],\n      \"items\": {\n        \"$id\": \"#root/TocExcludes/items\",\n        \"title\": \"Items\",\n        \"type\": \"string\",\n        \"default\": \"\",\n        \"examples\": [\n          \"heading1\",\n          \"heading2\"\n        ],\n        \"pattern\": \"^.*$\"\n      }\n    },\n    \"ExcludeMarkdownDirectories\": {\n      \"$id\": \"#root/ExcludeMarkdownDirectories\",\n      \"title\": \"ExcludeMarkdownDirectories\",\n      \"type\": \"array\",\n      \"default\": [],\n      \"items\": {\n        \"$id\": \"#root/ExcludeMarkdownDirectories/items\",\n        \"title\": \"Items\",\n        \"type\": \"string\",\n        \"default\": \"\",\n        \"examples\": [\n          \"directory1\",\n          \"directory2\"\n        ],\n        \"pattern\": \"^.*$\"\n      }\n    },\n    \"ExcludeSnippetDirectories\": {\n      \"$id\": \"#root/ExcludeSnippetDirectories\",\n      \"title\": \"ExcludeSnippetDirectories\",\n      \"type\": \"array\",\n      \"default\": [],\n      \"items\": {\n        \"$id\": \"#root/ExcludeSnippetDirectories/items\",\n        \"title\": \"Items\",\n        \"type\": \"string\",\n        \"default\": \"\",\n        \"examples\": [\n          \"directory1\",\n          \"directory1\"\n        ],\n        \"pattern\": \"^.*$\"\n      }\n    },\n    \"ExcludeSnippetFiles\": {\n      \"$id\": \"#root/ExcludeSnippetFiles\",\n      \"title\": \"ExcludeSnippetFiles\",\n      \"type\": \"array\",\n      \"default\": [],\n      \"items\": {\n        \"$id\": \"#root/ExcludeSnippetFiles/items\",\n        \"title\": \"Items\",\n        \"type\": \"string\",\n        \"default\": \"\",\n        \"examples\": [\n          \"*.verified.txt\",\n          \"*.received.txt\"\n        ],\n        \"pattern\": \"^.*$\"\n      }\n    },\n    \"ExcludeDirectories\": {\n      \"$id\": \"#root/ExcludeDirectories\",\n      \"title\": \"ExcludeDirectories\",\n      \"type\": \"array\",\n      \"default\": [],\n      \"items\": {\n        \"$id\": \"#root/ExcludeDirectories/items\",\n        \"title\": \"Items\",\n        \"type\": \"string\",\n        \"default\": \"\",\n        \"examples\": [\n          \"directory1\",\n          \"directory2\"\n        ],\n        \"pattern\": \"^.*$\"\n      }\n    },\n    \"DocumentExtensions\": {\n      \"$id\": \"#root/DocumentExtensions\",\n      \"title\": \"Documentextensions\",\n      \"type\": \"array\",\n      \"default\": [],\n      \"items\": {\n        \"$id\": \"#root/DocumentExtensions/items\",\n        \"title\": \"Items\",\n        \"type\": \"string\",\n        \"default\": \"md\",\n        \"examples\": [\n          \"md\"\n        ],\n        \"pattern\": \"^.*$\"\n      }\n    },\n    \"Convention\": {\n      \"$id\": \"#root/Convention\",\n      \"title\": \"Convention\",\n      \"type\": \"string\",\n      \"default\": \"InPlaceOverwrite\",\n      \"enum\": [\n        \"InPlaceOverwrite\",\n        \"SourceTransform\"\n      ],\n      \"pattern\": \"^.*$\"\n    },\n    \"WriteHeader\": {\n      \"$id\": \"#root/WriteHeader\",\n      \"title\": \"Writeheader\",\n      \"type\": \"boolean\",\n      \"examples\": [\n        false\n      ],\n      \"default\": true\n    },\n    \"MaxWidth\": {\n      \"$id\": \"#root/MaxWidth\",\n      \"title\": \"Maxwidth\",\n      \"type\": \"integer\",\n      \"examples\": [\n        80\n      ],\n      \"default\": 100\n    },\n    \"Header\": {\n      \"$id\": \"#root/Header\",\n      \"title\": \"Header\",\n      \"type\": \"string\",\n      \"default\": \"\",\n      \"examples\": [\n        \"GENERATED FILE - Source File: {relativePath}\"\n      ],\n      \"pattern\": \"^.*$\"\n    },\n    \"UrlPrefix\": {\n      \"$id\": \"#root/UrlPrefix\",\n      \"title\": \"UrlPrefix\",\n      \"type\": \"string\",\n      \"default\": \"\",\n      \"examples\": [\n        \"TheUrlPrefix\"\n      ],\n      \"pattern\": \"^.*$\"\n    },\n    \"TreatMissingAsWarning\": {\n      \"$id\": \"#root/TreatMissingAsWarning\",\n      \"title\": \"TreatMissingAsWarning\",\n      \"type\": \"boolean\",\n      \"examples\": [\n        false\n      ],\n      \"default\": true\n    },\n    \"ValidateContent\": {\n      \"$id\": \"#root/ValidateContent\",\n      \"title\": \"ValidateContent\",\n      \"type\": \"boolean\",\n      \"examples\": [\n        false\n      ],\n      \"default\": true\n    }\n  }\n}"
  },
  {
    "path": "src/Benchmarks/Benchmarks.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <OutputType>Exe</OutputType>\n  </PropertyGroup>\n  <ItemGroup>\n    <PackageReference Include=\"BenchmarkDotNet\" />\n    <ProjectReference Include=\"..\\MarkdownSnippets\\MarkdownSnippets.csproj\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/Benchmarks/FullRenderBenchmarks.cs",
    "content": "using System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Text;\nusing BenchmarkDotNet.Attributes;\nusing MarkdownSnippets;\n\n[MemoryDiagnoser]\npublic class FullRenderBenchmarks\n{\n    string repoRoot = null!;\n    Dictionary<string, IReadOnlyList<Snippet>> snippetLookup = null!;\n    string markdownInput = null!;\n\n    static bool DirectoryFilter(string path) =>\n        !path.Contains(\"IncludeFileFinder\") &&\n        !path.Contains(\"DirectoryMarkdownProcessor\") &&\n        !DefaultDirectoryExclusions.ShouldExcludeDirectory(path);\n\n    static bool SnippetDirectoryFilter(string path) =>\n        DirectoryFilter(path) &&\n        !path.Contains(\"badsnippets\");\n\n    [GlobalSetup]\n    public void Setup()\n    {\n        repoRoot = GitRepoDirectoryFinder.FindForFilePath();\n\n        // pre-load snippets for the in-memory MarkdownProcessor benchmark\n        var processor = new DirectoryMarkdownProcessor(\n            targetDirectory: repoRoot,\n            directoryIncludes: DirectoryFilter,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: SnippetDirectoryFilter,\n            scanForMdFiles: false,\n            tocLevel: 1);\n        snippetLookup = processor.Snippets\n            .Where(s => !s.IsInError)\n            .GroupBy(s => s.Key)\n            .ToDictionary(g => g.Key, g => (IReadOnlyList<Snippet>) g.ToList());\n\n        // build a synthetic markdown doc that references real snippet keys\n        var sb = new StringBuilder();\n        sb.AppendLine(\"# Benchmark Document\");\n        sb.AppendLine();\n        foreach (var key in snippetLookup.Keys.Take(50))\n        {\n            sb.AppendLine($\"snippet: {key}\");\n            sb.AppendLine();\n        }\n\n        markdownInput = sb.ToString();\n    }\n\n    /// <summary>\n    /// End-to-end: file discovery, snippet extraction, markdown processing, and file writing.\n    /// </summary>\n    [Benchmark(Description = \"Full repo render\")]\n    public void FullRepoRender()\n    {\n        var processor = new DirectoryMarkdownProcessor(\n            targetDirectory: repoRoot,\n            directoryIncludes: DirectoryFilter,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: SnippetDirectoryFilter,\n            tocLevel: 1,\n            tocExcludes: [\"Icon\", \"Credits\", \"Release Notes\"]);\n        processor.Run();\n    }\n\n    /// <summary>\n    /// Constructor only: file discovery + snippet extraction (no markdown processing).\n    /// </summary>\n    [Benchmark(Description = \"File discovery + snippet extraction\")]\n    public DirectoryMarkdownProcessor FileDiscoveryAndSnippetExtraction() =>\n        new(\n            targetDirectory: repoRoot,\n            directoryIncludes: DirectoryFilter,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: SnippetDirectoryFilter,\n            scanForMdFiles: false,\n            tocLevel: 1);\n\n    /// <summary>\n    /// In-memory markdown processing with pre-loaded snippets, no file I/O.\n    /// </summary>\n    [Benchmark(Description = \"MarkdownProcessor.Apply (50 snippets)\")]\n    public string MarkdownProcessorApply()\n    {\n        var processor = new MarkdownProcessor(\n            convention: DocumentConvention.SourceTransform,\n            snippets: snippetLookup,\n            includes: [],\n            appendSnippets: SimpleSnippetMarkdownHandling.Append,\n            snippetSourceFiles: [],\n            tocLevel: 2,\n            writeHeader: false,\n            targetDirectory: repoRoot,\n            validateContent: false,\n            allFiles: []);\n\n        return processor.Apply(markdownInput, \"benchmark.source.md\");\n    }\n}\n"
  },
  {
    "path": "src/Benchmarks/Program.cs",
    "content": "using BenchmarkDotNet.Running;\n\nBenchmarkRunner.Run<FullRenderBenchmarks>(args: args);\n"
  },
  {
    "path": "src/ConfigReader/AssemblyInfo.cs",
    "content": "﻿[assembly: InternalsVisibleTo(\"ConfigReader.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e191859fcd1deee68b96927c170783ced0c9a471a6424a0a011cfd31156a49dd73c4ad4a88b995fb918c0b43e0c005ef5fb72d53a328a64bde825cb5f2e4c53d66f69fcbb87d6737128b98e677a42091974b5f56093123a2dd6bc738af751b101d41c4f7a996e217b61967a3aa1ae7bc791d19c1cbeef47f0cdd20d288dff1a3\")]"
  },
  {
    "path": "src/ConfigReader/ConfigDefaults.cs",
    "content": "﻿public static class ConfigDefaults\n{\n    public static ConfigResult Convert(ConfigInput? fileConfig, ConfigInput otherConfig)\n    {\n        if (fileConfig == null)\n        {\n            return new()\n            {\n                ValidateContent = otherConfig.ValidateContent.GetValueOrDefault(),\n                OmitSnippetLinks = otherConfig.OmitSnippetLinks.GetValueOrDefault(),\n                ReadOnly = otherConfig.ReadOnly.GetValueOrDefault(),\n                WriteHeader = otherConfig.WriteHeader,\n                LinkFormat = otherConfig.LinkFormat.GetValueOrDefault(LinkFormat.GitHub),\n                Convention = otherConfig.Convention.GetValueOrDefault(DocumentConvention.SourceTransform),\n                ExcludeDirectories = otherConfig.ExcludeDirectories,\n                ExcludeMarkdownDirectories = otherConfig.ExcludeMarkdownDirectories,\n                ExcludeSnippetDirectories = otherConfig.ExcludeSnippetDirectories,\n                ExcludeSnippetFiles = otherConfig.ExcludeSnippetFiles,\n                Header = otherConfig.Header,\n                UrlPrefix = otherConfig.UrlPrefix,\n                TocExcludes = otherConfig.TocExcludes,\n                UrlsAsSnippets = otherConfig.UrlsAsSnippets,\n                TocLevel = otherConfig.TocLevel.GetValueOrDefault(2),\n                MaxWidth = otherConfig.MaxWidth.GetValueOrDefault(int.MaxValue),\n                TreatMissingAsWarning = otherConfig.TreatMissingAsWarning.GetValueOrDefault(),\n            };\n        }\n\n        return new()\n        {\n            ValidateContent = GetValueOrDefault(\"ValidateContent\", otherConfig.ValidateContent, fileConfig.ValidateContent, false),\n            OmitSnippetLinks = GetValueOrDefault(\"OmitSnippetLinks\", otherConfig.OmitSnippetLinks, fileConfig.OmitSnippetLinks, false),\n            ReadOnly = GetValueOrNull(\"ReadOnly\", otherConfig.ReadOnly, fileConfig.ReadOnly),\n            WriteHeader = GetValueOrNull(\"WriteHeader\", otherConfig.WriteHeader, fileConfig.WriteHeader),\n            LinkFormat = GetValueOrDefault(\"LinkFormat\", otherConfig.LinkFormat, fileConfig.LinkFormat, LinkFormat.GitHub),\n            Convention = GetValueOrDefault(\"Convention\", otherConfig.Convention, fileConfig.Convention, DocumentConvention.SourceTransform),\n            TocLevel = GetValueOrDefault(\"TocLevel\", otherConfig.TocLevel, fileConfig.TocLevel, 2),\n            MaxWidth = GetValueOrDefault(\"MaxWidth\", otherConfig.MaxWidth, fileConfig.MaxWidth, int.MaxValue),\n            Header = GetValueOrDefault(\"Header\", otherConfig.Header, fileConfig.Header),\n            UrlPrefix = GetValueOrDefault(\"UrlPrefix\", otherConfig.UrlPrefix, fileConfig.UrlPrefix),\n            ExcludeDirectories = JoinLists(fileConfig.ExcludeDirectories, otherConfig.ExcludeDirectories),\n            ExcludeSnippetDirectories = JoinLists(fileConfig.ExcludeSnippetDirectories, otherConfig.ExcludeSnippetDirectories),\n            ExcludeSnippetFiles = JoinLists(fileConfig.ExcludeSnippetFiles, otherConfig.ExcludeSnippetFiles),\n            ExcludeMarkdownDirectories = JoinLists(fileConfig.ExcludeMarkdownDirectories, otherConfig.ExcludeMarkdownDirectories),\n            TocExcludes = JoinLists(fileConfig.TocExcludes, otherConfig.TocExcludes),\n            UrlsAsSnippets = JoinLists(fileConfig.UrlsAsSnippets, otherConfig.UrlsAsSnippets),\n            TreatMissingAsWarning = GetValueOrDefault(\n                \"TreatMissingAsWarning\",\n                otherConfig.TreatMissingAsWarning,\n                fileConfig.TreatMissingAsWarning,\n                false),\n        };\n    }\n\n    static List<string> JoinLists(List<string> list1, List<string> list2) =>\n        list1\n            .Concat(list2)\n            .Distinct()\n            .ToList();\n\n    static T GetValueOrDefault<T>(string name, T? input, T? config, T defaultValue)\n        where T : struct\n    {\n        if (input != null && config != null)\n        {\n            throw new ConfigurationException($\"'{name}' cannot be defined in both mdsnippets.json and input\");\n        }\n\n        if (input != null)\n        {\n            return input.Value;\n        }\n\n        if (config != null)\n        {\n            return config.Value;\n        }\n\n        return defaultValue;\n    }\n\n    static T? GetValueOrNull<T>(string name, T? input, T? config)\n        where T : struct\n    {\n        if (input != null &&\n            config != null)\n        {\n            throw new ConfigurationException($\"'{name}' cannot be defined in both mdsnippets.json and input\");\n        }\n\n        if (input != null)\n        {\n            return input.Value;\n        }\n\n        return config;\n    }\n\n    static string? GetValueOrDefault(string name, string? input, string? config)\n    {\n        if (input != null && config != null)\n        {\n            throw new ConfigurationException($\"'{name}' cannot be defined in both mdsnippets.json and input\");\n        }\n\n        if (input != null)\n        {\n            return input;\n        }\n\n        return config;\n    }\n}"
  },
  {
    "path": "src/ConfigReader/ConfigInput.cs",
    "content": "﻿public class ConfigInput\n{\n    public bool? ReadOnly { get; init; }\n    public bool? ValidateContent { get; init; }\n    public bool? OmitSnippetLinks { get; init; }\n    public LinkFormat? LinkFormat { get; init; }\n    public DocumentConvention? Convention { get; init; }\n    public int? TocLevel { get; init; }\n    public int? MaxWidth { get; init; }\n    public List<string> UrlsAsSnippets { get; init; } = [];\n    public List<string> ExcludeDirectories { get; init; } = [];\n    public List<string> ExcludeMarkdownDirectories { get; init; } = [];\n    public List<string> ExcludeSnippetDirectories { get; init; } = [];\n    public List<string> ExcludeSnippetFiles { get; init; } = [];\n    public bool? WriteHeader { get; init; }\n    public string? Header { get; init; }\n    public string? UrlPrefix { get; init; }\n    public List<string> TocExcludes { get; init; } = [];\n    public bool? TreatMissingAsWarning { get; init; }\n}"
  },
  {
    "path": "src/ConfigReader/ConfigReader.cs",
    "content": "﻿public static class ConfigReader\n{\n    public static (ConfigInput? config, string path) Read(string directory)\n    {\n        var exists = TryFindConfigFile(directory, out var path);\n        if (!exists)\n        {\n            return (null, path);\n        }\n\n        return (Parse(File.ReadAllText(path), path), path);\n    }\n\n    static bool TryFindConfigFile(string directory, out string path)\n    {\n        path = Path.Combine(directory, \"mdsnippets.json\");\n        var exists = File.Exists(path);\n        if (exists)\n        {\n            return true;\n        }\n\n        foreach (var subDirectory in Directory.EnumerateDirectories(directory))\n        {\n            path = Path.Combine(subDirectory, \"mdsnippets.json\");\n            exists = File.Exists(path);\n            if (exists)\n            {\n                return true;\n            }\n        }\n\n        path = \"\";\n        return false;\n    }\n\n    public static ConfigInput Parse(string contents, string path)\n    {\n        var config = DeSerialize(contents);\n\n        return new()\n        {\n            WriteHeader = config.WriteHeader,\n            ReadOnly = config.ReadOnly,\n            ValidateContent = config.ValidateContent,\n            OmitSnippetLinks = config.OmitSnippetLinks,\n            UrlsAsSnippets = config.UrlsAsSnippets,\n            ExcludeDirectories = config.ExcludeDirectories,\n            ExcludeMarkdownDirectories = config.ExcludeMarkdownDirectories,\n            ExcludeSnippetDirectories = config.ExcludeSnippetDirectories,\n            ExcludeSnippetFiles = config.ExcludeSnippetFiles,\n            Header = config.Header,\n            UrlPrefix = config.UrlPrefix,\n            TocExcludes = config.TocExcludes,\n            TocLevel = config.TocLevel,\n            MaxWidth = config.MaxWidth,\n            LinkFormat = GetLinkFormat(config.LinkFormat, path),\n            Convention = GetConvention(config.Convention, path),\n            TreatMissingAsWarning = config.TreatMissingAsWarning,\n        };\n    }\n\n    static ConfigSerialization DeSerialize(string contents)\n    {\n        using var stream = new MemoryStream(Encoding.UTF8.GetBytes(contents))\n        {\n            Position = 0\n        };\n        var serializer = new DataContractJsonSerializer(typeof(ConfigSerialization));\n        try\n        {\n            return (ConfigSerialization) serializer.ReadObject(stream)!;\n        }\n        catch (SerializationException exception)\n        {\n            throw new SnippetException(\n                $\"\"\"\n                 Failed to deserialize configuration. Error: {exception.Message}.\n                 Content:\n                 {contents}\n                 \"\"\");\n        }\n    }\n\n    static DocumentConvention? GetConvention(string? value, string path)\n    {\n        if (value == null)\n        {\n            return null;\n        }\n\n        if (Enum.TryParse<DocumentConvention>(value, out var convention))\n        {\n            return convention;\n        }\n\n        throw new ConfigurationException($\"Failed to parse DocumentConvention: {convention}. FilePath: {path}.\");\n    }\n\n    static LinkFormat? GetLinkFormat(string? value, string path)\n    {\n        if (value == null)\n        {\n            return null;\n        }\n\n        if (!Enum.TryParse<LinkFormat>(value, out var linkFormat))\n        {\n            throw new ConfigurationException($\"Failed to parse LinkFormat: {linkFormat}. FilePath: {path}.\");\n        }\n\n        return linkFormat;\n    }\n}"
  },
  {
    "path": "src/ConfigReader/ConfigReader.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFrameworks>netstandard2.0;netstandard2.1;net48;net8.0;net9.0</TargetFrameworks>\n    <GeneratePackageOnBuild>false</GeneratePackageOnBuild>\n    <GenerateDocumentationFile>false</GenerateDocumentationFile>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\MarkdownSnippets\\MarkdownSnippets.csproj\" />\n    <PackageReference Include=\"ProjectDefaults\" PrivateAssets=\"all\" />\n  </ItemGroup>\n\n</Project>"
  },
  {
    "path": "src/ConfigReader/ConfigResult.cs",
    "content": "﻿public class ConfigResult\n{\n    public bool? ReadOnly { get; init; }\n    public bool ValidateContent { get; init; }\n    public bool OmitSnippetLinks { get; init; }\n    public LinkFormat LinkFormat { get; init; }\n    public DocumentConvention Convention { get; init; }\n    public int TocLevel { get; init; }\n    public int MaxWidth { get; init; }\n    public List<string>? UrlsAsSnippets { get; init; }\n    public List<string>? ExcludeDirectories { get; init; }\n    public List<string>? ExcludeMarkdownDirectories { get; init; }\n    public List<string>? ExcludeSnippetDirectories { get; init; }\n    public List<string>? ExcludeSnippetFiles { get; init; }\n    public bool? WriteHeader { get; init; }\n    public string? Header { get; init; }\n    public string? UrlPrefix { get; init; }\n    public List<string>? TocExcludes { get; init; }\n    public bool TreatMissingAsWarning { get; init; }\n}"
  },
  {
    "path": "src/ConfigReader/ConfigSerialization.cs",
    "content": "﻿public class ConfigSerialization\n{\n    public bool? ReadOnly { get; set; }\n    public bool? ValidateContent { get; set; }\n    public bool? OmitSnippetLinks { get; set; }\n    public string? LinkFormat { get; set; }\n    public string? Convention { get; set; }\n    public bool? WriteHeader { get; set; }\n    public string? Header { get; set; }\n    public string? UrlPrefix { get; set; }\n    public int? TocLevel { get; set; }\n    public int? MaxWidth { get; set; }\n    public List<string> UrlsAsSnippets { get; set; } = [];\n    public List<string> ExcludeDirectories { get; set; } = [];\n    public List<string> ExcludeMarkdownDirectories { get; set; } = [];\n    public List<string> ExcludeSnippetDirectories { get; set; } = [];\n    public List<string> ExcludeSnippetFiles { get; set; } = [];\n    public List<string> TocExcludes { get; set; } = [];\n    public bool? TreatMissingAsWarning { get; set; }\n}"
  },
  {
    "path": "src/ConfigReader/ConfigurationException.cs",
    "content": "﻿class ConfigurationException(string message) :\n    Exception(message);"
  },
  {
    "path": "src/ConfigReader/ExcludeToFilterBuilder.cs",
    "content": "static class ExcludeToFilterBuilder\n{\n    public static ShouldIncludeDirectory ExcludesToFilter(List<string>? excludes) =>\n        path =>\n        {\n            if (DefaultDirectoryExclusions.ShouldExcludeDirectory(path))\n            {\n                return false;\n            }\n\n            if(excludes == null ||\n               excludes.Count == 0)\n            {\n                return true;\n            }\n\n            if (!excludes.Any(path.Contains))\n            {\n                return true;\n            }\n\n            if (!excludes.Any(path.Replace('\\\\', '/').Contains))\n            {\n                return true;\n            }\n\n            if (!excludes.Any(path.Replace('/', '\\\\').Contains))\n            {\n                return true;\n            }\n\n            return false;\n        };\n\n    public static ShouldIncludeFile? FileExcludesToFilter(List<string>? excludes)\n    {\n        if (excludes == null ||\n            excludes.Count == 0)\n        {\n            return null;\n        }\n\n        var regexes = excludes\n            .Select(GlobToRegex)\n            .ToArray();\n\n        return path =>\n        {\n            var name = Path.GetFileName(path);\n            foreach (var regex in regexes)\n            {\n                if (regex.IsMatch(name))\n                {\n                    return false;\n                }\n            }\n\n            return true;\n        };\n    }\n\n    static Regex GlobToRegex(string glob)\n    {\n        var builder = new StringBuilder(\"^\");\n        foreach (var c in glob)\n        {\n            switch (c)\n            {\n                case '*':\n                    builder.Append(\".*\");\n                    break;\n                case '?':\n                    builder.Append('.');\n                    break;\n                default:\n                    builder.Append(Regex.Escape(c.ToString()));\n                    break;\n            }\n        }\n\n        builder.Append('$');\n        return new(builder.ToString(), RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);\n    }\n}"
  },
  {
    "path": "src/ConfigReader/LogBuilder.cs",
    "content": "static class LogBuilder\n{\n    public static string BuildConfigLogMessage(string targetDirectory, ConfigResult config, string configFilePath)\n    {\n        var builder = new StringBuilder(\n            $\"\"\"\n             Config:\n                 TargetDirectory: {targetDirectory}\n                 UrlPrefix: {config.UrlPrefix}\n                 LinkFormat: {config.LinkFormat}\n                 Convention: {config.Convention}\n                 TocLevel: {config.TocLevel}\n                 ValidateContent: {config.ValidateContent}\n                 OmitSnippetLinks: {config.OmitSnippetLinks}\n                 TreatMissingAsWarning: {config.TreatMissingAsWarning}\n                 FileConfigPath: {configFilePath} (exists:{File.Exists(configFilePath)})\n\n             \"\"\");\n\n        if (config.Convention == DocumentConvention.SourceTransform)\n        {\n            var header = GetHeader(config);\n            Polyfill.AppendLine(\n                builder,\n                $\"\"\"\n                     ReadOnly: {config.ReadOnly}\n                     WriteHeader: {config.WriteHeader}\n                     Header: {header}\n                 \"\"\");\n        }\n\n        var maxWidth = config.MaxWidth;\n        if (maxWidth != int.MaxValue && maxWidth != 0)\n        {\n            Polyfill.AppendLine(\n                builder,\n                $\"    MaxWidth: {maxWidth}\");\n        }\n\n        var excludeDirectories = config.ExcludeDirectories;\n        if (excludeDirectories != null && excludeDirectories.Count != 0)\n        {\n            Polyfill.AppendLine(\n                builder,\n                $\"\"\"\n                     ExcludeDirectories:\n                         {string.Join(\"\\r\\n        \", excludeDirectories)}\n                 \"\"\");\n        }\n\n        var excludeMarkdownDirectories = config.ExcludeMarkdownDirectories;\n        if (excludeMarkdownDirectories != null && excludeMarkdownDirectories.Count != 0)\n        {\n            Polyfill.AppendLine(\n                builder,\n                $\"\"\"\n                     ExcludeMarkdownDirectories:\n                         {string.Join(\"\\r\\n        \", excludeMarkdownDirectories)}\n                 \"\"\");\n        }\n\n        var excludeSnippetDirectories = config.ExcludeSnippetDirectories;\n        if (excludeSnippetDirectories != null && excludeSnippetDirectories.Count != 0)\n        {\n            Polyfill.AppendLine(\n                builder,\n                $\"\"\"\n                     ExcludeSnippetDirectories:\n                         {string.Join(\"\\r\\n        \", excludeSnippetDirectories)}\n                 \"\"\");\n        }\n\n        var excludeSnippetFiles = config.ExcludeSnippetFiles;\n        if (excludeSnippetFiles != null && excludeSnippetFiles.Count != 0)\n        {\n            Polyfill.AppendLine(\n                builder,\n                $\"\"\"\n                     ExcludeSnippetFiles:\n                         {string.Join(\"\\r\\n        \", excludeSnippetFiles)}\n                 \"\"\");\n        }\n\n        var tocExcludes = config.TocExcludes;\n        if (tocExcludes != null && tocExcludes.Count != 0)\n        {\n            Polyfill.AppendLine(\n                builder,\n                $\"\"\"\n                     TocExcludes:\n                         {string.Join(\"\\r\\n        \", tocExcludes)}\n                 \"\"\");\n        }\n\n        var urlsAsSnippets = config.UrlsAsSnippets;\n        if (urlsAsSnippets != null && urlsAsSnippets.Count != 0)\n        {\n            Polyfill.AppendLine(\n                builder,\n                $\"\"\"\n                     UrlsAsSnippets:\n                         {string.Join(\"\\r\\n        \", urlsAsSnippets)}\n                 \"\"\");\n        }\n\n#if NET48\n        builder.AppendLine(\"    TargetFramework: net48\");\n#endif\n#if NET9_0\n        builder.AppendLine(\"    TargetFramework: net9.0\");\n#endif\n        builder.TrimEnd();\n        return builder.ToString();\n    }\n\n    static string? GetHeader(ConfigResult config)\n    {\n        var header = config.Header;\n        if (header == null)\n        {\n            return null;\n        }\n\n        var newlineIndent = $\"{Environment.NewLine}        \";\n        header = string.Join(newlineIndent, header.Lines());\n        header = header.Replace(@\"\\n\", newlineIndent);\n        return $\"\"\"\n\n                        {header}\n                \"\"\";\n    }\n}"
  },
  {
    "path": "src/ConfigReader/SharedGlobalUsings.cs",
    "content": "﻿global using Polyfills;\nglobal using MarkdownSnippets;\nglobal using System.Runtime.Serialization;\nglobal using System.Runtime.Serialization.Json;\nglobal using System.Text;\nglobal using System.Text.RegularExpressions;\n"
  },
  {
    "path": "src/ConfigReader.Tests/ConfigReader.Tests.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net9.0</TargetFramework>\n    <OutputType>Exe</OutputType>\n    <NoWarn>$(NoWarn);xUnit1051</NoWarn>\n    <RootNamespace>testing</RootNamespace>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\ConfigReader\\ConfigReader.csproj\" />\n    <PackageReference Include=\"ProjectFiles\" />\n    <PackageReference Include=\"Verify.DiffPlex\" />\n    <PackageReference Include=\"Verify.XunitV3\" />\n    <PackageReference Include=\"Argon\" />\n    <PackageReference Include=\"DiffEngine\" />\n    <PackageReference Include=\"EmptyFiles\" />\n    <PackageReference Include=\"SimpleInfoName\" />\n    <PackageReference Include=\"Verify\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\" PrivateAssets=\"all\" />\n    <PackageReference Include=\"xunit.v3\" />\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" />\n    <PackageReference Include=\"ProjectDefaults\" PrivateAssets=\"all\" />\n    <None Update=\"allConfig.json\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n    <None Update=\"SourceTransform.json\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n\n    <None Update=\"InPlaceOverwrite.json\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "src/ConfigReader.Tests/ConfigReaderTests.BadJson.verified.txt",
    "content": "﻿{\n  Type: SnippetException,\n  Message:\nFailed to deserialize configuration. Error: There was an error deserializing the object of type ConfigSerialization. Encountered unexpected character '\"'..\nContent:\n{\n  \"ValidateContent\": true\n  \"Convention\": \"InPlaceOverwrite\"\n},\n  StackTrace:\nat ConfigReader.DeSerialize(String contents)\nat ConfigReader.Parse(String contents, String path)\nat ConfigReaderTests.<>c.<BadJson>b__1_0()\n}"
  },
  {
    "path": "src/ConfigReader.Tests/ConfigReaderTests.Empty.verified.txt",
    "content": "﻿{}"
  },
  {
    "path": "src/ConfigReader.Tests/ConfigReaderTests.Values.verified.txt",
    "content": "﻿{\n  ReadOnly: false,\n  ValidateContent: true,\n  OmitSnippetLinks: true,\n  LinkFormat: Tfs,\n  Convention: InPlaceOverwrite,\n  TocLevel: 3,\n  MaxWidth: 80,\n  UrlsAsSnippets: [\n    Url1,\n    Url2\n  ],\n  ExcludeDirectories: [\n    Dir1,\n    Dir2\n  ],\n  ExcludeMarkdownDirectories: [\n    Dir2,\n    Dir3\n  ],\n  ExcludeSnippetDirectories: [\n    Dir4,\n    Dir5\n  ],\n  ExcludeSnippetFiles: [\n    *.verified.txt,\n    *.received.txt\n  ],\n  WriteHeader: true,\n  Header: GENERATED FILE - Source File: {relativePath},\n  UrlPrefix: TheUrlPrefix,\n  TocExcludes: [\n    Exclude Heading1,\n    Exclude Heading2\n  ],\n  TreatMissingAsWarning: true\n}"
  },
  {
    "path": "src/ConfigReader.Tests/ConfigReaderTests.cs",
    "content": "﻿public class ConfigReaderTests\n{\n    [Fact]\n    public Task Empty()\n    {\n        var config = ConfigReader.Parse(\"{}\", \"filePath\");\n\n        return Verify(config);\n    }\n\n    [Fact]\n    public Task BadJson() =>\n        Throws(() => ConfigReader.Parse(\n            \"\"\"\n            {\n              \"ValidateContent\": true\n              \"Convention\": \"InPlaceOverwrite\"\n            }\n            \"\"\",\n            \"filePath\"));\n\n    [Fact]\n    public Task Values()\n    {\n        var stream = File.ReadAllText(\"allConfig.json\");\n        var config = ConfigReader.Parse(stream, \"filePath\");\n        return Verify(config);\n    }\n\n    [Fact]\n    public void FileExcludesToFilter_NullOrEmpty_ReturnsNull()\n    {\n        Assert.Null(ExcludeToFilterBuilder.FileExcludesToFilter(null));\n        Assert.Null(ExcludeToFilterBuilder.FileExcludesToFilter([]));\n    }\n\n    [Fact]\n    public void FileExcludesToFilter_GlobMatching()\n    {\n        var filter = ExcludeToFilterBuilder.FileExcludesToFilter(\n        [\n            \"*.verified.txt\",\n            \"*.received.*\",\n            \"ignore?.cs\"\n        ])!;\n\n        Assert.False(filter(\"/a/b/Foo.verified.txt\"));\n        Assert.False(filter(@\"C:\\x\\y\\Foo.received.md\"));\n        Assert.False(filter(\"ignore1.cs\"));\n        Assert.True(filter(\"Foo.cs\"));\n        Assert.True(filter(\"Foo.txt\"));\n        Assert.True(filter(\"ignoreAB.cs\"));\n    }\n\n    [Fact]\n    public void FileExcludesToFilter_CaseInsensitive()\n    {\n        var filter = ExcludeToFilterBuilder.FileExcludesToFilter([\"*.VERIFIED.txt\"])!;\n        Assert.False(filter(\"foo.verified.TXT\"));\n    }\n}"
  },
  {
    "path": "src/ConfigReader.Tests/GlobalUsings.cs",
    "content": "﻿global using VerifyTests.DiffPlex;"
  },
  {
    "path": "src/ConfigReader.Tests/InPlaceOverwrite.json",
    "content": "﻿{\n  \"$schema\": \"https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/schema.json\",\n  \"Convention\": \"InPlaceOverwrite\"\n}"
  },
  {
    "path": "src/ConfigReader.Tests/ModuleInitializer.cs",
    "content": "﻿public static class ModuleInitializer\n{\n    [ModuleInitializer]\n    public static void Initialize() =>\n        VerifyDiffPlex.Initialize(OutputType.Compact);\n}"
  },
  {
    "path": "src/ConfigReader.Tests/SourceTransform.json",
    "content": "﻿{\n  \"$schema\": \"https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/schema.json\",\n  \"Convention\": \"SourceTransform\"\n}"
  },
  {
    "path": "src/ConfigReader.Tests/allConfig.json",
    "content": "﻿{\n  \"$schema\": \"https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/schema.json\",\n  \"ReadOnly\": false,\n  \"LinkFormat\": \"Tfs\",\n  \"TocLevel\": 3,\n  \"ExcludeDirectories\": [ \"Dir1\", \"Dir2\" ],\n  \"ExcludeMarkdownDirectories\": [ \"Dir2\", \"Dir3\" ],\n  \"ExcludeSnippetDirectories\": [ \"Dir4\", \"Dir5\" ],\n  \"ExcludeSnippetFiles\": [ \"*.verified.txt\", \"*.received.txt\" ],\n  \"UrlsAsSnippets\": [ \"Url1\", \"Url2\" ],\n  \"TocExcludes\": [ \"Exclude Heading1\", \"Exclude Heading2\" ],\n  \"Convention\": \"InPlaceOverwrite\",\n  \"WriteHeader\": true,\n  \"MaxWidth\": 80,\n  \"Header\": \"GENERATED FILE - Source File: {relativePath}\",\n  \"UrlPrefix\": \"TheUrlPrefix\",\n  \"TreatMissingAsWarning\": true,\n  \"ValidateContent\": true,\n  \"OmitSnippetLinks\": true\n}"
  },
  {
    "path": "src/Directory.Build.props",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project>\n  <PropertyGroup>\n    <NoWarn>CS1591;NU1608;NU1109</NoWarn>\n    <Version>28.3.0</Version>\n    <LangVersion>preview</LangVersion>\n    <AssemblyVersion>1.0.0</AssemblyVersion>\n    <PackageTags>Markdown, Snippets, mdsnippets, documentation, MarkdownSnippets</PackageTags>\n    <Description>Extracts snippets from code files and merges them into markdown documents.</Description>\n    <ResolveAssemblyReferencesSilent>true</ResolveAssemblyReferencesSilent>\n    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>\n    <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>\n    <SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>\n    <PolyStringInterpolation>true</PolyStringInterpolation>\n    <PackageReadmeFile>readme.md</PackageReadmeFile>\n  </PropertyGroup>\n  <ItemGroup>\n    <None Include=\"$(MSBuildThisFileDirectory)nuget-readme.md\" Pack=\"true\" PackagePath=\"readme.md\" />\n    <Using Include=\"System.ReadOnlySpan&lt;System.Char&gt;\" Alias=\"CharSpan\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/Directory.Packages.props",
    "content": "<Project>\n  <PropertyGroup>\n    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>\n    <CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>\n  </PropertyGroup>\n  <ItemGroup>\n    <PackageVersion Include=\"Argon\" Version=\"0.33.5\" />\n    <PackageVersion Include=\"BenchmarkDotNet\" Version=\"0.15.8\" />\n    <PackageVersion Include=\"CommandLineParser\" Version=\"2.9.1\" />\n    <PackageVersion Include=\"Microsoft.Build.Tasks.Core\" Version=\"18.4.0\" />\n    <PackageVersion Include=\"Microsoft.NET.Test.Sdk\" Version=\"18.5.0\" />\n    <PackageVersion Include=\"Microsoft.Sbom.Targets\" Version=\"4.1.5\" />\n    <PackageVersion Include=\"PackageShader.MsBuild\" Version=\"1.1.0\" />\n    <PackageVersion Include=\"Polyfill\" Version=\"10.3.0\" />\n    <PackageVersion Include=\"ProjectDefaults\" Version=\"1.0.173\" />\n    <PackageVersion Include=\"ProjectFiles\" Version=\"1.1.1\" />\n    <PackageVersion Include=\"System.Collections.Immutable\" Version=\"10.0.7\" />\n    <PackageVersion Include=\"System.Buffers\" Version=\"4.6.1\" />\n    <PackageVersion Include=\"System.Runtime.CompilerServices.Unsafe\" Version=\"6.1.2\" />\n    <PackageVersion Include=\"System.Numerics.Vectors\" Version=\"4.6.1\" />\n    <PackageVersion Include=\"System.Security.Cryptography.Xml\" Version=\"10.0.7\" />\n    <PackageVersion Include=\"System.Threading.Tasks.Extensions\" Version=\"4.6.3\" />\n    <PackageVersion Include=\"System.Memory\" Version=\"4.6.3\" />\n    <PackageVersion Include=\"System.Net.Http\" Version=\"4.3.4\" />\n    <PackageVersion Include=\"Verify.DiffPlex\" Version=\"3.1.2\" />\n    <PackageVersion Include=\"Verify.XunitV3\" Version=\"31.16.2\" />\n    <PackageVersion Include=\"xunit.v3\" Version=\"3.2.2\" />\n    <PackageVersion Include=\"xunit.runner.visualstudio\" Version=\"3.1.5\" />\n    <PackageVersion Include=\"DiffEngine\" Version=\"19.1.3\" />\n    <PackageVersion Include=\"EmptyFiles\" Version=\"8.18.1\" />\n    <PackageVersion Include=\"SimpleInfoName\" Version=\"3.2.0\" />\n    <PackageVersion Include=\"Verify\" Version=\"31.16.2\" />\n  </ItemGroup>\n</Project>"
  },
  {
    "path": "src/MarkdownSnippets/AssemblyInfo.cs",
    "content": "﻿[assembly: InternalsVisibleTo(\"Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e191859fcd1deee68b96927c170783ced0c9a471a6424a0a011cfd31156a49dd73c4ad4a88b995fb918c0b43e0c005ef5fb72d53a328a64bde825cb5f2e4c53d66f69fcbb87d6737128b98e677a42091974b5f56093123a2dd6bc738af751b101d41c4f7a996e217b61967a3aa1ae7bc791d19c1cbeef47f0cdd20d288dff1a3\")]\n[assembly: InternalsVisibleTo(\"mdsnippets, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e191859fcd1deee68b96927c170783ced0c9a471a6424a0a011cfd31156a49dd73c4ad4a88b995fb918c0b43e0c005ef5fb72d53a328a64bde825cb5f2e4c53d66f69fcbb87d6737128b98e677a42091974b5f56093123a2dd6bc738af751b101d41c4f7a996e217b61967a3aa1ae7bc791d19c1cbeef47f0cdd20d288dff1a3\")]\n[assembly: InternalsVisibleTo(\"MarkdownSnippets.MsBuild, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e191859fcd1deee68b96927c170783ced0c9a471a6424a0a011cfd31156a49dd73c4ad4a88b995fb918c0b43e0c005ef5fb72d53a328a64bde825cb5f2e4c53d66f69fcbb87d6737128b98e677a42091974b5f56093123a2dd6bc738af751b101d41c4f7a996e217b61967a3aa1ae7bc791d19c1cbeef47f0cdd20d288dff1a3\")]\n[assembly: InternalsVisibleTo(\"ConfigReader, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e191859fcd1deee68b96927c170783ced0c9a471a6424a0a011cfd31156a49dd73c4ad4a88b995fb918c0b43e0c005ef5fb72d53a328a64bde825cb5f2e4c53d66f69fcbb87d6737128b98e677a42091974b5f56093123a2dd6bc738af751b101d41c4f7a996e217b61967a3aa1ae7bc791d19c1cbeef47f0cdd20d288dff1a3\")]"
  },
  {
    "path": "src/MarkdownSnippets/ContentValidation.cs",
    "content": "static class ContentValidation\n{\n    static FrozenDictionary<string, string> phrases = FrozenDictionary.Create<string, string>([\n        new(\"a majority of \", \"most\"),\n        new(\"a number of\", \"some or many\"),\n        new(\"at an early date\", \"soon\"),\n        new(\"at the conclusion of\", \"after or following\"),\n        new(\"at the present time\", \"now\"),\n        new(\"at this point in time\", \"now\"),\n        new(\"based on the fact that\", \"because or since\"),\n        new(\"despite the fact that\", \"although\"),\n        new(\"due to the fact that\", \"because\"),\n        new(\"during the course of\", \"during\"),\n        new(\"during the time that\", \"during or while\"),\n        new(\"have the capability to\", \"can\"),\n        new(\"in connection with\", \"about\"),\n        new(\"in order to\", \"to\"),\n        new(\"in regard to \", \"regarding or about\"),\n        new(\"in the event of\", \"if\"),\n        new(\"in view of the fact that\", \"because\"),\n        new(\"it is often the case that\", \"often\"),\n        new(\"make reference to \", \"refer to\"),\n        new(\"of the opinion that\", \"think that \"),\n        new(\"on a daily basis\", \"daily\"),\n        new(\"on the grounds that\", \"because\"),\n        new(\"prior to\", \"before\"),\n        new(\"so as to\", \"to\"),\n        new(\"subsequent to\", \"after\"),\n        new(\"take into consideration\", \"consider\"),\n        new(\"until such time as\", \"until\"),\n        new(\"a lot\", \"many\"),\n        new(\"sort of\", \"similar or approximately\"),\n        new(\"kind of\", \"similar or approximately \")\n    ]);\n\n    static FrozenSet<string> invalidWordSet = new[]\n    {\n        \"you\",\n        \"we\",\n        \"our\",\n        \"your\",\n        \"us\",\n        \"please\",\n        \"yourself\",\n        \"just\",\n        \"simply\",\n        \"simple\",\n        \"easy\",\n        \"feel\",\n        \"think\",\n        \"above-mentioned\",\n        \"aforementioned\",\n        \"foregoing\",\n        \"henceforth\",\n        \"hereafter\",\n        \"heretofore\",\n        \"herewith\",\n        \"thereafter\",\n        \"thereof\",\n        \"therewith\",\n        \"whatsoever\",\n        \"whereat\",\n        \"wherein\",\n        \"whereof\"\n    }.ToFrozenSet();\n\n    static FrozenDictionary<string, KeyValuePair<string, string>[]> phrasesByFirstWord =\n        phrases\n            .GroupBy(p =>\n            {\n                var spaceIndex = p.Key.IndexOf(' ');\n                return spaceIndex == -1 ? p.Key : p.Key[..spaceIndex];\n            })\n            .ToFrozenDictionary(g => g.Key, g => g.ToArray());\n\n    public static IEnumerable<(string error, int column)> Verify(string line)\n    {\n        if (line.StartsWith('>'))\n        {\n            yield break;\n        }\n\n        var cleanedLine = Clean(line);\n\n        var message = \"No exclamation marks. If a statement is important make it bold. https://www.technicalcommunicationcenter.com/2011/12/30/the-discipline-of-punctuation-in-technical-writing/. \";\n        var exclamationIndex1 = cleanedLine.IndexOf(\"! \");\n        if (exclamationIndex1 != -1)\n        {\n            yield return (message, exclamationIndex1);\n        }\n\n        // Tokenize words with positions\n        var words = Tokenize(cleanedLine);\n\n        // Check invalid words via set lookup (report first occurrence only)\n        var seenWords = new HashSet<string>();\n        foreach (var (word, start) in words)\n        {\n            if (invalidWordSet.Contains(word) && seenWords.Add(word))\n            {\n                yield return ($\"Invalid word detected: '{word}'\", start - 1);\n            }\n        }\n\n        // Check phrases via first-word lookup (report first occurrence only)\n        var seenPhrases = new HashSet<string>();\n        foreach (var (word, start) in words)\n        {\n            if (phrasesByFirstWord.TryGetValue(word, out var candidates))\n            {\n                foreach (var candidate in candidates)\n                {\n                    if (seenPhrases.Contains(candidate.Key))\n                    {\n                        continue;\n                    }\n\n                    if (cleanedLine.AsSpan(start).StartsWith(candidate.Key.AsSpan(), StringComparison.Ordinal))\n                    {\n                        seenPhrases.Add(candidate.Key);\n                        yield return ($\"Invalid phrase detected: '{candidate.Key}'. Instead consider '{candidate.Value}'\", start);\n                    }\n                }\n            }\n        }\n    }\n\n    static List<(string word, int start)> Tokenize(string cleanedLine)\n    {\n        var words = new List<(string word, int start)>();\n        var span = cleanedLine.AsSpan();\n        var pos = 0;\n        while (pos < span.Length)\n        {\n            if (span[pos] == ' ')\n            {\n                pos++;\n                continue;\n            }\n\n            var wordStart = pos;\n            while (pos < span.Length && span[pos] != ' ')\n            {\n                pos++;\n            }\n\n            words.Add((span[wordStart..pos].ToString(), wordStart));\n        }\n\n        return words;\n    }\n\n    static string Clean(string input)\n    {\n        var length = input.Length + 2; // +2 for leading and trailing spaces\n        return string.Create(length, input, (span, source) =>\n        {\n            span[0] = ' ';\n            var index = 1;\n            foreach (var ch in source)\n            {\n                if (ch is '\\'' or '?' or '.' or ',')\n                {\n                    span[index++] = ' ';\n                }\n                else\n                {\n                    span[index++] = char.ToLowerInvariant(ch);\n                }\n            }\n            span[index] = ' ';\n        });\n    }\n}\n"
  },
  {
    "path": "src/MarkdownSnippets/ContentValidationException.cs",
    "content": "namespace MarkdownSnippets;\n\npublic class ContentValidationException(IReadOnlyList<ValidationError> errors) :\n    SnippetException(BuildMessage(errors))\n{\n    public IReadOnlyList<ValidationError> Errors { get; } = errors;\n\n    static string BuildMessage(IReadOnlyList<ValidationError> errors)\n    {\n        var builder = new StringBuilder(\"Content validation errors:\");\n        builder.AppendLine();\n        foreach (var error in errors)\n        {\n            if (error.File == null)\n            {\n                Polyfill.AppendLine(\n                    builder,\n                    $\"\"\"\n                     {error.Error}\n                       Line: {error.Line}\n                       Column: {error.Column}\n                     \"\"\");\n            }\n\n            Polyfill.AppendLine(\n                builder,\n                $\"\"\"\n                 {error.Error}\n                   File: {error.File}\n                   Line: {error.Line}\n                   Column: {error.Column}\n                 \"\"\");\n        }\n\n        return builder.ToString();\n    }\n\n    public override string ToString() => Message;\n}"
  },
  {
    "path": "src/MarkdownSnippets/Downloader/Downloader.cs",
    "content": "﻿static class Downloader\n{\n    static string cache = Path.Combine(Path.GetTempPath(), \"MarkdownSnippets\");\n\n    static Downloader()\n    {\n        Directory.CreateDirectory(cache);\n        foreach (var file in new DirectoryInfo(cache)\n                     .GetFiles()\n                     .OrderByDescending(_ => _.LastWriteTime)\n                     .Skip(100))\n        {\n            file.Delete();\n        }\n    }\n\n    static HttpClient httpClient = new()\n    {\n        Timeout = TimeSpan.FromSeconds(30)\n    };\n\n    public static async Task<(bool success, string? path)> DownloadFile(string uri)\n    {\n        var file = Path.Combine(cache, FileNameFromUrl.ConvertToFileName(uri));\n\n        if (File.Exists(file))\n        {\n            var fileTimestamp = Timestamp.GetTimestamp(file);\n            if (fileTimestamp.Expiry > DateTime.UtcNow)\n            {\n                return (true, file);\n            }\n        }\n\n        Timestamp webTimeStamp;\n        using (var request = new HttpRequestMessage(HttpMethod.Head, uri))\n        {\n            using var headResponse = await httpClient.SendAsync(request);\n            if (headResponse.StatusCode != HttpStatusCode.OK)\n            {\n                return (false, null);\n            }\n\n            webTimeStamp = Timestamp.GetTimestamp(headResponse);\n\n            if (File.Exists(file))\n            {\n                var fileTimestamp = Timestamp.GetTimestamp(file);\n                if (fileTimestamp.LastModified == webTimeStamp.LastModified)\n                {\n                    return (true, file);\n                }\n\n                File.Delete(file);\n            }\n        }\n\n        using var response = await httpClient.GetAsync(uri);\n        using var httpStream = await response.Content.ReadAsStreamAsync();\n        using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None))\n        {\n            await httpStream.CopyToAsync(fileStream);\n        }\n\n        webTimeStamp = Timestamp.GetTimestamp(response);\n\n        Timestamp.SetTimestamp(file, webTimeStamp);\n        return (true, file);\n    }\n\n    public static async Task<(bool success, string? content)> DownloadContent(string uri)\n    {\n        var (success, path) = await DownloadFile(uri);\n        if (success)\n        {\n            return (true, await File.ReadAllTextAsync(path!));\n        }\n\n        return (false, null);\n    }\n}"
  },
  {
    "path": "src/MarkdownSnippets/Downloader/FileNameFromUrl.cs",
    "content": "﻿static class FileNameFromUrl\n{\n    static FrozenSet<char> invalid = Path.GetInvalidFileNameChars().Concat(Path.GetInvalidPathChars()).ToFrozenSet();\n\n    public static string ConvertToFileName(string url)\n    {\n        var builder = StringBuilderCache.Acquire(url.Length);\n        foreach (var ch in url)\n        {\n            if (invalid.Contains(ch))\n            {\n                builder.Append('_');\n                continue;\n            }\n\n            builder.Append(ch);\n        }\n\n        return StringBuilderCache.GetStringAndRelease(builder);\n    }\n}\n"
  },
  {
    "path": "src/MarkdownSnippets/Downloader/Timestamp.cs",
    "content": "﻿class Timestamp\n{\n    static DateTime minFileDate = DateTime.FromFileTimeUtc(0);\n    public DateTime? Expiry;\n    public DateTime? LastModified;\n\n    public static Timestamp GetTimestamp(HttpResponseMessage headResponse)\n    {\n        var timestamp = new Timestamp();\n        var headers = headResponse.Content.Headers;\n        if (headers.LastModified != null)\n        {\n            timestamp.LastModified = headers.LastModified.Value.UtcDateTime;\n        }\n\n        if (headers.Expires != null)\n        {\n            timestamp.Expiry = headers.Expires.Value.UtcDateTime;\n        }\n\n        return timestamp;\n    }\n\n    public  static void SetTimestamp(string path, Timestamp timestamp)\n    {\n        File.SetCreationTimeUtc(path, timestamp.LastModified.GetValueOrDefault(DateTime.UtcNow));\n        File.SetLastWriteTimeUtc(path, timestamp.Expiry.GetValueOrDefault(minFileDate));\n    }\n\n    public static Timestamp GetTimestamp(string path)\n    {\n        var timestamp = new Timestamp();\n        var creationTimeUtc = File.GetCreationTimeUtc(path);\n        if (creationTimeUtc != minFileDate)\n        {\n            timestamp.LastModified = creationTimeUtc;\n        }\n\n        var lastWriteTimeUtc = File.GetLastWriteTimeUtc(path);\n        if (lastWriteTimeUtc != minFileDate)\n        {\n            timestamp.Expiry = lastWriteTimeUtc;\n        }\n\n        return timestamp;\n    }\n}"
  },
  {
    "path": "src/MarkdownSnippets/Extensions.cs",
    "content": "static class Extensions\n{\n    public static bool TryFindNewline(this TextReader reader, [NotNullWhen(true)] out string? newline)\n    {\n        do\n        {\n            var c = reader.Read();\n            if (c == -1)\n            {\n                break;\n            }\n\n            if (c == '\\r')\n            {\n                var peek = reader.Peek();\n                if (peek == -1)\n                {\n                    newline = \"\\r\";\n                    return true;\n                }\n\n                if (peek == '\\n')\n                {\n                    newline = \"\\r\\n\";\n                    return true;\n                }\n\n                newline = \"\\r\";\n                return true;\n            }\n\n            if (c == '\\n')\n            {\n                newline = \"\\n\";\n                return true;\n            }\n\n        } while (true);\n\n        newline = null;\n        return false;\n    }\n\n    public static void TrimEnd(this StringBuilder builder)\n    {\n        var i = builder.Length - 1;\n        for (; i >= 0; i--)\n        {\n            if (!char.IsWhiteSpace(builder[i]))\n            {\n                break;\n            }\n        }\n\n        if (i < builder.Length - 1)\n        {\n            builder.Length = i + 1;\n        }\n    }\n\n    public static IReadOnlyList<T> ToReadonlyList<T>(this IEnumerable<T> value) => value.ToList();\n\n    public static int LineCount(this CharSpan input)\n    {\n        var count = 1;\n        var len = input.Length;\n        for (var i = 0; i != len; ++i)\n        {\n            switch (input[i])\n            {\n                case '\\r':\n                    ++count;\n                    if (i + 1 != len && input[i + 1] == '\\n')\n                    {\n                        ++i;\n                    }\n\n                    break;\n                case '\\n':\n                    ++count;\n                    break;\n            }\n        }\n\n        return count;\n    }\n\n    public static int LastIndexOfSequence(this CharSpan value, char c, int max)\n    {\n        var index = 0;\n        while (true)\n        {\n            if (index == max)\n            {\n                return index;\n            }\n\n            if (index == value.Length)\n            {\n                return index;\n            }\n\n            var ch = value[index];\n            if (c != ch)\n            {\n                return index;\n            }\n\n            index++;\n        }\n    }\n\n    public static CharSpan TrimBackCommentChars(this CharSpan input, int startIndex)\n    {\n        for (var index = input.Length - 1; index >= startIndex; index--)\n        {\n            var ch = input[index];\n            if (char.IsLetterOrDigit(ch) || ch is ']' or ' ' or ')')\n            {\n                return input[startIndex..(index + 1)];\n            }\n        }\n\n        return string.Empty;\n    }\n\n    public static string[] Lines(this string value) =>\n        value.Split([\"\\r\\n\", \"\\r\", \"\\n\"], StringSplitOptions.None);\n\n    public static bool IsWhiteSpace(this CharSpan target)\n    {\n        for (var i = 0; i < target.Length; i++)\n        {\n            if (!char.IsWhiteSpace(target[i]))\n            {\n                return false;\n            }\n        }\n\n        return true;\n    }\n}"
  },
  {
    "path": "src/MarkdownSnippets/FileEx.cs",
    "content": "﻿static class FileEx\n{\n    public static string FixFileCapitalization(string file)\n    {\n        var fileName = Path.GetFileName(file);\n        var directory = Path.GetDirectoryName(file);\n        if (string.IsNullOrEmpty(directory))\n        {\n            directory = \".\";\n        }\n\n        var filePaths = Directory.GetFiles(directory, fileName, SearchOption.TopDirectoryOnly);\n        if (filePaths.Length == 0)\n        {\n            throw new FileNotFoundException($\"Could not find file: {file}\");\n        }\n\n        return filePaths[0];\n    }\n\n    public static string GetRelativePath(string file, string directory)\n    {\n        var fileUri = new Uri(file);\n        // Folders must end in a slash\n        if (!directory.EndsWith(Path.DirectorySeparatorChar))\n        {\n            directory += Path.DirectorySeparatorChar;\n        }\n\n        var directoryUri = new Uri(directory);\n        return Uri.UnescapeDataString(directoryUri.MakeRelativeUri(fileUri).ToString().Replace('/', Path.DirectorySeparatorChar));\n    }\n\n    public static string PrependSlash(string path)\n    {\n        if (path.StartsWith('/'))\n        {\n            return path;\n        }\n\n        return $\"/{path}\";\n    }\n\n    public static void ClearReadOnly(string path)\n    {\n        if (!File.Exists(path))\n        {\n            return;\n        }\n\n        var attributes = File.GetAttributes(path);\n        if ((attributes & FileAttributes.ReadOnly) != 0)\n        {\n            File.SetAttributes(path, attributes & ~FileAttributes.ReadOnly);\n        }\n    }\n\n    public static void MakeReadOnly(string path)\n    {\n        var attributes = File.GetAttributes(path);\n        File.SetAttributes(path, attributes | FileAttributes.ReadOnly);\n    }\n}"
  },
  {
    "path": "src/MarkdownSnippets/GitRepoDirectoryFinder.cs",
    "content": "﻿namespace MarkdownSnippets;\n\npublic static class GitRepoDirectoryFinder\n{\n    public static string FindForFilePath([CallerFilePath] string sourceFilePath = \"\")\n    {\n        Guard.FileExists(sourceFilePath, nameof(sourceFilePath));\n        var directory = Path.GetDirectoryName(sourceFilePath)!;\n        return FindForDirectory(directory);\n    }\n\n    public static string FindForDirectory(string directory)\n    {\n        Guard.DirectoryExists(directory, nameof(directory));\n        if (TryFind(directory, out var path))\n        {\n            return path;\n        }\n\n        throw new(\"Could not find git repository directory\");\n    }\n\n    static bool TryFind(string directory, [NotNullWhen(true)] out string? path)\n    {\n        if (TryFind(directory, \".git\", out var targetDirectory))\n        {\n            path = targetDirectory;\n            return true;\n        }\n\n        if (TryFind(directory, \".gitignore\", out targetDirectory))\n        {\n            path = targetDirectory;\n            return true;\n        }\n\n        path = null;\n        return false;\n    }\n\n    public static bool IsInGitRepository(string directory)\n    {\n        Guard.DirectoryExists(directory, nameof(directory));\n        return TryFind(directory, out _);\n    }\n\n    static bool TryFind(string directory, string suffix, [NotNullWhen(true)] out string? path)\n    {\n        Guard.DirectoryExists(directory, nameof(directory));\n\n        do\n        {\n            var combine = Path.Combine(directory, suffix);\n            if (Directory.Exists(combine) ||\n                File.Exists(combine))\n            {\n                path = directory;\n                return true;\n            }\n\n            var parent = Directory.GetParent(directory);\n            if (parent == null)\n            {\n                path = null;\n                return false;\n            }\n\n            directory = parent.FullName;\n        } while (true);\n    }\n}"
  },
  {
    "path": "src/MarkdownSnippets/GlobalUsings.cs",
    "content": "﻿global using System.Collections.Frozen;\nglobal using System.Diagnostics.CodeAnalysis;\nglobal using System.Net;\nglobal using System.Net.Http;\nglobal using System.Text.RegularExpressions;\nglobal using MarkdownSnippets;"
  },
  {
    "path": "src/MarkdownSnippets/Guard.cs",
    "content": "static class Guard\n{\n    public static void AgainstUpperCase(string value, string argumentName)\n    {\n        foreach (var c in value)\n        {\n            if (char.IsUpper(c))\n            {\n                throw new ArgumentException($\"Cannot contain upper case. Value: {value}\", argumentName);\n            }\n        }\n    }\n\n    public static void AgainstNegativeAndZero(int value, string argumentName)\n    {\n        if (value <= 0)\n        {\n            throw new ArgumentOutOfRangeException(argumentName,value, \"Zero or less\");\n        }\n    }\n\n    public static void AgainstNegative(int value, string argumentName)\n    {\n        if (value < 0)\n        {\n            throw new ArgumentOutOfRangeException(argumentName, value, \"negative\");\n        }\n    }\n\n    public static void AgainstNullAndEmpty(string? value, string argumentName)\n    {\n        if (string.IsNullOrWhiteSpace(value))\n        {\n            throw new ArgumentNullException(argumentName);\n        }\n    }\n\n    public static void DirectoryExists(string path, string argumentName)\n    {\n        AgainstNullAndEmpty(path, argumentName);\n        if (!Directory.Exists(path))\n        {\n            throw new ArgumentException($\"Directory does not exist: {path}\", argumentName);\n        }\n    }\n\n    public static void FileExists(string? path, string argumentName)\n    {\n        AgainstNullAndEmpty(path, argumentName);\n        if (!File.Exists(path))\n        {\n            throw new ArgumentException($\"File does not exist: {path}\", argumentName);\n        }\n    }\n\n    public static void AgainstEmpty(string? value, string argumentName)\n    {\n        if (value == null)\n        {\n            return;\n        }\n\n        if (string.IsNullOrWhiteSpace(value))\n        {\n            throw new ArgumentException(\"Cannot be only whitespace.\", argumentName);\n        }\n    }\n}"
  },
  {
    "path": "src/MarkdownSnippets/InterpretErrors.cs",
    "content": "namespace MarkdownSnippets;\n\n/// <summary>\n/// Extension method to convert various error cases.\n/// </summary>\npublic static class InterpretErrors\n{\n    /// <summary>\n    /// Converts <see cref=\"IEnumerable{Snippet}\"/> to a markdown string.\n    /// </summary>\n    public static string ErrorsAsMarkdown(this IReadOnlyList<Snippet> snippets)\n    {\n        if (snippets.Count == 0)\n        {\n            return \"\";\n        }\n\n        var builder = StringBuilderCache.Acquire();\n        builder.AppendLine(\"## Snippet errors\\r\\n\");\n        foreach (var error in snippets)\n        {\n            Polyfill.AppendLine(\n                builder,\n                $\" * {error}\");\n        }\n\n        builder.AppendLine();\n        return StringBuilderCache.GetStringAndRelease(builder);\n    }\n\n    /// <summary>\n    /// Converts <see cref=\"ProcessResult.MissingSnippets\"/> to a markdown string.\n    /// </summary>\n    public static string ErrorsAsMarkdown(this ProcessResult processResult)\n    {\n        var builder = StringBuilderCache.Acquire();\n        var missingSnippets = processResult.MissingSnippets;\n        if (missingSnippets.Count != 0)\n        {\n            builder.Append(\n                \"\"\"\n                ## Missing snippets\n\n                \"\"\");\n            foreach (var error in missingSnippets)\n            {\n                Polyfill.AppendLine(\n                    builder,\n                    $\" * Key:'{error.Key}' Line:'{error.LineNumber}'\");\n            }\n        }\n        //TODO: handle other errors\n\n        builder.AppendLine();\n        return StringBuilderCache.GetStringAndRelease(builder);\n    }\n}"
  },
  {
    "path": "src/MarkdownSnippets/KeyValidator.cs",
    "content": "﻿static class KeyValidator\n{\n    public static bool IsValidKey(CharSpan key)\n    {\n        if (key.Length == 0)\n        {\n            return false;\n        }\n\n        if (!char.IsLetterOrDigit(key[0]))\n        {\n            return false;\n        }\n\n        if (!char.IsLetterOrDigit(key[^1]))\n        {\n            return false;\n        }\n\n        return true;\n    }\n}"
  },
  {
    "path": "src/MarkdownSnippets/MarkdownProcessingException.cs",
    "content": "namespace MarkdownSnippets;\n\npublic class MarkdownProcessingException :\n    SnippetException\n{\n    public string? File { get; }\n    public int LineNumber { get; }\n\n    public MarkdownProcessingException(string message, string? file, int lineNumber) :\n        base($\"{message} File: {file}. LineNumber: {lineNumber}.\")\n    {\n        Guard.AgainstNegativeAndZero(lineNumber, nameof(lineNumber));\n        Guard.AgainstEmpty(file, nameof(file));\n        File = file;\n        LineNumber = lineNumber;\n    }\n\n    public override string ToString() => Message;\n}"
  },
  {
    "path": "src/MarkdownSnippets/MarkdownSnippets.csproj",
    "content": "\n<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFrameworks>netstandard2.0;netstandard2.1;net48;net8.0;net9.0;net10.0</TargetFrameworks>\n  </PropertyGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.Sbom.Targets\" PrivateAssets=\"all\" Condition=\"'$(CI)' == 'true'\" />\n    <PackageReference Include=\"ProjectDefaults\" PrivateAssets=\"all\" />\n    <PackageReference Include=\"Polyfill\" PrivateAssets=\"all\" />\n    <PackageReference Include=\"System.Collections.Immutable\"  Condition=\"'$(TargetFramework)' != 'net10.0'\"/>\n    <PackageReference Include=\"System.Memory\" Condition=\"'$(TargetFrameworkIdentifier)' == '.NETStandard' OR '$(TargetFrameworkIdentifier)' == '.NETFramework' OR '$(TargetFrameworkIdentifier)' == '.NETCOREAPP'\" />\n    <PackageReference Include=\"System.Net.Http\" Condition=\"'$(TargetFramework)' == 'net48'\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/MarkdownSnippets/MissingIncludesException.cs",
    "content": "namespace MarkdownSnippets;\n\npublic class MissingIncludesException(IReadOnlyList<MissingInclude> missing) :\n    SnippetException($\"Missing includes: {string.Join(\", \", missing.Select(_ => _.Key))}\")\n{\n    public IReadOnlyList<MissingInclude> Missing { get; } = missing;\n\n    public override string ToString() => Message;\n}"
  },
  {
    "path": "src/MarkdownSnippets/MissingSnippetsException.cs",
    "content": "namespace MarkdownSnippets;\n\npublic class MissingSnippetsException(IReadOnlyList<MissingSnippet> missing) :\n    SnippetException($\"Missing snippets:{Environment.NewLine}  {Report(missing)}\")\n{\n    public IReadOnlyList<MissingSnippet> Missing { get; } = missing;\n\n    static string Report(IReadOnlyList<MissingSnippet> missing) =>\n        string.Join(\n            $\"{Environment.NewLine}  \",\n            missing.GroupBy(_ => _.File ?? \"file-unknown\")\n                .Select(_ => $\"{_.Key}: {string.Join(',', _.Select(_ => _.Key))}\"));\n\n    public override string ToString() => Message;\n}\n"
  },
  {
    "path": "src/MarkdownSnippets/NewLineConfigReader.cs",
    "content": "static class NewLineConfigReader\n{\n    public static string ReadNewLine(string directory, IEnumerable<string> mdFiles)\n    {\n        var newLine = TryReadFromGitAttributes(directory);\n        if (newLine != null)\n        {\n            return newLine;\n        }\n\n        newLine = TryReadFromEditorConfig(directory);\n        if (newLine != null)\n        {\n            return newLine;\n        }\n\n        return DetectFromFiles(mdFiles);\n    }\n\n    static string DetectFromFiles(IEnumerable<string> mdFiles)\n    {\n        foreach (var mdFile in mdFiles.OrderBy(_ => _.Length))\n        {\n            using var reader = File.OpenText(mdFile);\n            if (reader.TryFindNewline(out var detectedNewLine))\n            {\n                return detectedNewLine;\n            }\n        }\n\n        return Environment.NewLine;\n    }\n\n    static string? TryReadFromGitAttributes(string directory)\n    {\n        var gitAttributesPath = FindFileUpward(directory, \".gitattributes\");\n        if (gitAttributesPath == null)\n        {\n            return null;\n        }\n\n        var lines = File.ReadAllLines(gitAttributesPath);\n        return ParseGitAttributesEol(lines);\n    }\n\n    static string? ParseGitAttributesEol(string[] lines)\n    {\n        string? wildcardEol = null;\n        string? extensionEol = null;\n\n        foreach (var line in lines)\n        {\n            var trimmed = line.Trim();\n            if (trimmed.Length == 0 || trimmed.StartsWith('#'))\n            {\n                continue;\n            }\n\n            var eolValue = ExtractGitAttributeEol(trimmed);\n            if (eolValue == null)\n            {\n                continue;\n            }\n\n            var pattern = GetGitAttributePattern(trimmed);\n            if (pattern == \"*\")\n            {\n                wildcardEol = eolValue;\n            }\n            else if (pattern is \"*.md\" or \"*.MD\")\n            {\n                extensionEol = eolValue;\n            }\n        }\n\n        // More specific pattern wins\n        var eol = extensionEol ?? wildcardEol;\n        return EolValueToNewLine(eol);\n    }\n\n    static string? ExtractGitAttributeEol(string line)\n    {\n        // Look for eol=lf or eol=crlf in the line\n        var eolIndex = line.IndexOf(\"eol=\", StringComparison.OrdinalIgnoreCase);\n        if (eolIndex == -1)\n        {\n            return null;\n        }\n\n        var valueStart = eolIndex + 4;\n        var valueEnd = valueStart;\n        while (valueEnd < line.Length && !char.IsWhiteSpace(line[valueEnd]))\n        {\n            valueEnd++;\n        }\n\n        var value = line.AsSpan(valueStart, valueEnd - valueStart);\n        if (value.Equals(\"lf\", StringComparison.OrdinalIgnoreCase))\n        {\n            return \"lf\";\n        }\n\n        if (value.Equals(\"crlf\", StringComparison.OrdinalIgnoreCase))\n        {\n            return \"crlf\";\n        }\n\n        if (value.Equals(\"cr\", StringComparison.OrdinalIgnoreCase))\n        {\n            return \"cr\";\n        }\n\n        return null;\n    }\n\n    static string GetGitAttributePattern(string line)\n    {\n        // Pattern is the first whitespace-delimited token\n        var end = 0;\n        while (end < line.Length && !char.IsWhiteSpace(line[end]))\n        {\n            end++;\n        }\n\n        return line[..end];\n    }\n\n    static string? TryReadFromEditorConfig(string directory)\n    {\n        var editorConfigPath = FindFileUpward(directory, \".editorconfig\");\n        if (editorConfigPath == null)\n        {\n            return null;\n        }\n\n        var lines = File.ReadAllLines(editorConfigPath);\n        return ParseEditorConfigEol(lines);\n    }\n\n    static string? ParseEditorConfigEol(string[] lines)\n    {\n        string? globalEol = null;\n        string? extensionEol = null;\n        var inWildcardSection = false;\n        var inExtensionSection = false;\n\n        foreach (var line in lines)\n        {\n            var trimmed = line.Trim();\n            if (trimmed.Length == 0 || trimmed.StartsWith('#') || trimmed.StartsWith(';'))\n            {\n                continue;\n            }\n\n            // Check for section headers\n            if (trimmed.StartsWith('[') && trimmed.EndsWith(']'))\n            {\n                var section = trimmed.AsSpan(1, trimmed.Length - 2);\n                inWildcardSection = section.SequenceEqual(\"*\");\n                inExtensionSection = EditorConfigSectionMatchesMd(section);\n                continue;\n            }\n\n            // Parse key=value\n            var equalsIndex = trimmed.IndexOf('=');\n            if (equalsIndex == -1)\n            {\n                continue;\n            }\n\n            var key = trimmed[..equalsIndex].Trim().ToLowerInvariant();\n            var value = trimmed[(equalsIndex + 1)..].Trim().ToLowerInvariant();\n\n            if (key == \"end_of_line\")\n            {\n                if (inExtensionSection)\n                {\n                    extensionEol = value;\n                }\n                else if (inWildcardSection)\n                {\n                    globalEol = value;\n                }\n            }\n        }\n\n        // More specific section wins\n        var eol = extensionEol ?? globalEol;\n        return EolValueToNewLine(eol);\n    }\n\n    static bool EditorConfigSectionMatchesMd(CharSpan section)\n    {\n        // Handle patterns like *.md, *.{md,txt}, etc.\n        if (section.Length == 0 || section[0] != '*')\n        {\n            return false;\n        }\n\n        var pattern = section[1..];\n        if (pattern.Equals(\".md\", StringComparison.OrdinalIgnoreCase))\n        {\n            return true;\n        }\n\n        // Handle {md,txt} style patterns like *.{md,txt}\n        if (pattern.Length > 0 && pattern[0] == '.')\n        {\n            var braceStart = pattern.IndexOf('{');\n            var braceEnd = pattern.IndexOf('}');\n            if (braceStart != -1 && braceEnd > braceStart)\n            {\n                var extensionsSpan = pattern.Slice(braceStart + 1, braceEnd - braceStart - 1);\n                foreach (var range in extensionsSpan.Split(','))\n                {\n                    var ext = extensionsSpan[range].Trim();\n                    if (ext.Equals(\"md\", StringComparison.OrdinalIgnoreCase))\n                    {\n                        return true;\n                    }\n                }\n            }\n        }\n\n        return false;\n    }\n\n    static string? EolValueToNewLine(string? eolValue) =>\n        eolValue switch\n        {\n            \"lf\" => \"\\n\",\n            \"crlf\" => \"\\r\\n\",\n            \"cr\" => \"\\r\",\n            _ => null\n        };\n\n    static string? FindFileUpward(string directory, string fileName)\n    {\n        var current = directory;\n        while (current != null)\n        {\n            var filePath = Path.Combine(current, fileName);\n            if (File.Exists(filePath))\n            {\n                return filePath;\n            }\n\n            var parent = Directory.GetParent(current);\n            current = parent?.FullName;\n        }\n\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/MarkdownSnippets/Paths.cs",
    "content": "static class Paths\n{\n    public static bool IsMdFile(this string value) =>\n        value.EndsWith(\".md\", StringComparison.OrdinalIgnoreCase) ||\n        value.EndsWith(\".mdx\", StringComparison.OrdinalIgnoreCase);\n\n    public static bool IsSourceMdFile(this string value) =>\n        value.EndsWith(\".source.md\", StringComparison.OrdinalIgnoreCase) ||\n        value.EndsWith(\".source.mdx\", StringComparison.OrdinalIgnoreCase);\n\n    public static bool IsIncludeMdFile(this string value) =>\n        value.EndsWith(\".include.md\", StringComparison.OrdinalIgnoreCase);\n}"
  },
  {
    "path": "src/MarkdownSnippets/Processing/AppendSnippetsToMarkdown.cs",
    "content": "namespace MarkdownSnippets;\n\npublic delegate void AppendSnippetsToMarkdown(string key, IEnumerable<Snippet> snippets, Action<string> appendLine);"
  },
  {
    "path": "src/MarkdownSnippets/Processing/DirectoryMarkdownProcessor.cs",
    "content": "﻿namespace MarkdownSnippets;\n\npublic class DirectoryMarkdownProcessor\n{\n    DocumentConvention convention;\n    bool writeHeader;\n    bool validateContent;\n    string? header;\n    bool readOnly;\n    int tocLevel;\n    IEnumerable<string>? tocExcludes;\n    Action<string> log;\n    string targetDirectory;\n    List<string> mdFiles = [];\n    List<Include> includes = [];\n    List<Snippet> snippets = [];\n    public IReadOnlyList<Snippet> Snippets => snippets;\n    List<string> snippetSourceFiles = [];\n    AppendSnippetsToMarkdown appendSnippets;\n    bool treatMissingAsWarning;\n    string newLine;\n    List<string> allFiles;\n\n    public DirectoryMarkdownProcessor(\n        string targetDirectory,\n        ShouldIncludeDirectory directoryIncludes,\n        ShouldIncludeDirectory markdownDirectoryIncludes,\n        ShouldIncludeDirectory snippetDirectoryIncludes,\n        DocumentConvention convention = DocumentConvention.SourceTransform,\n        bool scanForMdFiles = true,\n        bool scanForSnippets = true,\n        bool scanForIncludes = true,\n        Action<string>? log = null,\n        bool? writeHeader = null,\n        string? header = null,\n        bool? readOnly = null,\n        LinkFormat linkFormat = LinkFormat.GitHub,\n        int tocLevel = 2,\n        IEnumerable<string>? tocExcludes = null,\n        bool treatMissingAsWarning = false,\n        int maxWidth = int.MaxValue,\n        string? urlPrefix = null,\n        bool validateContent = false,\n        string? newLine = null,\n        bool omitSnippetLinks = false,\n        ShouldIncludeFile? snippetFileIncludes = null) :\n        this(\n            targetDirectory,\n            new SnippetMarkdownHandling(targetDirectory, linkFormat, omitSnippetLinks, urlPrefix).Append,\n            directoryIncludes,\n            markdownDirectoryIncludes,\n            snippetDirectoryIncludes,\n            convention,\n            scanForMdFiles: scanForMdFiles,\n            scanForSnippets: scanForSnippets,\n            scanForIncludes: scanForIncludes,\n            log: log,\n            writeHeader: writeHeader,\n            header: header,\n            readOnly: readOnly,\n            tocLevel: tocLevel,\n            tocExcludes: tocExcludes,\n            treatMissingAsWarning: treatMissingAsWarning,\n            maxWidth: maxWidth,\n            validateContent: validateContent,\n            newLine: newLine,\n            snippetFileIncludes: snippetFileIncludes)\n    {\n    }\n\n    public DirectoryMarkdownProcessor(\n        string targetDirectory,\n        AppendSnippetsToMarkdown appendSnippets,\n        ShouldIncludeDirectory directoryIncludes,\n        ShouldIncludeDirectory markdownDirectoryIncludes,\n        ShouldIncludeDirectory snippetDirectoryIncludes,\n        DocumentConvention convention = DocumentConvention.SourceTransform,\n        bool scanForMdFiles = true,\n        bool scanForSnippets = true,\n        bool scanForIncludes = true,\n        Action<string>? log = null,\n        bool? writeHeader = null,\n        string? header = null,\n        bool? readOnly = null,\n        int tocLevel = 2,\n        IEnumerable<string>? tocExcludes = null,\n        bool treatMissingAsWarning = false,\n        int maxWidth = int.MaxValue,\n        bool validateContent = false,\n        string? newLine = null,\n        ShouldIncludeFile? snippetFileIncludes = null)\n    {\n        this.appendSnippets = appendSnippets;\n        this.convention = convention;\n        this.writeHeader = writeHeader.GetValueOrDefault(convention == DocumentConvention.SourceTransform);\n        this.readOnly = readOnly.GetValueOrDefault(false);\n        this.validateContent = validateContent;\n        this.header = header;\n        this.tocLevel = tocLevel;\n        this.tocExcludes = tocExcludes;\n        this.newLine = newLine!;\n        this.treatMissingAsWarning = treatMissingAsWarning;\n\n        this.log = log ?? (_ => Trace.WriteLine(_));\n\n        Guard.DirectoryExists(targetDirectory, nameof(targetDirectory));\n        this.targetDirectory = Path.GetFullPath(targetDirectory);\n\n        var fileFinder = new FileFinder(targetDirectory, convention, directoryIncludes, markdownDirectoryIncludes, snippetDirectoryIncludes, snippetFileIncludes);\n\n        var (snippetFiles, mdFiles, includeFiles, allFiles) = fileFinder.FindFiles();\n        this.allFiles = allFiles;\n        if (scanForMdFiles)\n        {\n            this.mdFiles.AddRange(mdFiles);\n        }\n\n        if (scanForSnippets)\n        {\n            InitNewLine();\n            if (snippetFiles.Count != 0)\n            {\n                snippetSourceFiles.AddRange(snippetFiles);\n                this.log($\"Found {snippetFiles.Count} files for snippets\");\n            }\n\n            var stopwatch = Stopwatch.StartNew();\n            var read = FileSnippetExtractor.Read(snippetFiles, maxWidth, this.newLine).ToList();\n            if (read.Count != 0)\n            {\n                snippets.AddRange(read);\n                this.log($\"Added {read.Count} snippets ({stopwatch.ElapsedMilliseconds}ms)\");\n            }\n        }\n\n        if (scanForIncludes)\n        {\n            var includeKeys = new HashSet<string>();\n            foreach (var file in includeFiles)\n            {\n                var key = Path.GetFileName(file).Replace(\".include.md\", \"\");\n                if (!includeKeys.Add(key))\n                {\n                    throw new($\"Duplicate include: {key}\");\n                }\n\n                includes.Add(Include.Build(key, File.ReadAllLines(file), file));\n            }\n        }\n    }\n\n    [MemberNotNull(nameof(newLine))]\n    void InitNewLine()\n    {\n        if (newLine != null)\n        {\n            return;\n        }\n\n        newLine = NewLineConfigReader.ReadNewLine(targetDirectory, mdFiles);\n    }\n\n    public void AddSnippets(List<Snippet> snippets)\n    {\n        var files = snippets\n            .Where(_ => _.Path != null)\n            .Select(_ => _.Path!)\n            .Distinct()\n            .ToList();\n        if (files.Count != 0)\n        {\n            snippetSourceFiles.AddRange(files);\n        }\n\n        if (snippets.Count != 0)\n        {\n            this.snippets.AddRange(snippets);\n        }\n    }\n\n    public void AddSnippets(params Snippet[] snippets) =>\n        AddSnippets(snippets.ToList());\n\n    public void AddMdFiles(params string[] files)\n    {\n        foreach (var file in files)\n        {\n            mdFiles.Add(file);\n        }\n    }\n\n    public void Run()\n    {\n        if (mdFiles.Count == 0)\n        {\n            if (convention == DocumentConvention.InPlaceOverwrite)\n            {\n                throw new SnippetException(\"No markdown files found.\");\n            }\n\n            throw new SnippetException(\n                $$\"\"\"\n                  No markdown files found. This may be due to the DocumentConvention being SourceTransform.\n                  See https://github.com/SimonCropp/MarkdownSnippets#document-convention\n                  To move to InPlaceOverwrite add a file named `mdsnippets.json` in the target directory ({{targetDirectory}}) that contains:\n                  {\n                    \"Convention\": \"InPlaceOverwrite\"\n                  }\n                  \"\"\");\n        }\n\n        foreach (var group in snippets.GroupBy(_ => _.Path))\n        {\n            if (group.Key == null)\n            {\n                log(\"Snippets added with no path\");\n            }\n            else\n            {\n                log($\"Snippets extracted from {group.Key}\");\n            }\n\n            foreach (var snippet in group)\n            {\n                log($\"\\t{snippet.Key}\");\n            }\n        }\n\n        var stopwatch = Stopwatch.StartNew();\n        var processor = new MarkdownProcessor(\n            convention,\n            Snippets.ToDictionary(),\n            includes,\n            appendSnippets,\n            snippetSourceFiles,\n            allFiles,\n            tocLevel,\n            writeHeader,\n            targetDirectory,\n            validateContent,\n            header,\n            tocExcludes,\n            newLine);\n        foreach (var sourceFile in mdFiles)\n        {\n            ProcessFile(sourceFile, processor);\n        }\n\n        log($\"MarkdownProcessor Finished. {stopwatch.ElapsedMilliseconds}ms\");\n    }\n\n    void ProcessFile(string sourceFile, MarkdownProcessor markdownProcessor)\n    {\n        string targetFile;\n        if (convention == DocumentConvention.SourceTransform)\n        {\n            targetFile = TargetFileForSourceTransform(sourceFile, targetDirectory);\n        }\n        else\n        {\n            targetFile = sourceFile;\n        }\n\n        var lines = ReadLines(sourceFile);\n\n        FileEx.ClearReadOnly(targetFile);\n\n        var relativeSource = sourceFile[targetDirectory.Length..]\n            .Replace('\\\\', '/');\n        var result = markdownProcessor.Apply(lines, newLine, relativeSource);\n\n        var missingSnippets = result.MissingSnippets;\n        if (missingSnippets.Count != 0)\n        {\n            // If the config value is set to treat missing snippets as warnings, then don't throw\n            if (treatMissingAsWarning)\n            {\n                foreach (var missing in missingSnippets)\n                {\n                    log($\"WARN: The source file:{missing.File} includes a key {missing.Key}, however the snippet is missing. Make sure that the snippet is defined.\");\n                }\n            }\n            else\n            {\n                throw new MissingSnippetsException(missingSnippets);\n            }\n        }\n\n        var missingIncludes = result.MissingIncludes;\n        if (missingIncludes.Count != 0)\n        {\n            // If the config value is set to treat missing include as warnings, then don't throw\n            if (treatMissingAsWarning)\n            {\n                foreach (var missing in missingIncludes)\n                {\n                    log($\"WARN: The source file:{missing.File} includes a key {missing.Key}, however the include is missing. Make sure that the include is defined.\");\n                }\n            }\n            else\n            {\n                throw new MissingIncludesException(missingIncludes);\n            }\n        }\n\n        var errors = result.ValidationErrors;\n        if (errors.Count != 0)\n        {\n            throw new ContentValidationException(errors);\n        }\n\n        WriteLines(targetFile, lines);\n\n        if (readOnly)\n        {\n            FileEx.MakeReadOnly(targetFile);\n        }\n    }\n\n    void WriteLines(string target, List<Line> lines)\n    {\n        var directoryName = Path.GetDirectoryName(target)!;\n        Directory.CreateDirectory(directoryName);\n        using var writer = File.CreateText(target);\n        writer.NewLine = newLine;\n        foreach (var line in lines)\n        {\n            writer.WriteLine(line.Current);\n        }\n    }\n\n    static List<Line> ReadLines(string sourceFile)\n    {\n        using var reader = File.OpenText(sourceFile);\n        return Lines.ReadAllLines(reader, sourceFile).ToList();\n    }\n\n    static string TargetFileForSourceTransform(string sourceFile, string targetDirectory)\n    {\n        var relativePath = FileEx.GetRelativePath(sourceFile, targetDirectory);\n\n        var filtered = relativePath.Split(Path.DirectorySeparatorChar)\n            .Where(_ => !string.Equals(_, \"mdsource\", StringComparison.OrdinalIgnoreCase))\n            .ToArray();\n        var sourceTrimmed = Path.Combine(filtered);\n        var targetFile = Path.Combine(targetDirectory, sourceTrimmed);\n        // remove \".md\" from \".source.md\" then change \".source\" to \".md\"\n        targetFile = targetFile.Replace(\".source.\", \".\");\n        return targetFile;\n    }\n}"
  },
  {
    "path": "src/MarkdownSnippets/Processing/DocumentConvention.cs",
    "content": "namespace MarkdownSnippets;\n\npublic enum DocumentConvention\n{\n    SourceTransform,\n    InPlaceOverwrite\n}"
  },
  {
    "path": "src/MarkdownSnippets/Processing/HeaderWriter.cs",
    "content": "﻿static class HeaderWriter\n{\n    static string[] defaultHeaderLines;\n\n    internal const string DefaultHeader =\n        \"\"\"\n        GENERATED FILE - DO NOT EDIT\n        This file was generated by [MarkdownSnippets](https://github.com/SimonCropp/MarkdownSnippets).\n        Source File: {relativePath}\n        To change this file edit the source file and then run MarkdownSnippets.\n        \"\"\";\n\n    static HeaderWriter() =>\n        defaultHeaderLines = DefaultHeader.Lines();\n\n    public static string WriteHeader(string relativePath, string? header, string newline)\n    {\n        var lines = Header(header);\n        var inner = string.Join(newline, lines)\n            .Replace(\"{relativePath}\", relativePath)\n            .Replace(@\"\\n\", newline);\n        return $\"<!--{newline}{inner}{newline}-->{newline}\";\n    }\n\n    static string[] separator = [\"\\r\\n\", \"\\r\", \"\\n\", @\"\\n\"];\n\n    static string[] Header(string? header)\n    {\n        if (header == null)\n        {\n            return defaultHeaderLines;\n        }\n\n        if (header.Contains(\"<!--\") ||\n            header.Contains(\"-->\"))\n        {\n            throw new SnippetException(\"Header cannot contain `<!--` or `-->`.\");\n        }\n\n        return header.Split(separator, StringSplitOptions.None);\n    }\n}"
  },
  {
    "path": "src/MarkdownSnippets/Processing/IncludeProcessor.cs",
    "content": "class IncludeProcessor\n{\n    DocumentConvention convention;\n    Dictionary<string, Include> includesLookup;\n    IReadOnlyDictionary<string, IReadOnlyList<Snippet>> snippets;\n    IReadOnlyList<string> allFiles;\n    string targetDirectory;\n\n    public IncludeProcessor(\n        DocumentConvention convention,\n        IReadOnlyList<Include> includes,\n        IReadOnlyDictionary<string, IReadOnlyList<Snippet>> snippets,\n        string targetDirectory,\n        IReadOnlyList<string> allFiles)\n    {\n        targetDirectory = Path.GetFullPath(targetDirectory);\n        this.targetDirectory = targetDirectory.Replace('\\\\', '/');\n        this.convention = convention;\n        includesLookup = includes.ToDictionary(_ => _.Key);\n        this.snippets = snippets;\n        this.allFiles = allFiles;\n    }\n\n    public bool TryProcessInclude(List<Line> lines, Line line, ICollection<Include> used, int index, List<MissingInclude> missing, string? relativePath)\n    {\n        var current = line.Current;\n\n        if (current.StartsWith(\"include: \"))\n        {\n            var includeKey = line.Current[9..];\n            Inner(lines, line, used, index, missing, includeKey, relativePath);\n            return true;\n        }\n\n        if (convention == DocumentConvention.SourceTransform)\n        {\n            return false;\n        }\n\n        static string GetIncludeKey(CharSpan substring)\n        {\n            var indexOfDotPath = substring.IndexOf(\". path:\", StringComparison.Ordinal);\n            if (indexOfDotPath != -1)\n            {\n                return substring[..indexOfDotPath].ToString();\n            }\n\n            return substring[..^4].ToString();\n        }\n\n        var indexSingleLineInclude = current.IndexOf(\"<!-- singleLineInclude: \", StringComparison.Ordinal);\n        if (indexSingleLineInclude > 0)\n        {\n            var substring = current.AsSpan()[(indexSingleLineInclude + 24)..];\n            var includeKey = GetIncludeKey(substring);\n            Inner(lines, line, used, index, missing, includeKey, relativePath);\n            return true;\n        }\n\n        if (current.StartsWith(\"<!-- include: \", StringComparison.Ordinal))\n        {\n            var substring = current.AsSpan()[14..];\n            var includeKey = GetIncludeKey(substring);\n            lines.RemoveUntil(index + 1, \"<!-- endInclude -->\", line.Path, line);\n            Inner(lines, line, used, index, missing, includeKey, relativePath);\n            return true;\n        }\n\n        var indexOfInclude = current.IndexOf(\"<!-- include: \", StringComparison.Ordinal);\n        if (indexOfInclude > 0)\n        {\n            var substring = current.AsSpan()[(indexOfInclude + 14)..];\n            var includeKey = GetIncludeKey(substring);\n            lines.RemoveUntil(index + 1, \"<!-- endInclude -->\", line.Path, line);\n            Inner(lines, line, used, index, missing, includeKey, relativePath);\n            return true;\n        }\n\n        return false;\n    }\n\n    void Inner(List<Line> lines, Line line, ICollection<Include> used, int index, List<MissingInclude> missing, string includeKey, string? relativePath)\n    {\n        if (includesLookup.TryGetValue(includeKey, out var include))\n        {\n            AddInclude(lines, line, used, index, include, true);\n            return;\n        }\n\n        if (snippets.TryGetValue(includeKey, out var snippetsResult))\n        {\n            if (snippetsResult.Count > 1)\n            {\n                throw new(\"Only one snippet may be used as an include\");\n            }\n\n            if (snippetsResult.Count == 1)\n            {\n                var snippet = snippetsResult[0];\n                var snippetInclude = Include.Build(snippet.Key, snippet.Value.Lines(), snippet.Path);\n                AddInclude(lines, line, used, index, snippetInclude, true);\n                return;\n            }\n        }\n\n        if (includeKey.StartsWith(\"http\"))\n        {\n            var (success, httpPath) = Downloader.DownloadFile(includeKey).GetAwaiter().GetResult();\n            if (success)\n            {\n                include = Include.Build(includeKey, File.ReadAllLines(httpPath!), null);\n                AddInclude(lines, line, used, index, include, false);\n                return;\n            }\n        }\n\n        if (RelativeFile.Find(allFiles, targetDirectory, includeKey, relativePath, line.Path, out var path))\n        {\n            include = Include.Build(includeKey, File.ReadAllLines(path), path);\n            AddInclude(lines, line, used, index, include, false);\n            return;\n        }\n\n        missing.Add(new(includeKey, index + 1, line.Path));\n        line.Current = $\"** Could not find include '{includeKey}' ** <!-- singleLineInclude: {includeKey} -->\";\n    }\n\n    void AddInclude(List<Line> lines, Line line, ICollection<Include> used, int index, Include include, bool writePath)\n    {\n        used.Add(include);\n        var linesToInject = BuildIncludes(line, include, writePath).ToList();\n        lines[index] = linesToInject[0];\n\n        if (linesToInject.Count > 1)\n        {\n            lines.InsertRange(index + 1, linesToInject.GetRange(1, linesToInject.Count - 1));\n        }\n    }\n\n    IEnumerable<Line> BuildIncludes(Line line, Include include, bool writePath)\n    {\n        var path = GetPath(include);\n\n        var count = include.Lines.Count;\n        if (count == 0)\n        {\n            return BuildEmpty(line, path, include, writePath);\n        }\n\n        if (count == 1)\n        {\n            return BuildSingle(line, path, include, writePath);\n        }\n\n        return BuildMultiple(line, path, include, writePath);\n    }\n\n    static IEnumerable<Line> BuildMultiple(Line line, string? path, Include include, bool writePath)\n    {\n        var lines = include.Lines;\n        var count = lines.Count;\n        var first = lines[0];\n        var key = include.Key;\n        if (ShouldWriteIncludeOnDiffLine(first))\n        {\n            if (writePath)\n            {\n                yield return line.WithCurrent($\"<!-- include: {key}. path: {path} -->\");\n            }\n            else\n            {\n                yield return line.WithCurrent($\"<!-- include: {key} -->\");\n            }\n\n            yield return new(first, path, 1);\n        }\n        else\n        {\n            if (writePath)\n            {\n                yield return line.WithCurrent($\"{first}<!-- include: {key}. path: {path} -->\");\n            }\n            else\n            {\n                yield return line.WithCurrent($\"{first}<!-- include: {key} -->\");\n            }\n        }\n\n        for (var index = 1; index < lines.Count - 1; index++)\n        {\n            var includeLine = lines[index];\n            yield return new(includeLine, path, index);\n        }\n\n        var last = lines[^1];\n        if (ShouldWriteIncludeOnDiffLine(last))\n        {\n            yield return new(last, path, count);\n            yield return new(\"<!-- endInclude -->\", path, count);\n        }\n        else\n        {\n            yield return new($\"{last}<!-- endInclude -->\", path, count);\n        }\n    }\n\n    static bool ShouldWriteIncludeOnDiffLine(string line) =>\n        SnippetKey.IsSnippetLine(line) ||\n        line.StartsWith(\"<!-- endSnippet -->\") ||\n        line.EndsWith(\"```\") ||\n        line.StartsWith('|') ||\n        line.EndsWith('|');\n\n    static IEnumerable<Line> BuildEmpty(Line line, string? path, Include include, bool writePath)\n    {\n        if (writePath)\n        {\n            yield return line.WithCurrent($\"<!-- emptyInclude: {include.Key}. path: {path} -->\");\n        }\n        else\n        {\n            yield return line.WithCurrent($\"<!-- emptyInclude: {include.Key} -->\");\n        }\n    }\n\n    static IEnumerable<Line> BuildSingle(Line line, string? path, Include include, bool writePath)\n    {\n        var first = include.Lines[0];\n        var key = include.Key;\n        if (ShouldWriteIncludeOnDiffLine(first))\n        {\n            if (writePath)\n            {\n                yield return line.WithCurrent($\"<!-- include: {key}. path: {path} -->\");\n            }\n            else\n            {\n                yield return line.WithCurrent($\"<!-- include: {key} -->\");\n            }\n\n            yield return new(first, path, 1);\n            yield return new(\"<!-- endInclude -->\", path, 1);\n        }\n        else\n        {\n            if (writePath)\n            {\n                yield return line.WithCurrent($\"{first}<!-- singleLineInclude: {key}. path: {path} -->\");\n            }\n            else\n            {\n                yield return line.WithCurrent($\"{first}<!-- singleLineInclude: {key} -->\");\n            }\n        }\n    }\n\n    string? GetPath(IContent include)\n    {\n        if (include.Path == null)\n        {\n            return null;\n        }\n\n        var path = include.Path.Replace('\\\\', '/');\n        if (!path.StartsWith(targetDirectory))\n        {\n            return path;\n        }\n\n        return path[targetDirectory.Length..];\n    }\n}"
  },
  {
    "path": "src/MarkdownSnippets/Processing/Line.cs",
    "content": "[DebuggerDisplay(\"Line={LineNumber}, Original={Original}, Current={Current}\")]\nclass Line\n{\n    public Line(string original, string? path, int lineNumber)\n    {\n        Original = original;\n        Current = original;\n        Path = path;\n        LineNumber = lineNumber;\n        LeadingWhitespace = GetLeadingWhitespace(original);\n    }\n\n    public Line WithCurrent(string current) =>\n        new(Original, Path, LineNumber)\n        {\n            Current = current\n        };\n\n    public readonly string Original;\n\n    public override string ToString() =>\n        throw new();\n\n    public string Current\n    {\n        get;\n        set\n        {\n            IsWhiteSpace = value.IsWhiteSpace();\n            Length = value.Length;\n            field = value;\n        }\n    } = null!;\n\n    public string? Path { get; }\n    public int LineNumber { get; }\n\n    public int Length { get; private set; }\n\n    public bool IsWhiteSpace { get; private set; }\n\n    public string LeadingWhitespace { get; }\n\n    static string GetLeadingWhitespace(string text)\n    {\n        var length = 0;\n        foreach (var c in text)\n        {\n            if (c is ' ' or '\\t')\n            {\n                length++;\n            }\n            else\n            {\n                break;\n            }\n        }\n        return text[..length];\n    }\n}"
  },
  {
    "path": "src/MarkdownSnippets/Processing/Lines.cs",
    "content": "﻿static class Lines\n{\n    public static void RemoveUntil(\n        this List<Line> lines,\n        int index,\n        string match,\n        string? path,\n        Line startLine)\n    {\n        var endIndex = index;\n        while (endIndex < lines.Count)\n        {\n            if (lines[endIndex].Current.Contains(match))\n            {\n                lines.RemoveRange(index, endIndex - index + 1);\n                return;\n            }\n\n            endIndex++;\n        }\n\n        throw new MarkdownProcessingException($\"Expected to find `{match}`.\", path, startLine.LineNumber);\n    }\n\n    public static IEnumerable<Line> ReadAllLines(TextReader textReader, string? path)\n    {\n        var index = 1;\n        while (textReader.ReadLine() is { } line)\n        {\n            yield return new(line, path, index);\n            index++;\n        }\n    }\n}"
  },
  {
    "path": "src/MarkdownSnippets/Processing/LinkFormat.cs",
    "content": "namespace MarkdownSnippets;\n\npublic enum LinkFormat\n{\n    GitHub,\n    Tfs,\n    Bitbucket,\n    GitLab,\n    DevOps,\n    None\n}"
  },
  {
    "path": "src/MarkdownSnippets/Processing/Markdown.cs",
    "content": "static class Markdown\n{\n    static Regex stripLinkRegex = new(@\"\\[(.*?)\\][\\[\\(].*?[\\]\\)]\", RegexOptions.Compiled);\n\n    public static string StripMarkdown(string input)\n    {\n        var title = input.Replace(\"*\",\"\");\n        return stripLinkRegex.Replace(title, \"$1\");\n    }\n}"
  },
  {
    "path": "src/MarkdownSnippets/Processing/MarkdownProcessor.cs",
    "content": "namespace MarkdownSnippets;\n\n/// <summary>\n/// Merges <see cref=\"Snippet\"/>s with an input file/text.\n/// </summary>\npublic class MarkdownProcessor\n{\n    DocumentConvention convention;\n    IReadOnlyDictionary<string, IReadOnlyList<Snippet>> snippets;\n    AppendSnippetsToMarkdown appendSnippets;\n    bool writeHeader;\n    bool validateContent;\n    string newLine;\n    string? header;\n    int tocLevel;\n    List<string> tocExcludes;\n    List<string> snippetSourceFiles;\n    IncludeProcessor includeProcessor;\n\n    static List<string> validationExcludes =\n    [\n        \"code_of_conduct\",\n        \".github\",\n        \"license\"\n    ];\n\n    string targetDirectory;\n    IReadOnlyList<string> allFiles;\n\n    public MarkdownProcessor(\n        DocumentConvention convention,\n        IReadOnlyDictionary<string, IReadOnlyList<Snippet>> snippets,\n        IReadOnlyList<Include> includes,\n        AppendSnippetsToMarkdown appendSnippets,\n        IReadOnlyList<string> snippetSourceFiles,\n        IReadOnlyList<string> allFiles,\n        int tocLevel,\n        bool writeHeader,\n        string targetDirectory,\n        bool validateContent,\n        string? header = null,\n        IEnumerable<string>? tocExcludes = null,\n        string newLine = \"\\n\")\n    {\n        Guard.AgainstEmpty(header, nameof(header));\n        Guard.AgainstNegativeAndZero(tocLevel, nameof(tocLevel));\n        Guard.AgainstNullAndEmpty(targetDirectory, nameof(targetDirectory));\n\n        if (convention == DocumentConvention.InPlaceOverwrite && writeHeader)\n        {\n            throw new SnippetException(\"WriteHeader is not allowed with InPlaceOverwrite convention.\");\n        }\n\n        this.targetDirectory = Path.GetFullPath(targetDirectory).Replace('\\\\', '/');\n\n        this.allFiles = allFiles\n            .Select(_ => _.Replace('\\\\', '/'))\n            .ToList();\n\n        this.convention = convention;\n        this.snippets = snippets;\n        this.appendSnippets = appendSnippets;\n        this.writeHeader = writeHeader;\n        this.validateContent = validateContent;\n        this.newLine = newLine;\n        this.header = header;\n        this.tocLevel = tocLevel;\n        if (tocExcludes == null)\n        {\n            this.tocExcludes = [];\n        }\n        else\n        {\n            this.tocExcludes = tocExcludes.ToList();\n        }\n\n        this.snippetSourceFiles = snippetSourceFiles\n            .Select(_ => _.Replace('\\\\', '/'))\n            .ToList();\n        includeProcessor = new(convention, includes, snippets, targetDirectory, this.allFiles);\n    }\n\n    public string Apply(string input, string? file = null)\n    {\n        Guard.AgainstEmpty(file, nameof(file));\n        var builder = StringBuilderCache.Acquire();\n        try\n        {\n            using var reader = new StringReader(input);\n            using var writer = new StringWriter(builder);\n            var processResult = Apply(reader, writer, file);\n            var missing = processResult.MissingSnippets;\n            if (missing.Count != 0)\n            {\n                throw new MissingSnippetsException(missing);\n            }\n\n            return builder.ToString();\n        }\n        finally\n        {\n            StringBuilderCache.Release(builder);\n        }\n    }\n\n    /// <summary>\n    /// Apply to <paramref name=\"writer\"/>.\n    /// </summary>\n    public ProcessResult Apply(TextReader textReader, TextWriter writer, string? file = null)\n    {\n        Guard.AgainstEmpty(file, nameof(file));\n        var lines = Lines.ReadAllLines(textReader, null).ToList();\n        writer.NewLine = newLine;\n        var result = Apply(lines, newLine, file);\n\n        for (var index = 0; index < lines.Count - 1; index++)\n        {\n            var line = lines[index];\n            writer.WriteLine(line.Current);\n        }\n\n        writer.Write(lines.Last().Current);\n\n        return result;\n    }\n\n    internal ProcessResult Apply(List<Line> lines, string newLine, string? relativePath)\n    {\n        var missingSnippets = new List<MissingSnippet>();\n        var validationErrors = new List<ValidationError>();\n        var missingIncludes = new List<MissingInclude>();\n        var usedSnippets = new HashSet<Snippet>();\n        var usedIncludes = new HashSet<Include>();\n        var builder = new StringBuilder();\n        Line? tocLine = null;\n\n        Action<string> CreateIndentedAppendLine(string indent) => s =>\n        {\n            var first = true;\n            foreach (var line in s.AsSpan().EnumerateLines())\n            {\n                if (!first)\n                {\n                    builder.Append(newLine);\n                }\n                first = false;\n                builder.Append(indent);\n                builder.Append(line);\n            }\n            builder.Append(newLine);\n        };\n\n        var headerLines = new List<Line>();\n        for (var index = 0; index < lines.Count; index++)\n        {\n            var line = lines[index];\n\n            if (ValidateContent(relativePath, line, validationErrors))\n            {\n                continue;\n            }\n\n            if (includeProcessor.TryProcessInclude(lines, line, usedIncludes, index, missingIncludes, relativePath))\n            {\n                continue;\n            }\n\n            if (line.Current.StartsWith('#'))\n            {\n                if (tocLine != null)\n                {\n                    headerLines.Add(line);\n                }\n\n                continue;\n            }\n\n            if (line.Current.TrimStart() == \"toc\")\n            {\n                tocLine = line;\n                continue;\n            }\n\n            void AppendSnippet(string key1)\n            {\n                builder.Clear();\n                var indentedAppendLine = CreateIndentedAppendLine(line.LeadingWhitespace);\n                ProcessSnippetLine(indentedAppendLine, missingSnippets, usedSnippets, key1, relativePath, line);\n                builder.TrimEnd();\n                line.Current = builder.ToString();\n            }\n\n            void AppendWebSnippet(string url, string snippetKey, string? viewUrl = null)\n            {\n                builder.Clear();\n                var indentedAppendLine = CreateIndentedAppendLine(line.LeadingWhitespace);\n                ProcessWebSnippetLine(indentedAppendLine, missingSnippets, usedSnippets, url, snippetKey, viewUrl, line);\n                builder.TrimEnd();\n                line.Current = builder.ToString();\n            }\n\n            if (SnippetKey.ExtractSnippet(line, out var key))\n            {\n                AppendSnippet(key);\n                continue;\n            }\n\n            if (SnippetKey.ExtractWebSnippet(line, out var url, out var snippetKey, out var viewUrl))\n            {\n                AppendWebSnippet(url, snippetKey, viewUrl);\n                continue;\n            }\n\n            if (convention == DocumentConvention.SourceTransform)\n            {\n                continue;\n            }\n\n            if (SnippetKey.ExtractStartCommentSnippet(line, out key))\n            {\n                AppendSnippet(key);\n\n                index++;\n\n                lines.RemoveUntil(\n                    index,\n                    \"<!-- endSnippet -->\",\n                    relativePath,\n                    line);\n                continue;\n            }\n\n            if (SnippetKey.ExtractStartCommentWebSnippet(line, out url, out snippetKey, out viewUrl))\n            {\n                AppendWebSnippet(url, snippetKey, viewUrl);\n\n                index++;\n\n                lines.RemoveUntil(\n                    index,\n                    \"<!-- endSnippet -->\",\n                    relativePath,\n                    line);\n                continue;\n            }\n\n            if (line.Current.TrimStart() == \"<!-- toc -->\")\n            {\n                tocLine = line;\n\n                index++;\n\n                lines.RemoveUntil(index, \"<!-- endToc -->\", relativePath, line);\n            }\n        }\n\n        if (writeHeader)\n        {\n            lines.Insert(0, new(HeaderWriter.WriteHeader(relativePath!, header, newLine), \"\", 0));\n        }\n\n        tocLine?.Current = TocBuilder.BuildToc(headerLines, tocLevel, tocExcludes, newLine);\n\n        return new(\n            missingSnippets: missingSnippets,\n            usedSnippets: usedSnippets.ToList(),\n            usedIncludes: usedIncludes.ToList(),\n            missingIncludes: missingIncludes,\n            validationErrors: validationErrors);\n    }\n\n    bool ValidateContent(string? relativePath, Line line, List<ValidationError> validationErrors)\n    {\n        if (!validateContent)\n        {\n            return false;\n        }\n\n        if (relativePath != null &&\n            validationExcludes.Any(relativePath.Contains))\n        {\n            return false;\n        }\n\n        var found = false;\n        foreach (var error in ContentValidation.Verify(line.Original))\n        {\n            validationErrors.Add(new(error.error, line.LineNumber, error.column, line.Path));\n            found = true;\n        }\n\n        return found;\n    }\n\n    void ProcessSnippetLine(Action<string> appendLine, List<MissingSnippet> missings, HashSet<Snippet> used, string key, string? relativePath, Line line)\n    {\n        appendLine($\"<!-- snippet: {key} -->\");\n\n        if (TryGetSnippets(key, relativePath, line.Path, out var snippetsForKey))\n        {\n            appendSnippets(key, snippetsForKey, appendLine);\n            appendLine(\"<!-- endSnippet -->\");\n            used.UnionWith(snippetsForKey);\n            return;\n        }\n\n        var missing = new MissingSnippet(key, line.LineNumber, line.Path);\n        missings.Add(missing);\n        appendLine(\"```\");\n        appendLine($\"** Could not find snippet '{key}' **\");\n        appendLine(\"```\");\n        appendLine(\"<!-- endSnippet -->\");\n    }\n\n    void ProcessWebSnippetLine(Action<string> appendLine, List<MissingSnippet> missings, HashSet<Snippet> used, string url, string snippetKey, string? viewUrl, Line line)\n    {\n        var commentText = viewUrl == null\n            ? $\"<!-- web-snippet: {url}#{snippetKey} -->\"\n            : $\"<!-- web-snippet: {url}#{snippetKey} {viewUrl} -->\";\n        appendLine(commentText);\n        // Download file content\n        try\n        {\n            var (success, content) = Downloader.DownloadContent(url).GetAwaiter().GetResult();\n            if (!success || string.IsNullOrWhiteSpace(content))\n            {\n                var missing = new MissingSnippet($\"{url}#{snippetKey}\", line.LineNumber, line.Path);\n                missings.Add(missing);\n                appendLine(\"```\");\n                appendLine($\"** Could not fetch or parse web-snippet '{url}#{snippetKey}' **\");\n                appendLine(\"```\");\n                appendLine(\"<!-- endSnippet -->\");\n                return;\n            }\n            // Extract snippets from content\n            using var reader = new StringReader(content);\n            var snippets = FileSnippetExtractor.Read(reader, url);\n            var found = snippets.FirstOrDefault(_ => _.Key == snippetKey);\n            if (found == null)\n            {\n                var missing = new MissingSnippet($\"{url}#{snippetKey}\", line.LineNumber, line.Path);\n                missings.Add(missing);\n                appendLine(\"```\");\n                appendLine($\"** Could not find snippet '{snippetKey}' in '{url}' **\");\n                appendLine(\"```\");\n                appendLine(\"<!-- endSnippet -->\");\n                return;\n            }\n            // Create new snippet with viewUrl if provided\n            var snippetToAppend = viewUrl == null\n                ? found\n                : Snippet.Build(\n                    language: found.Language,\n                    startLine: found.StartLine,\n                    endLine: found.EndLine,\n                    value: found.Value,\n                    key: found.Key,\n                    path: found.Path,\n                    expressiveCode: found.ExpressiveCode,\n                    viewUrl: viewUrl);\n            appendSnippets(snippetKey, [snippetToAppend], appendLine);\n            appendLine(\"<!-- endSnippet -->\");\n            used.Add(snippetToAppend);\n        }\n        catch\n        {\n            var missing = new MissingSnippet($\"{url}#{snippetKey}\", line.LineNumber, line.Path);\n            missings.Add(missing);\n            appendLine(\"```\");\n            appendLine($\"** Could not fetch or parse web-snippet '{url}#{snippetKey}' **\");\n            appendLine(\"```\");\n            appendLine(\"<!-- endSnippet -->\");\n        }\n    }\n\n    bool TryGetSnippets(\n        string key,\n        string? relativePath,\n        string? linePath,\n        [NotNullWhen(true)] out IReadOnlyList<Snippet>? snippetsForKey)\n    {\n        if (snippets.TryGetValue(key, out snippetsForKey))\n        {\n            return true;\n        }\n\n        if (key.StartsWith(\"http\"))\n        {\n            return GetForHttp(key, out snippetsForKey);\n        }\n\n        return FilesToSnippets(key, relativePath, linePath, out snippetsForKey);\n    }\n\n    bool FilesToSnippets(\n        string key,\n        string? relativePath,\n        string? linePath,\n        [NotNullWhen(true)] out IReadOnlyList<Snippet>? snippetsForKey)\n    {\n        var keyWithDirChar = FileEx.PrependSlash(key);\n\n        snippetsForKey = snippetSourceFiles\n            .Where(file => file.EndsWith(keyWithDirChar, StringComparison.OrdinalIgnoreCase))\n            .Select(file => FileToSnippet(key, file, file))\n            .ToList();\n        if (snippetsForKey.Count != 0)\n        {\n            return true;\n        }\n\n        if (RelativeFile.Find(allFiles, targetDirectory, key, relativePath, linePath, out var path))\n        {\n            snippetsForKey = SnippetsForFile(key, path);\n            return true;\n        }\n\n        snippetsForKey = null!;\n        return false;\n    }\n\n\n    List<Snippet> SnippetsForFile(string key, string relativeToRoot) =>\n        [FileToSnippet(key, relativeToRoot, null)];\n\n    bool GetForHttp(string key, out IReadOnlyList<Snippet> snippetsForKey)\n    {\n        var (success, path) = Downloader.DownloadFile(key).GetAwaiter().GetResult();\n        if (!success)\n        {\n            snippetsForKey = null!;\n            return false;\n        }\n\n        snippetsForKey = SnippetsForFile(key, path!);\n        return true;\n    }\n\n    Snippet FileToSnippet(string key, string file, string? path)\n    {\n        var (text, lineCount) = ReadNonStartEndLines(file);\n\n        if (lineCount == 0)\n        {\n            lineCount++;\n        }\n\n        return Snippet.Build(\n            startLine: 1,\n            endLine: lineCount,\n            value: text,\n            key: key,\n            language: FileSnippetExtractor.GetLanguageFromPath(file),\n            path: path,\n            expressiveCode: null);\n    }\n\n    (string text, int lineCount) ReadNonStartEndLines(string file)\n    {\n        var builder = StringBuilderCache.Acquire();\n        try\n        {\n            var lineCount = 0;\n            foreach (var line in File.ReadLines(file))\n            {\n                if (!StartEndTester.IsStartOrEnd(line.AsSpan().TrimStart()))\n                {\n                    if (lineCount > 0)\n                    {\n                        builder.Append(newLine);\n                    }\n\n                    builder.Append(line);\n                    lineCount++;\n                }\n            }\n\n            builder.TrimEnd();\n            var start = 0;\n            while (start < builder.Length && char.IsWhiteSpace(builder[start]))\n            {\n                start++;\n            }\n\n            return (builder.ToString(start, builder.Length - start), lineCount);\n        }\n        finally\n        {\n            StringBuilderCache.Release(builder);\n        }\n    }\n}\n"
  },
  {
    "path": "src/MarkdownSnippets/Processing/MissingInclude.cs",
    "content": "namespace MarkdownSnippets;\n\n/// <summary>\n/// Part of <see cref=\"ProcessResult\"/>.\n/// </summary>\n[DebuggerDisplay(\"Key={Key}, Line={LineNumber}\")]\npublic class MissingInclude\n{\n    /// <summary>\n    /// Initialise a new instance of <see cref=\"MissingInclude\"/>.\n    /// </summary>\n    public MissingInclude(string key, int lineNumber, string? file)\n    {\n        Guard.AgainstNullAndEmpty(key, nameof(key));\n        Guard.AgainstNegativeAndZero(lineNumber, nameof(lineNumber));\n        Guard.AgainstEmpty(file, nameof(file));\n        Key = key;\n        LineNumber = lineNumber;\n        File = file;\n    }\n\n    /// <summary>\n    /// The key of the missing include.\n    /// </summary>\n    public string Key { get; }\n\n    /// <summary>\n    /// The line number in the input text where the include was expected to be injected.\n    /// </summary>\n    public int LineNumber { get; }\n\n    /// <summary>\n    /// The File of the missing include.\n    /// </summary>\n    public string? File { get; }\n\n    public override string ToString()\n    {\n        if (File == null)\n        {\n            return $\"\"\"\n                    MissingInclude.\n                      LineNumber: {LineNumber}\n                      Key: {Key}\n                    \"\"\";\n        }\n\n        return $\"\"\"\n                MissingInclude.\n                  File: {File}\n                  LineNumber: {LineNumber}\n                  Key: {Key}\n                \"\"\";\n    }\n}"
  },
  {
    "path": "src/MarkdownSnippets/Processing/MissingSnippet.cs",
    "content": "namespace MarkdownSnippets;\n\n/// <summary>\n/// Part of <see cref=\"ProcessResult\"/>.\n/// </summary>\n[DebuggerDisplay(\"Key={Key}, Line={LineNumber}\")]\npublic class MissingSnippet\n{\n    /// <summary>\n    /// Initialise a new instance of <see cref=\"MissingSnippet\"/>.\n    /// </summary>\n    public MissingSnippet(string key, int lineNumber, string? file)\n    {\n        Guard.AgainstNullAndEmpty(key, nameof(key));\n        Guard.AgainstNegativeAndZero(lineNumber, nameof(lineNumber));\n        Guard.AgainstEmpty(file, nameof(file));\n        Key = key;\n        LineNumber = lineNumber;\n        File = file;\n    }\n\n    /// <summary>\n    /// The key of the missing snippet.\n    /// </summary>\n    public string Key { get; }\n\n    /// <summary>\n    /// The line number in the input text where the snippet was expected to be injected.\n    /// </summary>\n    public int LineNumber { get; }\n\n    /// <summary>\n    /// The File of the missing snippet.\n    /// </summary>\n    public string? File { get; }\n\n    public override string ToString()\n    {\n        if (File == null)\n        {\n            return $\"\"\"\n                    MissingSnippet.\n                      LineNumber: {LineNumber}\n                      Key: {Key}\n                    \"\"\";\n        }\n\n        return $\"\"\"\n                MissingSnippet.\n                  File: {File}\n                  LineNumber: {LineNumber}\n                  Key: {Key}\n                \"\"\";\n    }\n}"
  },
  {
    "path": "src/MarkdownSnippets/Processing/ProcessResult.cs",
    "content": "namespace MarkdownSnippets;\n\n/// <summary>\n/// The result of <see cref=\"MarkdownProcessor\"/> Apply methods.\n/// </summary>\npublic class ProcessResult(\n        IReadOnlyList<Snippet> usedSnippets,\n        IReadOnlyList<MissingSnippet> missingSnippets,\n        IReadOnlyList<Include> usedIncludes,\n        IReadOnlyList<MissingInclude> missingIncludes,\n        IReadOnlyList<ValidationError> validationErrors) :\n        IEnumerable<Snippet>\n{\n    /// <summary>\n    /// List of all <see cref=\"Snippet\"/>s that the markdown file used.\n    /// </summary>\n    public IReadOnlyList<Snippet> UsedSnippets { get; } = usedSnippets;\n\n    /// <summary>\n    /// List of all <see cref=\"Include\"/>s that the markdown file used.\n    /// </summary>\n    public IReadOnlyList<Include> UsedIncludes { get; } = usedIncludes;\n\n    /// <summary>\n    /// Enumerates through the <see cref=\"UsedSnippets\" /> but will first throw an exception if there are any <see cref=\"MissingSnippets\" />.\n    /// </summary>\n    public virtual IEnumerator<Snippet> GetEnumerator()\n    {\n        if (MissingSnippets.Count != 0)\n        {\n            throw new MissingSnippetsException(MissingSnippets);\n        }\n\n        if (MissingIncludes.Count != 0)\n        {\n            throw new MissingIncludesException(MissingIncludes);\n        }\n\n        if (ValidationErrors.Count != 0)\n        {\n            throw new ContentValidationException(ValidationErrors);\n        }\n\n        return UsedSnippets.GetEnumerator();\n    }\n\n    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();\n\n    /// <summary>\n    /// List of all snippets that the markdown file expected but did not exist in the input snippets.\n    /// </summary>\n    public IReadOnlyList<MissingSnippet> MissingSnippets { get; } = missingSnippets;\n\n    /// <summary>\n    /// List of all validation errors that the markdown file.\n    /// </summary>\n    public IReadOnlyList<ValidationError> ValidationErrors { get; } = validationErrors;\n\n    /// <summary>\n    /// List of all includes that the markdown file expected but did not exist in the input includes.\n    /// </summary>\n    public IReadOnlyList<MissingInclude> MissingIncludes { get; } = missingIncludes;\n}"
  },
  {
    "path": "src/MarkdownSnippets/Processing/RelativeFile.cs",
    "content": "static class RelativeFile\n{\n    static bool InnerFind(IReadOnlyList<string> allFiles, string targetDirectory, string key, string? relativePath, string? linePath, out string path)\n    {\n        if (!key.Contains('.'))\n        {\n            path = null!;\n            return false;\n        }\n\n        var relativeToRoot = Path.Combine(targetDirectory, key);\n        if (File.Exists(relativeToRoot))\n        {\n            path = relativeToRoot;\n            return true;\n        }\n\n        var documentDirectory = Path.GetDirectoryName(relativePath);\n        if (documentDirectory != null)\n        {\n            var relativeToDocument = Path.Combine(targetDirectory, documentDirectory.Trim('/', '\\\\'), key);\n            if (File.Exists(relativeToDocument))\n            {\n                path = relativeToDocument;\n                return true;\n            }\n        }\n\n        var lineDirectory = Path.GetDirectoryName(linePath);\n        if (lineDirectory != null)\n        {\n            var relativeToLine = Path.Combine(lineDirectory, key);\n            if (File.Exists(relativeToLine))\n            {\n                path = relativeToLine;\n                return true;\n            }\n        }\n\n        if (File.Exists(key))\n        {\n            path = Path.GetFullPath(key);\n            return true;\n        }\n\n        var suffix = FileEx.PrependSlash(key);\n        string? endWith = null;\n        foreach (var file in allFiles)\n        {\n            if (file.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))\n            {\n                endWith = file;\n                break;\n            }\n        }\n        if (endWith != null)\n        {\n            path = endWith;\n            return true;\n        }\n\n        path = null!;\n        return false;\n    }\n\n    public static bool Find(\n        IReadOnlyList<string> allFiles,\n        string targetDirectory,\n        string key,\n        string? relativePath,\n        string? linePath,\n        [NotNullWhen(true)] out string? path)\n    {\n        if (!InnerFind(allFiles, targetDirectory, key, relativePath, linePath, out path))\n        {\n            return false;\n        }\n\n        path = FileEx.FixFileCapitalization(path);\n        return true;\n    }\n}"
  },
  {
    "path": "src/MarkdownSnippets/Processing/SimpleSnippetMarkdownHandling.cs",
    "content": "namespace MarkdownSnippets;\n\n/// <summary>\n/// Simple markdown handling to be passed to <see cref=\"MarkdownProcessor\"/>.\n/// </summary>\npublic static class SimpleSnippetMarkdownHandling\n{\n    public static void Append(string key, IEnumerable<Snippet> snippets, Action<string> appendLine)\n    {\n        foreach (var snippet in snippets)\n        {\n            WriteSnippet(appendLine, snippet);\n        }\n    }\n\n    static void WriteSnippet(Action<string> appendLine, Snippet snippet)\n    {\n        if (snippet.ExpressiveCode is null)\n        {\n            appendLine($\"```{snippet.Language}\");\n        }\n        else\n        {\n            appendLine($\"```{snippet.Language} {snippet.ExpressiveCode}\");\n        }\n\n        appendLine(snippet.Value);\n        appendLine(\"```\");\n    }\n}"
  },
  {
    "path": "src/MarkdownSnippets/Processing/SnippetKey.cs",
    "content": "static class SnippetKey\n{\n    public static bool ExtractStartCommentSnippet(Line line, [NotNullWhen(true)] out string? key)\n    {\n        var lineCurrent = line.Current.AsSpan().TrimStart();\n        if (!IsStartCommentSnippetLine(lineCurrent))\n        {\n            key = null;\n            return false;\n        }\n\n        var substring = lineCurrent[14..];\n        var indexOf = substring.IndexOf(\"-->\", StringComparison.Ordinal);\n        if (indexOf < 0)\n        {\n            throw new SnippetException($\"Could not find closing '-->' in: {line.Original}. Path: {line.Path}. Line: {line.LineNumber}\");\n        }\n\n        key = substring[..indexOf].Trim().ToString();\n        return true;\n    }\n\n    public static bool ExtractStartCommentWebSnippet(Line line, [NotNullWhen(true)] out string? url, [NotNullWhen(true)] out string? snippetKey) =>\n        ExtractStartCommentWebSnippet(line, out url, out snippetKey, out _);\n\n    public static bool ExtractStartCommentWebSnippet(Line line, [NotNullWhen(true)] out string? url, [NotNullWhen(true)] out string? snippetKey, out string? viewUrl)\n    {\n        var lineCurrent = line.Current.AsSpan().TrimStart();\n        if (!IsStartCommentWebSnippetLine(lineCurrent))\n        {\n            url = null;\n            snippetKey = null;\n            viewUrl = null;\n            return false;\n        }\n\n        var substring = lineCurrent[18..]; // after \"<!-- web-snippet: \"\n        var indexOf = substring.IndexOf(\"-->\", StringComparison.Ordinal);\n        if (indexOf < 0)\n        {\n            throw new SnippetException($\"Could not find closing '-->' in: {line.Original}. Path: {line.Path}. Line: {line.LineNumber}\");\n        }\n\n        var value = substring[..indexOf].Trim();\n\n        // Check for optional second URL separated by whitespace\n        var firstSpaceIndex = value.IndexOfAny([' ', '\\t']);\n        CharSpan firstPart;\n        if (firstSpaceIndex >= 0)\n        {\n            firstPart = value[..firstSpaceIndex];\n            var secondPart = value[(firstSpaceIndex + 1)..].TrimStart();\n            var nextSpace = secondPart.IndexOfAny([' ', '\\t']);\n            viewUrl = (nextSpace >= 0 ? secondPart[..nextSpace] : secondPart).ToString();\n        }\n        else\n        {\n            firstPart = value;\n            viewUrl = null;\n        }\n\n        var hashIndex = firstPart.LastIndexOf('#');\n        if (hashIndex < 0 || hashIndex == firstPart.Length - 1)\n        {\n            url = null;\n            snippetKey = null;\n            viewUrl = null;\n            return false;\n        }\n        url = firstPart[..hashIndex].ToString();\n        snippetKey = firstPart[(hashIndex + 1)..].ToString();\n        return true;\n    }\n\n    public static bool ExtractSnippet(Line line, [NotNullWhen(true)] out string? key)\n    {\n        var lineCurrent = line.Current.AsSpan().TrimStart();\n        if (!IsSnippetLine(lineCurrent))\n        {\n            key = null;\n            return false;\n        }\n\n        var keySpan = lineCurrent[8..].Trim();\n        if (keySpan.IsWhiteSpace())\n        {\n            throw new SnippetException($\"Could not parse snippet from: {line.Original}. Path: {line.Path}. Line: {line.LineNumber}\");\n        }\n\n        key = keySpan.ToString();\n        return true;\n    }\n\n    public static bool ExtractWebSnippet(Line line, [NotNullWhen(true)] out string? url, [NotNullWhen(true)] out string? snippetKey) =>\n        ExtractWebSnippet(line, out url, out snippetKey, out _);\n\n    public static bool ExtractWebSnippet(Line line, [NotNullWhen(true)] out string? url, [NotNullWhen(true)] out string? snippetKey, out string? viewUrl)\n    {\n        var lineCurrent = line.Current.AsSpan().TrimStart();\n        if (!IsWebSnippetLine(lineCurrent))\n        {\n            url = null;\n            snippetKey = null;\n            viewUrl = null;\n            return false;\n        }\n        var value = lineCurrent[12..].Trim(); // after 'web-snippet:'\n\n        // Check for optional second URL separated by whitespace\n        var firstSpaceIndex = value.IndexOfAny([' ', '\\t']);\n        CharSpan firstPart;\n        if (firstSpaceIndex >= 0)\n        {\n            firstPart = value[..firstSpaceIndex];\n            var secondPart = value[(firstSpaceIndex + 1)..].TrimStart();\n            var nextSpace = secondPart.IndexOfAny([' ', '\\t']);\n            viewUrl = (nextSpace >= 0 ? secondPart[..nextSpace] : secondPart).ToString();\n        }\n        else\n        {\n            firstPart = value;\n            viewUrl = null;\n        }\n\n        var hashIndex = firstPart.LastIndexOf('#');\n        if (hashIndex < 0 || hashIndex == firstPart.Length - 1)\n        {\n            url = null;\n            snippetKey = null;\n            viewUrl = null;\n            return false;\n        }\n        url = firstPart[..hashIndex].ToString();\n        snippetKey = firstPart[(hashIndex + 1)..].ToString();\n        return true;\n    }\n\n    public static bool IsSnippetLine(string line) =>\n        IsSnippetLine(line.AsSpan());\n\n    public static bool IsSnippetLine(CharSpan line) =>\n        line.StartsWith(\"snippet:\", StringComparison.OrdinalIgnoreCase);\n\n    public static bool IsStartCommentSnippetLine(string line) =>\n        IsStartCommentSnippetLine(line.AsSpan());\n\n    public static bool IsStartCommentSnippetLine(CharSpan line) =>\n        line.StartsWith(\"<!-- snippet:\", StringComparison.OrdinalIgnoreCase);\n\n    public static bool IsWebSnippetLine(string line) =>\n        IsWebSnippetLine(line.AsSpan());\n\n    public static bool IsWebSnippetLine(CharSpan line) =>\n        line.StartsWith(\"web-snippet:\", StringComparison.OrdinalIgnoreCase);\n\n    public static bool IsStartCommentWebSnippetLine(string line) =>\n        IsStartCommentWebSnippetLine(line.AsSpan());\n\n    public static bool IsStartCommentWebSnippetLine(CharSpan line) =>\n        line.StartsWith(\"<!-- web-snippet:\", StringComparison.OrdinalIgnoreCase);\n}\n"
  },
  {
    "path": "src/MarkdownSnippets/Processing/SnippetMarkdownHandling.cs",
    "content": "namespace MarkdownSnippets;\n\n/// <summary>\n/// Handling to be passed to <see cref=\"MarkdownProcessor\"/>.\n/// </summary>\npublic class SnippetMarkdownHandling\n{\n    LinkFormat linkFormat;\n    bool omitSnippetLinks;\n    string? urlPrefix;\n    string targetDirectory;\n\n    public SnippetMarkdownHandling(string targetDirectory, LinkFormat linkFormat, bool omitSnippetLinks, string? urlPrefix = null)\n    {\n        this.linkFormat = linkFormat;\n        this.omitSnippetLinks = omitSnippetLinks;\n        this.urlPrefix = urlPrefix;\n        Guard.AgainstNullAndEmpty(targetDirectory, nameof(targetDirectory));\n        targetDirectory = Path.GetFullPath(targetDirectory);\n        this.targetDirectory = targetDirectory.Replace('\\\\', '/');\n    }\n\n    public void Append(string key, IEnumerable<Snippet> snippets, Action<string> appendLine)\n    {\n        Guard.AgainstNullAndEmpty(key, nameof(key));\n        uint index = 0;\n        foreach (var snippet in snippets)\n        {\n            WriteSnippet(appendLine, snippet, index);\n            index++;\n        }\n    }\n\n    void WriteSnippet(Action<string> appendLine, Snippet snippet, uint index)\n    {\n        if (omitSnippetLinks)\n        {\n            WriteSnippetValueAndLanguage(appendLine, snippet);\n            return;\n        }\n\n        var anchor = GetAnchorText(snippet, index);\n\n        appendLine($\"<a id='{anchor}'></a>\");\n        WriteSnippetValueAndLanguage(appendLine, snippet);\n\n        var supText = GetSupText(snippet, anchor);\n        appendLine($\"<sup>{supText}</sup>\");\n    }\n\n    static string GetAnchorText(Snippet snippet, uint index)\n    {\n        string id;\n        var pathStr = snippet.Path;\n        if (pathStr != null && pathStr.StartsWith(\"http\", StringComparison.OrdinalIgnoreCase))\n        {\n            // For web-snippets, include the full URL and the snippet key, with '#' encoded\n            var combined = $\"snippet-{pathStr}#{snippet.Key}\";\n            id = combined.Replace(\"#\", \"%23\");\n        }\n        else\n        {\n            id = $\"snippet-{snippet.Key}\";\n        }\n\n        if (index == 0)\n        {\n            return id;\n        }\n\n        return $\"{id}-{index}\";\n    }\n\n    string GetSupText(Snippet snippet, string anchor)\n    {\n        var linkForAnchor = $\"<a href='#{anchor}' title='Start of snippet'>anchor</a>\";\n        var pathLocal = snippet.Path;\n        if (pathLocal == null || linkFormat == LinkFormat.None)\n        {\n            return linkForAnchor;\n        }\n\n        var path = pathLocal.Replace('\\\\', '/');\n        if (path.StartsWith(\"http\", StringComparison.OrdinalIgnoreCase))\n        {\n            // For web-snippets: use ViewUrl if provided, otherwise link back to the original URL with the snippet key as a hash\n            if (snippet.ViewUrl != null)\n            {\n                return $\"<a href='{snippet.ViewUrl}#L{snippet.StartLine}-L{snippet.EndLine}' title='Snippet source file'>anchor</a>\";\n            }\n            return $\"<a href='{path}#{snippet.Key}' title='Snippet source file'>anchor</a>\";\n        }\n        if (!path.StartsWith(targetDirectory))\n        {\n            // if file is not in the targetDirectory then the url wont work\n            return linkForAnchor;\n        }\n\n        path = path[targetDirectory.Length..];\n\n        var builder = new StringBuilder($\"<a href='{urlPrefix}\");\n        BuildLink(snippet, path, builder);\n        Polyfill.Append(\n            builder,\n            $\"' title='Snippet source file'>snippet source</a> | {linkForAnchor}\");\n        return builder.ToString();\n    }\n\n    static void WriteSnippetValueAndLanguage(Action<string> appendLine, Snippet snippet)\n    {\n        if (snippet.ExpressiveCode is null)\n        {\n            appendLine($\"```{snippet.Language}\");\n        }\n        else\n        {\n            appendLine($\"```{snippet.Language} {snippet.ExpressiveCode}\");\n        }\n        appendLine(snippet.Value);\n        appendLine(\"```\");\n    }\n\n    void BuildLink(Snippet snippet, string path, StringBuilder builder)\n    {\n        if (linkFormat == LinkFormat.DevOps)\n        {\n            path = WebUtility.UrlEncode(path)!;\n        }\n        else\n        {\n            path = path.Replace(\"#\", \"%23\");\n        }\n        #region BuildLink\n        switch (linkFormat)\n        {\n            case LinkFormat.GitHub:\n                Polyfill.Append(builder, $\"{path}#L{snippet.StartLine}-L{snippet.EndLine}\");\n                return;\n            case LinkFormat.Tfs:\n                Polyfill.Append(builder, $\"{path}&line={snippet.StartLine}&lineEnd={snippet.EndLine}\");\n                return;\n            case LinkFormat.Bitbucket:\n                Polyfill.Append(builder, $\"{path}#lines={snippet.StartLine}:{snippet.EndLine}\");\n                return;\n            case LinkFormat.GitLab:\n                Polyfill.Append(builder, $\"{path}#L{snippet.StartLine}-{snippet.EndLine}\");\n                return;\n            case LinkFormat.DevOps:\n                Polyfill.Append(builder, $\"?path={path}&line={snippet.StartLine}&lineEnd={snippet.EndLine}&lineStartColumn=1&lineEndColumn=999\");\n                return;\n            case LinkFormat.None:\n                throw new($\"Unknown LinkFormat: {linkFormat}\");\n        }\n        #endregion\n    }\n}\n"
  },
  {
    "path": "src/MarkdownSnippets/Processing/TocBuilder.cs",
    "content": "static class TocBuilder\n{\n    public static string BuildToc(List<Line> headerLines, int level, List<string> tocExcludes, string newLine)\n    {\n        var excludesSet = new HashSet<string>(tocExcludes, StringComparer.OrdinalIgnoreCase);\n        var processed = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);\n        var builder = new StringBuilder();\n        builder.Append(\"<!-- toc -->\");\n        builder.Append(newLine);\n        builder.Append(\"## Contents\");\n        builder.Append(newLine);\n        builder.Append(newLine);\n        const int startingLevel = 2;\n        var headerDepth = level + startingLevel - 1;\n        var headingCount = 0;\n        foreach (var headerLine in headerLines)\n        {\n            var current = headerLine.Current;\n            var trimmedHash = current.TrimStart('#');\n            if (!trimmedHash.StartsWith(' '))\n            {\n                continue;\n            }\n\n            var headerLevel = current.Length - trimmedHash.Length;\n            if (headerLevel == 1)\n            {\n                continue;\n            }\n\n            if (headerLevel > headerDepth)\n            {\n                continue;\n            }\n\n            var title = GetTitle(trimmedHash);\n            if (excludesSet.Contains(title))\n            {\n                continue;\n            }\n\n            headingCount++;\n\n            builder.Append(' ', (headerLevel - 1) * 2);\n            builder.Append(\"* [\");\n            builder.Append(title);\n            builder.Append(\"](#\");\n            BuildLink(builder, processed, title);\n            builder.Append(')');\n            builder.Append(newLine);\n        }\n\n        if (headingCount == 0)\n        {\n            return $\"<!-- toc -->{newLine}<!-- endToc -->\";\n        }\n\n        builder.TrimEnd();\n        builder.Append(\"<!-- endToc -->\");\n\n        return builder.ToString();\n    }\n\n    static string GetTitle(string current)\n    {\n        var trim = current[1..].Trim();\n        return Markdown.StripMarkdown(trim);\n    }\n\n    static void BuildLink(StringBuilder builder, Dictionary<string, int> processed, string title)\n    {\n        processed.TryGetValue(title, out var processedCount);\n        processed[title] = processedCount + 1;\n        SanitizeLink(builder, title);\n        if (processedCount > 0)\n        {\n            builder.Append('-');\n            builder.Append(processedCount);\n        }\n    }\n\n    internal static void SanitizeLink(StringBuilder builder, CharSpan title)\n    {\n        foreach (var ch in title)\n        {\n            if (char.IsLetterOrDigit(ch) || ch is '-' or '_')\n            {\n                builder.Append(char.ToLowerInvariant(ch));\n            }\n            else if (char.IsWhiteSpace(ch))\n            {\n                builder.Append('-');\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/MarkdownSnippets/Processing/ValidationError.cs",
    "content": "namespace MarkdownSnippets;\n\n/// <summary>\n/// Part of <see cref=\"ProcessResult\"/>.\n/// </summary>\n[DebuggerDisplay(\"Error={Error}, Line={Line}:{Column}\")]\npublic class ValidationError\n{\n    /// <summary>\n    /// Initialise a new instance of <see cref=\"ValidationError\"/>.\n    /// </summary>\n    public ValidationError(string error, int line, int column, string? file)\n    {\n        Guard.AgainstNullAndEmpty(error, nameof(error));\n        Guard.AgainstNegativeAndZero(line, nameof(line));\n        Guard.AgainstNegative(column, nameof(column));\n        Guard.AgainstEmpty(file, nameof(file));\n        Error = error;\n        Line = line;\n        Column = column;\n        File = file;\n    }\n\n    /// <summary>\n    /// The error.\n    /// </summary>\n    public string Error { get; }\n\n    /// <summary>\n    /// The line number in the input text.\n    /// </summary>\n    public int Line { get; }\n\n    /// <summary>\n    /// The column number in the line.\n    /// </summary>\n    public int Column { get; }\n\n    /// <summary>\n    /// The File.\n    /// </summary>\n    public string? File { get; }\n\n    public override string ToString()\n    {\n        if (File == null)\n        {\n            return $\"\"\"\n                    ContentError.\n                      Line: {Line}\n                      Column: {Column}\n                      Error: {Error}\n                    \"\"\";\n        }\n\n        return $\"\"\"\n                ContentError.\n                  File: {File}\n                  Line: {Line}\n                  Column: {Column}\n                  Error: {Error}\n                \"\"\";\n    }\n}"
  },
  {
    "path": "src/MarkdownSnippets/Reading/EndFunc.cs",
    "content": "delegate bool EndFunc(CharSpan line);"
  },
  {
    "path": "src/MarkdownSnippets/Reading/Exclusions/DefaultDirectoryExclusions.cs",
    "content": "﻿namespace MarkdownSnippets;\n\npublic static class DefaultDirectoryExclusions\n{\n    public static bool ShouldExcludeDirectory(string path)\n    {\n        var suffix = Path\n            .GetFileName(path)\n            .ToLowerInvariant();\n        if (suffix is\n            // source control\n            \".git\" or\n\n            // ide temp files\n            \".vs\" or\n            \".vscode\" or\n            \".idea\" or\n\n            // package cache\n            \"packages\" or\n            \"node_modules\" or\n\n            // build output\n            \"dist\" or\n            \".angular\" or\n            \"bin\" or\n            \"obj\")\n        {\n            return true;\n        }\n\n        var directory = new DirectoryInfo(path);\n        return directory.Attributes.HasFlag(FileAttributes.Hidden);\n    }\n}"
  },
  {
    "path": "src/MarkdownSnippets/Reading/Exclusions/SnippetFileExclusions.cs",
    "content": "﻿namespace MarkdownSnippets;\n\npublic static class SnippetFileExclusions\n{\n    public static bool IsBinary(string extension) =>\n        binaryFileExtensionsFrozen.Contains(extension);\n\n    public static bool CanContainCommentsExtension(string extension) =>\n        !noAcceptCommentsExtensionsFrozen.Contains(extension);\n\n    static FrozenSet<string> noAcceptCommentsExtensionsFrozen = FrozenSet.Create(\n        StringComparer.OrdinalIgnoreCase,\n        source:\n        [\n            //files that dont accept comments hence cant contain snippets\n\n            #region NoAcceptCommentsExtensions\n\n            \"DotSettings\",\n            \"csv\",\n            \"json\",\n            \"geojson\",\n            \"sln\"\n\n            #endregion\n        ]);\n\n    public static void AddNoAcceptCommentsExtensions(params string[] extensions)\n    {\n        var set = new HashSet<string>(noAcceptCommentsExtensionsFrozen, StringComparer.OrdinalIgnoreCase);\n        foreach (var extension in extensions)\n        {\n            set.Add(extension);\n        }\n\n        noAcceptCommentsExtensionsFrozen = set.ToFrozenSet(StringComparer.OrdinalIgnoreCase);\n    }\n\n    public static void RemoveNoAcceptCommentsExtensions(params string[] extensions)\n    {\n        var set = new HashSet<string>(noAcceptCommentsExtensionsFrozen, StringComparer.OrdinalIgnoreCase);\n        foreach (var extension in extensions)\n        {\n            set.Remove(extension);\n        }\n\n        noAcceptCommentsExtensionsFrozen = set.ToFrozenSet(StringComparer.OrdinalIgnoreCase);\n    }\n\n    static FrozenSet<string> binaryFileExtensionsFrozen = FrozenSet.Create(\n        StringComparer.OrdinalIgnoreCase,\n        source:\n        [\n            #region BinaryFileExtensions\n\n            \"user\",\n            // extra binary\n            \"mdb\",\n            \"binlog\",\n            \"shp\",\n            \"dbf\",\n            \"shx\",\n            \"pbf\",\n            \"map\",\n            \"sbn\",\n\n            //from https://github.com/sindresorhus/binary-extensions/blob/master/binary-extensions.json\n            \"3dm\",\n            \"3ds\",\n            \"3g2\",\n            \"3gp\",\n            \"7z\",\n            \"a\",\n            \"aac\",\n            \"adp\",\n            \"ai\",\n            \"aif\",\n            \"aiff\",\n            \"alz\",\n            \"ape\",\n            \"apk\",\n            \"appimage\",\n            \"ar\",\n            \"arj\",\n            \"asf\",\n            \"au\",\n            \"avi\",\n            \"bak\",\n            \"baml\",\n            \"bh\",\n            \"bin\",\n            \"bk\",\n            \"bmp\",\n            \"btif\",\n            \"bz2\",\n            \"bzip2\",\n            \"cab\",\n            \"caf\",\n            \"cgm\",\n            \"class\",\n            \"cmx\",\n            \"cpio\",\n            \"cr2\",\n            \"cur\",\n            \"dat\",\n            \"dcm\",\n            \"deb\",\n            \"dex\",\n            \"djvu\",\n            \"dll\",\n            \"dmg\",\n            \"dng\",\n            \"doc\",\n            \"docm\",\n            \"docx\",\n            \"dot\",\n            \"dotm\",\n            \"dra\",\n            \"DS_Store\",\n            \"dsk\",\n            \"dts\",\n            \"dtshd\",\n            \"dvb\",\n            \"dwg\",\n            \"dxf\",\n            \"ecelp4800\",\n            \"ecelp7470\",\n            \"ecelp9600\",\n            \"egg\",\n            \"eol\",\n            \"eot\",\n            \"epub\",\n            \"exe\",\n            \"f4v\",\n            \"fbs\",\n            \"fh\",\n            \"fla\",\n            \"flac\",\n            \"flatpak\",\n            \"fli\",\n            \"flv\",\n            \"fpx\",\n            \"fst\",\n            \"fvt\",\n            \"g3\",\n            \"gh\",\n            \"gif\",\n            \"graffle\",\n            \"gz\",\n            \"gzip\",\n            \"h261\",\n            \"h263\",\n            \"h264\",\n            \"icns\",\n            \"ico\",\n            \"ief\",\n            \"img\",\n            \"ipa\",\n            \"iso\",\n            \"jar\",\n            \"jpeg\",\n            \"jpg\",\n            \"jpgv\",\n            \"jpm\",\n            \"jxr\",\n            \"key\",\n            \"ktx\",\n            \"lha\",\n            \"lib\",\n            \"lvp\",\n            \"lz\",\n            \"lzh\",\n            \"lzma\",\n            \"lzo\",\n            \"m3u\",\n            \"m4a\",\n            \"m4v\",\n            \"mar\",\n            \"mdi\",\n            \"mht\",\n            \"mid\",\n            \"midi\",\n            \"mj2\",\n            \"mka\",\n            \"mkv\",\n            \"mmr\",\n            \"mng\",\n            \"mobi\",\n            \"mov\",\n            \"movie\",\n            \"mp3\",\n            \"mp4\",\n            \"mp4a\",\n            \"mpeg\",\n            \"mpg\",\n            \"mpga\",\n            \"mxu\",\n            \"nef\",\n            \"npx\",\n            \"numbers\",\n            \"nupkg\",\n            \"o\",\n            \"oga\",\n            \"ogg\",\n            \"ogv\",\n            \"otf\",\n            \"pages\",\n            \"pbm\",\n            \"pcx\",\n            \"pdb\",\n            \"pdf\",\n            \"pea\",\n            \"pgm\",\n            \"pic\",\n            \"png\",\n            \"pnm\",\n            \"pot\",\n            \"potm\",\n            \"potx\",\n            \"ppa\",\n            \"ppam\",\n            \"ppm\",\n            \"pps\",\n            \"ppsm\",\n            \"ppsx\",\n            \"ppt\",\n            \"pptm\",\n            \"pptx\",\n            \"psd\",\n            \"pya\",\n            \"pyc\",\n            \"pyo\",\n            \"pyv\",\n            \"qt\",\n            \"rar\",\n            \"ras\",\n            \"raw\",\n            \"resources\",\n            \"rgb\",\n            \"rip\",\n            \"rlc\",\n            \"rmf\",\n            \"rmvb\",\n            \"rpm\",\n            \"rtf\",\n            \"rz\",\n            \"s3m\",\n            \"s7z\",\n            \"scpt\",\n            \"sgi\",\n            \"shar\",\n            \"snap\",\n            \"sil\",\n            \"sketch\",\n            \"slk\",\n            \"smv\",\n            \"snk\",\n            \"so\",\n            \"stl\",\n            \"suo\",\n            \"sub\",\n            \"swf\",\n            \"tar\",\n            \"tbz\",\n            \"tbz2\",\n            \"tga\",\n            \"tgz\",\n            \"thmx\",\n            \"tif\",\n            \"tiff\",\n            \"tlz\",\n            \"ttc\",\n            \"ttf\",\n            \"txz\",\n            \"udf\",\n            \"uvh\",\n            \"uvi\",\n            \"uvm\",\n            \"uvp\",\n            \"uvs\",\n            \"uvu\",\n            \"viv\",\n            \"vob\",\n            \"war\",\n            \"wav\",\n            \"wax\",\n            \"wbmp\",\n            \"wdp\",\n            \"weba\",\n            \"webm\",\n            \"webp\",\n            \"whl\",\n            \"wim\",\n            \"wm\",\n            \"wma\",\n            \"wmv\",\n            \"wmx\",\n            \"woff\",\n            \"woff2\",\n            \"wrm\",\n            \"wvx\",\n            \"xbm\",\n            \"xif\",\n            \"xla\",\n            \"xlam\",\n            \"xls\",\n            \"xlsb\",\n            \"xlsm\",\n            \"xlsx\",\n            \"xlt\",\n            \"xltm\",\n            \"xltx\",\n            \"xm\",\n            \"xmind\",\n            \"xpi\",\n            \"xpm\",\n            \"xwd\",\n            \"xz\",\n            \"z\",\n            \"zip\",\n            \"zipx\"\n\n            #endregion\n        ]);\n\n    public static void AddBinaryFileExtensions(params string[] extensions)\n    {\n        var set = new HashSet<string>(binaryFileExtensionsFrozen, StringComparer.OrdinalIgnoreCase);\n        foreach (var extension in extensions)\n        {\n            set.Add(extension);\n        }\n\n        binaryFileExtensionsFrozen = set.ToFrozenSet(StringComparer.OrdinalIgnoreCase);\n    }\n\n    public static void RemoveBinaryFileExtensions(params string[] extensions)\n    {\n        var set = new HashSet<string>(binaryFileExtensionsFrozen, StringComparer.OrdinalIgnoreCase);\n        foreach (var extension in extensions)\n        {\n            set.Remove(extension);\n        }\n\n        binaryFileExtensionsFrozen = set.ToFrozenSet(StringComparer.OrdinalIgnoreCase);\n    }\n}\n"
  },
  {
    "path": "src/MarkdownSnippets/Reading/FileFinder.cs",
    "content": "﻿class FileFinder(\n    string targetDirectory,\n    DocumentConvention convention,\n    ShouldIncludeDirectory directoryIncludes,\n    ShouldIncludeDirectory markdownDirectoryIncludes,\n    ShouldIncludeDirectory snippetDirectoryIncludes,\n    ShouldIncludeFile? snippetFileIncludes = null)\n{\n    List<string> snippetFiles = [];\n    List<string> mdFiles = [];\n    List<string> allFiles = [];\n    List<string> includeFiles = [];\n\n    public (List<string> snippetFiles, List<string> mdFiles, List<string> includeFiles, List<string> allFiles) FindFiles()\n    {\n        ProcessFiles(targetDirectory);\n        foreach (var subDirectory in Directory.EnumerateDirectories(targetDirectory)\n                     .Where(path => directoryIncludes(path)))\n        {\n            FindFiles(subDirectory);\n        }\n\n        snippetFiles.Sort(StringComparer.Ordinal);\n        mdFiles.Sort(StringComparer.Ordinal);\n        includeFiles.Sort(StringComparer.Ordinal);\n        allFiles.Sort(StringComparer.Ordinal);\n        return (snippetFiles, mdFiles, includeFiles, allFiles);\n    }\n\n    void FindFiles(string directory)\n    {\n        ProcessFiles(directory);\n\n        foreach (var subDirectory in Directory.EnumerateDirectories(directory)\n                     .Where(path => directoryIncludes(path)))\n        {\n            FindFiles(subDirectory);\n        }\n    }\n\n    void ProcessFiles(string directory)\n    {\n        var scanForMarkdown = markdownDirectoryIncludes(directory);\n        var scanForSnippets = snippetDirectoryIncludes(directory);\n        if (scanForSnippets && scanForMarkdown)\n        {\n            foreach (var file in EnumerateFiles(directory))\n            {\n                allFiles.Add(file);\n\n                if (file.IsMdFile())\n                {\n                    ProcessMarkdown(file);\n\n                    continue;\n                }\n\n                if (IncludeAsSnippet(file))\n                {\n                    snippetFiles.Add(file);\n                }\n            }\n\n            return;\n        }\n\n        if (scanForSnippets)\n        {\n            foreach (var file in EnumerateFiles(directory)\n                         .Where(_ => !_.IsMdFile()))\n            {\n                allFiles.Add(file);\n                if (IncludeAsSnippet(file))\n                {\n                    snippetFiles.Add(file);\n                }\n            }\n\n            return;\n        }\n\n        if (scanForMarkdown)\n        {\n            foreach (var file in EnumerateFiles(directory)\n                         .Where(Paths.IsMdFile))\n            {\n                allFiles.Add(file);\n                ProcessMarkdown(file);\n            }\n        }\n    }\n\n    bool IncludeAsSnippet(string file) =>\n        snippetFileIncludes == null || snippetFileIncludes(file);\n\n    static IEnumerable<string> EnumerateFiles(string directory) =>\n        Directory.EnumerateFiles(directory)\n            .Where(ShouldInclude);\n\n    void ProcessMarkdown(string file)\n    {\n        if (file.IsIncludeMdFile())\n        {\n            includeFiles.Add(file);\n            return;\n        }\n\n        if (convention != DocumentConvention.SourceTransform)\n        {\n            mdFiles.Add(file);\n            return;\n        }\n\n        if (file.IsSourceMdFile())\n        {\n            mdFiles.Add(file);\n        }\n    }\n\n    static bool ShouldInclude(string file)\n    {\n        var extension = FileSnippetExtractor.GetLanguageFromPath(file);\n        if (extension == string.Empty)\n        {\n            return false;\n        }\n\n        return !SnippetFileExclusions.IsBinary(extension);\n    }\n}"
  },
  {
    "path": "src/MarkdownSnippets/Reading/FileSnippetExtractor.cs",
    "content": "﻿namespace MarkdownSnippets;\n\n/// <summary>\n/// Extracts <see cref=\"Snippet\"/>s from a given input.\n/// </summary>\npublic static class FileSnippetExtractor\n{\n    /// <summary>\n    /// Each url will be accessible using the file name as a key. Any snippets within the files will be extracted and accessible as individual keyed snippets.\n    /// </summary>\n    public static Task AppendUrlAsSnippet(this ICollection<Snippet> snippets, string url)\n    {\n        Guard.AgainstNullAndEmpty(url, nameof(url));\n        return AppendUrlAsSnippet(snippets, url, Path.GetFileName(url).ToLowerInvariant());\n    }\n\n    /// <summary>\n    /// Each url will be accessible using the file name as a key. Any snippets within the files will be extracted and accessible as individual keyed snippets.\n    /// </summary>\n    public static Task AppendUrlsAsSnippets(this ICollection<Snippet> snippets, params string[] urls) =>\n        snippets.AppendUrlsAsSnippets((IEnumerable<string>) urls);\n\n    /// <summary>\n    /// Each url will be accessible using the file name as a key. Any snippets within the files will be extracted and accessible as individual keyed snippets.\n    /// </summary>\n    public static async Task AppendUrlsAsSnippets(this ICollection<Snippet> snippets, IEnumerable<string> urls)\n    {\n        foreach (var url in urls)\n        {\n            await snippets.AppendUrlAsSnippet(url);\n        }\n    }\n\n    /// <summary>\n    /// The url will be accessible using the file name as a key. Any snippets within the file will be extracted and accessible as individual keyed snippets.\n    /// </summary>\n    public static async Task AppendUrlAsSnippet(ICollection<Snippet> snippets, string url, string key)\n    {\n        Guard.AgainstNullAndEmpty(url, nameof(url));\n        var (success, content) = await Downloader.DownloadContent(url);\n        if (!success)\n        {\n            throw new SnippetException($\"Unable to get UrlAsSnippet: {url}\");\n        }\n\n        var snippet = Snippet.Build(1, content!.LineCount(), content!, key, GetLanguageFromPath(url), url, null);\n        snippets.Add(snippet);\n\n        using var reader = new StringReader(content!);\n        foreach (var innerSnippet in Read(reader, url))\n        {\n            snippets.Add(innerSnippet);\n        }\n    }\n\n    public static void AppendFileAsSnippet(this ICollection<Snippet> snippets, string filePath)\n    {\n        Guard.FileExists(filePath, nameof(filePath));\n        AppendFileAsSnippet(snippets, filePath, Path.GetFileName(filePath).ToLowerInvariant());\n    }\n\n    public static void AppendFilesAsSnippets(this ICollection<Snippet> snippets, params string[] filePaths)\n    {\n        foreach (var filePath in filePaths)\n        {\n            snippets.AppendFileAsSnippet(filePath);\n        }\n    }\n\n    public static void AppendFileAsSnippet(ICollection<Snippet> snippets, string filePath, string key)\n    {\n        Guard.FileExists(filePath, nameof(filePath));\n        var text = File.ReadAllText(filePath);\n        var snippet = Snippet.Build(1, text.LineCount(), text, key, GetLanguageFromPath(filePath), filePath, null);\n        snippets.Add(snippet);\n    }\n\n    /// <summary>\n    /// Read from paths.\n    /// </summary>\n    /// <param name=\"paths\">The paths to extract <see cref=\"Snippet\"/>s from.</param>\n    /// <param name=\"maxWidth\">Controls the maximum character width for snippets. Must be positive.</param>\n    /// <param name=\"newLine\">The string to use as a line separator in snippets.</param>\n    public static IEnumerable<Snippet> Read(IEnumerable<string> paths, int maxWidth = int.MaxValue, string newLine = \"\\n\") =>\n        paths\n            .Where(_ => SnippetFileExclusions.CanContainCommentsExtension(GetLanguageFromPath(_)))\n            .SelectMany(path => Read(path, maxWidth, newLine));\n\n    /// <summary>\n    /// Read from a path.\n    /// </summary>\n    /// <param name=\"path\">The current path to extract <see cref=\"Snippet\"/>s from.</param>\n    /// <param name=\"maxWidth\">Controls the maximum character width for snippets. Must be positive.</param>\n    /// <param name=\"newLine\">The string to use as a line separator in snippets.</param>\n    public static IEnumerable<Snippet> Read(string path, int maxWidth = int.MaxValue, string newLine = \"\\n\")\n    {\n        Guard.AgainstNegativeAndZero(maxWidth, nameof(maxWidth));\n        Guard.AgainstNullAndEmpty(path, nameof(path));\n        if (!File.Exists(path))\n        {\n            return [];\n        }\n\n        using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);\n        using var reader = new StreamReader(stream);\n        return Read(reader, path, maxWidth, newLine).ToList();\n    }\n\n    /// <summary>\n    /// Read from a <see cref=\"TextReader\"/>.\n    /// </summary>\n    /// <param name=\"textReader\">The <see cref=\"TextReader\"/> to read from.</param>\n    /// <param name=\"path\">The current path being used to extract <see cref=\"Snippet\"/>s from. Only used for logging purposes in this overload.</param>\n    /// <param name=\"maxWidth\">Controls the maximum character width for snippets. Must be positive.</param>\n    /// <param name=\"newLine\">The string to use as a line separator in snippets.</param>\n    public static IEnumerable<Snippet> Read(TextReader textReader, string path, int maxWidth = int.MaxValue, string newLine = \"\\n\")\n    {\n        Guard.AgainstNegativeAndZero(maxWidth, nameof(maxWidth));\n        Guard.AgainstNullAndEmpty(path, nameof(path));\n        return GetSnippets(textReader, path, maxWidth, newLine);\n    }\n\n    public static string GetLanguageFromPath(string path)\n    {\n        var extension = Path.GetExtension(path);\n        // ReSharper disable once ConstantConditionalAccessQualifier\n        var s = extension?.TrimStart('.');\n        return s?.ToLowerInvariant() ?? string.Empty;\n    }\n\n    static IEnumerable<Snippet> GetSnippets(TextReader stringReader, string path, int maxWidth, string newLine)\n    {\n        var language = GetLanguageFromPath(path);\n        var loopStack = new LoopStack();\n        var index = 0;\n        while (true)\n        {\n            index++;\n            var line = stringReader.ReadLine();\n            if (line == null)\n            {\n                if (loopStack.IsInSnippet)\n                {\n                    var current = loopStack.Current;\n                    yield return Snippet.BuildError(\n                        error: \"Snippet was not closed\",\n                        path: path,\n                        lineNumberInError: current.StartLine + 1,\n                        key: current.Key);\n                }\n\n                break;\n            }\n\n            var trimmedLine = line.AsSpan().Trim();\n\n            if (StartEndTester.IsStart(trimmedLine, path, out var key, out var endFunc, out var expressive, out var languageOverride))\n            {\n                loopStack.Push(endFunc, key, index, maxWidth, newLine, expressive, languageOverride);\n                continue;\n            }\n\n            if (!loopStack.IsInSnippet)\n            {\n                continue;\n            }\n\n            if (!loopStack.Current.EndFunc(trimmedLine))\n            {\n                Snippet? error = null;\n                try\n                {\n                    loopStack.AppendLine(line);\n                }\n                catch (LineTooLongException exception)\n                {\n                    var current = loopStack.Current;\n                    error = Snippet.BuildError(\n                        error: \"Line too long: \" + exception.Line,\n                        path: path,\n                        lineNumberInError: current.StartLine + 1,\n                        key: current.Key);\n                }\n\n                if (error != null)\n                {\n                    yield return error;\n                    break;\n                }\n\n                continue;\n            }\n\n            yield return BuildSnippet(path, loopStack, language, index);\n            loopStack.Pop();\n        }\n    }\n\n    static Snippet BuildSnippet(string path, LoopStack loopStack, string language, int index)\n    {\n        var loopState = loopStack.Current;\n\n        var value = loopState.GetLines();\n\n        return Snippet.Build(\n            startLine: loopState.StartLine,\n            endLine: index,\n            key: loopState.Key,\n            value: value,\n            path: path,\n            language: loopState.Language ?? language,\n            expressiveCode: loopState.ExpressiveCode\n        );\n    }\n}\n"
  },
  {
    "path": "src/MarkdownSnippets/Reading/IContent.cs",
    "content": "﻿namespace MarkdownSnippets;\n\npublic interface IContent\n{\n    string? Path { get; }\n}"
  },
  {
    "path": "src/MarkdownSnippets/Reading/Include.cs",
    "content": "﻿namespace MarkdownSnippets;\n\n[DebuggerDisplay(\"Key={Key}, Path={Path}, Error={Error}\")]\npublic class Include :\n    IContent\n{\n    /// <summary>\n    /// Initialise a new instance of an in-error <see cref=\"Include\"/>.\n    /// </summary>\n    public static Include BuildError(string key, int lineNumberInError, string path, string error)\n    {\n        Guard.AgainstNegativeAndZero(lineNumberInError, nameof(lineNumberInError));\n        Guard.AgainstNullAndEmpty(key, nameof(key));\n        Guard.AgainstNullAndEmpty(error, nameof(error));\n        return new()\n        {\n            Key = key,\n            IsInError = true,\n            Path = path,\n            Error = error\n        };\n    }\n\n    /// <summary>\n    /// Initialise a new instance of <see cref=\"Include\"/>.\n    /// </summary>\n    public static Include Build(string key, IReadOnlyList<string> lines, string? path)\n    {\n        Guard.AgainstNullAndEmpty(key, nameof(key));\n        Guard.AgainstEmpty(path, nameof(path));\n        return new()\n        {\n            lines = lines,\n            Key = key,\n            Path = path,\n            Error = \"\"\n        };\n    }\n\n    public string Error { get; private init; } = null!;\n\n    public bool IsInError { get; private init; }\n\n    /// <summary>\n    /// The key used to identify the snippet.\n    /// </summary>\n    public string Key { get; private init; } = null!;\n\n    /// <summary>\n    /// The path the snippet was read from.\n    /// </summary>\n    public string? Path { get; private init; }\n\n    public IReadOnlyList<string> Lines\n    {\n        get\n        {\n            ThrowIfIsInError();\n            return lines!;\n        }\n    }\n\n    IReadOnlyList<string>? lines;\n\n    void ThrowIfIsInError()\n    {\n        if (IsInError)\n        {\n            throw new SnippetReadingException($\"Cannot access when {nameof(IsInError)}. Key: {Key}. Path: {Path}. Error: {Error}\");\n        }\n    }\n\n    public override string ToString() =>\n        $\"\"\"\n         ReadInclude.\n           Key: {Key}\n           Path: {Path}\n           Error: {Error}\n\n         \"\"\";\n}"
  },
  {
    "path": "src/MarkdownSnippets/Reading/LineTooLongException.cs",
    "content": "﻿class LineTooLongException(string line) :\n    Exception\n{\n    public string Line { get; } = line;\n}"
  },
  {
    "path": "src/MarkdownSnippets/Reading/LoopStack.cs",
    "content": "﻿[DebuggerDisplay(\"Depth={stack.Count}, IsInSnippet={IsInSnippet}\")]\nclass LoopStack\n{\n    public bool IsInSnippet => stack.Count > 0;\n\n    public LoopState Current => stack.Peek();\n\n    public void AppendLine(string line)\n    {\n        foreach (var state in stack)\n        {\n            state.AppendLine(line);\n        }\n    }\n\n    public void Pop() => stack.Pop();\n\n    public void Push(EndFunc endFunc, CharSpan key, int startLine, int maxWidth, string newLine, CharSpan expressiveCode, CharSpan language)\n    {\n        var expressiveCodeString = expressiveCode.Length == 0 ? null : expressiveCode.ToString();\n        var languageString = language.Length == 0 ? null : language.ToString();\n\n        var state = new LoopState(key.ToString(), endFunc, startLine, maxWidth, newLine, expressiveCodeString, languageString);\n        stack.Push(state);\n    }\n\n    Stack<LoopState> stack = [];\n}"
  },
  {
    "path": "src/MarkdownSnippets/Reading/LoopState.cs",
    "content": "﻿[DebuggerDisplay(\"Key={Key}\")]\nclass LoopState(string key, EndFunc endFunc, int startLine, int maxWidth, string newLine, string? expressiveCode = null, string? language = null)\n{\n    public string GetLines()\n    {\n        if (builder == null)\n        {\n            return string.Empty;\n        }\n\n        try\n        {\n            builder.TrimEnd();\n            return builder.ToString();\n        }\n        finally\n        {\n            StringBuilderCache.Release(builder);\n        }\n    }\n\n    public void AppendLine(string line)\n    {\n        builder ??= StringBuilderCache.Acquire();\n\n        if (builder.Length == 0)\n        {\n            if (line.IsWhiteSpace())\n            {\n                return;\n            }\n\n            CheckWhiteSpace(line, ' ');\n            CheckWhiteSpace(line, '\\t');\n        }\n        else\n        {\n            if (newlineCount < 2)\n            {\n                builder.Append(newLine);\n            }\n        }\n\n        var paddingToRemove = line.LastIndexOfSequence(paddingChar, paddingLength);\n\n        var lineLength = line.Length - paddingToRemove;\n        if (lineLength > maxWidth)\n        {\n            throw new LineTooLongException(line.AsSpan(paddingToRemove, lineLength).ToString());\n        }\n\n        builder.Append(line, paddingToRemove, lineLength);\n        if (line.Length == 0)\n        {\n            newlineCount++;\n        }\n        else\n        {\n            newlineCount = 0;\n        }\n    }\n\n    void CheckWhiteSpace(CharSpan line, char whiteSpace)\n    {\n        var c = line[0];\n        if (c != whiteSpace)\n        {\n            return;\n        }\n\n        paddingChar = whiteSpace;\n        for (var index = 1; index < line.Length; index++)\n        {\n            paddingLength++;\n            var ch = line[index];\n            if (ch != whiteSpace)\n            {\n                break;\n            }\n        }\n    }\n\n    StringBuilder? builder;\n    public string Key { get; } = key;\n    char paddingChar;\n    int paddingLength;\n    public EndFunc EndFunc { get; } = endFunc;\n    public int StartLine { get; } = startLine;\n    int newlineCount;\n    public string? ExpressiveCode { get; } = expressiveCode;\n    public string? Language { get; } = language;\n}"
  },
  {
    "path": "src/MarkdownSnippets/Reading/ReadSnippets.cs",
    "content": "﻿namespace MarkdownSnippets;\n\n[DebuggerDisplay(\"Count={Snippets.Count}\")]\npublic class ReadSnippets :\n    IEnumerable<Snippet>\n{\n    public IReadOnlyList<Snippet> Snippets { get; }\n    public IReadOnlyList<string> Files { get; }\n    public IReadOnlyDictionary<string, IReadOnlyList<Snippet>> Lookup { get; }\n    public IReadOnlyList<Snippet> SnippetsInError { get; }\n\n    public ReadSnippets(IReadOnlyList<Snippet> snippets, IReadOnlyList<string> files)\n    {\n        Snippets = snippets;\n        Files = files;\n        SnippetsInError = Snippets.Where(_ => _.IsInError).Distinct().ToList();\n        Lookup = Snippets.ToDictionary();\n    }\n\n    /// <summary>\n    /// Enumerates through the <see cref=\"Snippets\" /> but will first throw an exception if there are any <see cref=\"SnippetsInError\" />.\n    /// </summary>\n    public virtual IEnumerator<Snippet> GetEnumerator()\n    {\n        if (SnippetsInError.Count != 0)\n        {\n            throw new SnippetReadingException($\"SnippetsInError: {string.Join(\", \", SnippetsInError.Select(_ => _.Key))}\");\n        }\n\n        return Snippets.GetEnumerator();\n    }\n\n    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();\n}"
  },
  {
    "path": "src/MarkdownSnippets/Reading/ShouldIncludeDirectory.cs",
    "content": "﻿namespace MarkdownSnippets;\n\npublic delegate bool ShouldIncludeDirectory(string directoryPath);"
  },
  {
    "path": "src/MarkdownSnippets/Reading/ShouldIncludeFile.cs",
    "content": "namespace MarkdownSnippets;\n\npublic delegate bool ShouldIncludeFile(string filePath);\n"
  },
  {
    "path": "src/MarkdownSnippets/Reading/Snippet.cs",
    "content": "﻿namespace MarkdownSnippets;\n\n[DebuggerDisplay(\"Key={Key}, FileLocation={FileLocation}, Error={Error}\")]\npublic class Snippet :\n    IContent\n{\n    /// <summary>\n    /// Initialise a new instance of an in-error <see cref=\"Snippet\"/>.\n    /// </summary>\n    public static Snippet BuildError(string key, int lineNumberInError, string path, string error)\n    {\n        Guard.AgainstNegativeAndZero(lineNumberInError, nameof(lineNumberInError));\n        Guard.AgainstNullAndEmpty(key, nameof(key));\n        Guard.AgainstNullAndEmpty(error, nameof(error));\n        return new()\n        {\n            Key = key,\n            StartLine = lineNumberInError,\n            EndLine = lineNumberInError,\n            IsInError = true,\n            Path = path,\n            Error = error\n        };\n    }\n\n    /// <summary>\n    /// Initialise a new instance of <see cref=\"Snippet\"/>.\n    /// </summary>\n    public static Snippet Build(int startLine, int endLine, string value, string key, string language, string? path, string? expressiveCode)\n    {\n        Guard.AgainstNullAndEmpty(key, nameof(key));\n        Guard.AgainstEmpty(path, nameof(path));\n        Guard.AgainstEmpty(expressiveCode, nameof(expressiveCode));\n        Guard.AgainstUpperCase(language, nameof(language));\n        if (language.StartsWith('.'))\n        {\n            throw new ArgumentException(\"Language cannot start with '.'\", nameof(language));\n        }\n\n        Guard.AgainstNegativeAndZero(startLine, nameof(startLine));\n        Guard.AgainstNegativeAndZero(endLine, nameof(endLine));\n        return new()\n        {\n            StartLine = startLine,\n            EndLine = endLine,\n            value = value,\n            Key = key,\n            language = language,\n            Path = path,\n            ExpressiveCode = expressiveCode,\n            Error = \"\",\n            ViewUrl = null\n        };\n    }\n\n    /// <summary>\n    /// Initialise a new instance of <see cref=\"Snippet\"/> with an optional view URL.\n    /// </summary>\n    public static Snippet Build(int startLine, int endLine, string value, string key, string language, string? path, string? expressiveCode, string? viewUrl)\n    {\n        Guard.AgainstNullAndEmpty(key, nameof(key));\n        Guard.AgainstEmpty(path, nameof(path));\n        Guard.AgainstEmpty(expressiveCode, nameof(expressiveCode));\n        Guard.AgainstEmpty(viewUrl, nameof(viewUrl));\n        Guard.AgainstUpperCase(language, nameof(language));\n        if (language.StartsWith('.'))\n        {\n            throw new ArgumentException(\"Language cannot start with '.'\", nameof(language));\n        }\n\n        Guard.AgainstNegativeAndZero(startLine, nameof(startLine));\n        Guard.AgainstNegativeAndZero(endLine, nameof(endLine));\n        return new()\n        {\n            StartLine = startLine,\n            EndLine = endLine,\n            value = value,\n            Key = key,\n            language = language,\n            Path = path,\n            ExpressiveCode = expressiveCode,\n            Error = \"\",\n            ViewUrl = viewUrl\n        };\n    }\n\n    public string Error { get; private init; } = null!;\n\n    public bool IsInError { get; private init; }\n\n    /// <summary>\n    /// The key used to identify the snippet.\n    /// </summary>\n    public string Key { get; private init; } = null!;\n\n    /// <summary>\n    /// An associated expressive code block with the snippet\n    /// See https://expressive-code.com/\n    /// </summary>\n    public string? ExpressiveCode { get; private init; }\n\n    /// <summary>\n    /// Optional URL to use for viewing the snippet source (for web-snippets).\n    /// </summary>\n    public string? ViewUrl { get; private init; }\n\n    /// <summary>\n    /// The language of the snippet, extracted from the file extension of the input file.\n    /// </summary>\n    public string Language\n    {\n        get\n        {\n            ThrowIfIsInError();\n            return language!;\n        }\n    }\n\n    string? language;\n\n    /// <summary>\n    /// The path the snippet was read from.\n    /// </summary>\n    public string? Path { get; private init; }\n\n    /// <summary>\n    /// The line the snippets started on.\n    /// </summary>\n    public int StartLine { get; private init; }\n\n    /// <summary>\n    /// The line the snippet ended on.\n    /// </summary>\n    public int EndLine { get; private init; }\n\n    /// <summary>\n    /// The <see cref=\"Path\"/>, <see cref=\"StartLine\"/>, and <see cref=\"EndLine\"/> concatenated.\n    /// </summary>\n    public string? FileLocation\n    {\n        get\n        {\n            if (Path == null)\n            {\n                return null;\n            }\n\n            return $\"{Path}({StartLine}-{EndLine})\";\n        }\n    }\n\n    public string Value\n    {\n        get\n        {\n            ThrowIfIsInError();\n            return value!;\n        }\n    }\n\n    string? value;\n\n    void ThrowIfIsInError()\n    {\n        if (IsInError)\n        {\n            throw new SnippetReadingException($\"Cannot access when {nameof(IsInError)}. Key: {Key}. FileLocation: {FileLocation}. Error: {Error}\");\n        }\n    }\n\n    public override string ToString() =>\n        $\"\"\"\n         ReadSnippet.\n           Key: {Key}\n           FileLocation: {FileLocation}\n           Error: {Error}\n\n         \"\"\";\n}"
  },
  {
    "path": "src/MarkdownSnippets/Reading/StartEndTester.cs",
    "content": "static class StartEndTester\n{\n    internal static bool IsStartOrEnd(CharSpan line)\n    {\n        var trimmedLine = line.Trim();\n        return IsBeginSnippet(trimmedLine) ||\n               IsEndSnippet(trimmedLine) ||\n               IsStartRegion(trimmedLine) ||\n               IsEndRegion(trimmedLine);\n    }\n\n    internal static bool IsStart(\n        CharSpan trimmedLine,\n        CharSpan path,\n        out CharSpan currentKey,\n        [NotNullWhen(true)] out EndFunc? endFunc,\n        out CharSpan expressiveCode,\n        out CharSpan language)\n    {\n        if (IsBeginSnippet(trimmedLine, path, out currentKey, out expressiveCode, out language))\n        {\n            endFunc = IsEndSnippet;\n            return true;\n        }\n\n        if (IsStartRegion(trimmedLine, out currentKey))\n        {\n            endFunc = IsEndRegion;\n            // not supported for regions\n            expressiveCode = null;\n            language = null;\n            return true;\n        }\n\n        expressiveCode = null;\n        language = null;\n        endFunc = throwFunc;\n\n        return false;\n    }\n\n    static EndFunc throwFunc = _ => throw new(\"Do not use out func\");\n\n    static bool IsEndRegion(CharSpan line) =>\n        line.StartsWith(\"#endregion\", StringComparison.Ordinal);\n\n    static bool IsEndSnippet(CharSpan line) =>\n        IndexOf(line, \"end-snippet\") >= 0;\n\n    static bool IsStartRegion(CharSpan line) =>\n        line.StartsWith(\"#region \", StringComparison.Ordinal);\n\n    internal static bool IsStartRegion(\n        CharSpan line,\n        out CharSpan key)\n    {\n        if (!line.StartsWith(\"#region \", StringComparison.Ordinal))\n        {\n            key = null;\n            return false;\n        }\n\n        var substring = line[8..].Trim();\n\n        if (substring.Contains(' ') ||\n            !KeyValidator.IsValidKey(substring))\n        {\n            key = null;\n            return false;\n        }\n\n        key = substring.ToString();\n        return true;\n    }\n\n    static bool IsBeginSnippet(CharSpan line)\n    {\n        var startIndex = IndexOf(line, \"begin-snippet: \");\n        return startIndex != -1;\n    }\n\n    internal static bool IsBeginSnippet(\n        CharSpan line,\n        CharSpan path,\n        out CharSpan key,\n        out CharSpan expressiveCode) =>\n        IsBeginSnippet(line, path, out key, out expressiveCode, out _);\n\n    internal static bool IsBeginSnippet(\n        CharSpan line,\n        CharSpan path,\n        out CharSpan key,\n        out CharSpan expressiveCode,\n        out CharSpan language)\n    {\n        expressiveCode = null;\n        language = null;\n        var beginSnippetIndex = IndexOf(line, \"begin-snippet: \");\n        if (beginSnippetIndex == -1)\n        {\n            key = null;\n            return false;\n        }\n\n        var startIndex = beginSnippetIndex + 15;\n        var substring = line\n            .TrimBackCommentChars(startIndex);\n\n        var startArgs = substring.IndexOf('(');\n        if (startArgs == -1)\n        {\n            key = substring.Trim();\n        }\n        else\n        {\n            substring = substring.Trim();\n            key = substring[..startArgs].Trim();\n\n            if (!substring.EndsWith(')'))\n            {\n                throw new SnippetReadingException(\n                    $\"\"\"\n                     ExpressiveCode must end with ')`.\n                     Key: {key}\n                     Path: {path}\n                     Line: {line}\n                     \"\"\");\n            }\n\n            var args = substring[(startArgs + 1)..^1].Trim();\n            args = ExtractLanguage(args, key, path, line, out language);\n            expressiveCode = args;\n        }\n\n        if (key.Length == 0)\n        {\n            throw new SnippetReadingException(\n                $\"\"\"\n                 No Key could be derived.\n                 Path: {path}\n                 Line: '{line}'\n                 \"\"\");\n        }\n\n        if (KeyValidator.IsValidKey(key))\n        {\n            return true;\n        }\n\n        throw new SnippetReadingException(\n            $\"\"\"\n             Key cannot contain whitespace or start/end with symbols.\n             Key: {key}\n             Path: {path}\n             Line: {line}\n             \"\"\");\n    }\n\n    static CharSpan ExtractLanguage(CharSpan args, scoped CharSpan key, scoped CharSpan path, scoped CharSpan line, out CharSpan language)\n    {\n        language = null;\n        if (!args.StartsWith(\"lang=\", StringComparison.Ordinal))\n        {\n            return args;\n        }\n\n        var rest = args[5..];\n        var end = rest.IndexOf(' ');\n        CharSpan value;\n        CharSpan remainder;\n        if (end == -1)\n        {\n            value = rest;\n            remainder = null;\n        }\n        else\n        {\n            value = rest[..end];\n            remainder = rest[(end + 1)..].Trim();\n        }\n\n        if (value.Length == 0)\n        {\n            throw new SnippetReadingException(\n                $\"\"\"\n                 lang= must have a value.\n                 Key: {key}\n                 Path: {path}\n                 Line: {line}\n                 \"\"\");\n        }\n\n        foreach (var c in value)\n        {\n            if (c is (< 'a' or > 'z') and (< '0' or > '9'))\n            {\n                throw new SnippetReadingException(\n                    $\"\"\"\n                     lang value must be lowercase alphanumeric.\n                     Key: {key}\n                     Value: {value.ToString()}\n                     Path: {path}\n                     Line: {line}\n                     \"\"\");\n            }\n        }\n\n        language = value;\n        return remainder;\n    }\n\n    static int IndexOf(CharSpan line, CharSpan value)\n    {\n        if (value.Length > line.Length)\n        {\n            return -1;\n        }\n\n        var charactersToScan = Math.Min(line.Length, value.Length + 10);\n        return line[..charactersToScan].IndexOf(value, StringComparison.Ordinal);\n    }\n}"
  },
  {
    "path": "src/MarkdownSnippets/SnippetException.cs",
    "content": "namespace MarkdownSnippets;\n\npublic class SnippetException(string message) :\n    Exception(message);"
  },
  {
    "path": "src/MarkdownSnippets/SnippetExtensions.cs",
    "content": "static class SnippetExtensions\n{\n    public static Dictionary<string, IReadOnlyList<Snippet>> ToDictionary(this IEnumerable<Snippet> value) =>\n        value\n            .GroupBy(_ => _.Key)\n            .ToDictionary(\n                keySelector: _ => _.Key,\n                elementSelector: _ => _.OrderBy(ScrubPath).ToReadonlyList());\n\n    static string? ScrubPath(Snippet snippet)\n    {\n        var path = snippet.Path;\n        if (path == null)\n        {\n            return null;\n        }\n\n        var count = path.Count(_ => _ is '/' or '\\\\');\n        if (count == 0)\n        {\n            return path;\n        }\n\n        return string.Create(path.Length - count, path, (span, source) =>\n        {\n            var index = 0;\n            foreach (var ch in source)\n            {\n                if (ch is not ('/' or '\\\\'))\n                {\n                    span[index++] = ch;\n                }\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "src/MarkdownSnippets/SnippetReadingException.cs",
    "content": "namespace MarkdownSnippets;\n\npublic class SnippetReadingException(string message) :\n    SnippetException(message);"
  },
  {
    "path": "src/MarkdownSnippets/StringBuilderCache.cs",
    "content": "static class StringBuilderCache\n{\n    const int MAX_BUILDER_SIZE = 360;\n\n    [ThreadStatic]\n    static StringBuilder? CachedInstance;\n\n    public static StringBuilder Acquire(int capacity = 16)\n    {\n        if (capacity <= MAX_BUILDER_SIZE)\n        {\n            var builder = CachedInstance;\n            // Avoid StringBuilder block fragmentation by getting a new StringBuilder\n            // when the requested size is larger than the current capacity\n            if (capacity <= builder?.Capacity)\n            {\n                CachedInstance = null;\n                builder.Clear();\n                return builder;\n            }\n        }\n        return new(capacity);\n    }\n\n    public static void Release(StringBuilder builder)\n    {\n        if (builder.Capacity <= MAX_BUILDER_SIZE)\n        {\n            CachedInstance = builder;\n        }\n    }\n\n    public static string GetStringAndRelease(StringBuilder builder)\n    {\n        var result = builder.ToString();\n        Release(builder);\n        return result;\n    }\n}"
  },
  {
    "path": "src/MarkdownSnippets.MsBuild/DocoTask.cs",
    "content": "using Microsoft.Build.Framework;\nusing Task = Microsoft.Build.Utilities.Task;\n\nnamespace MarkdownSnippets;\n\npublic class DocoTask :\n    Task,\n    ICancelableTask\n{\n    [Required] public string ProjectDirectory { get; set; } = null!;\n    public bool? ReadOnly { get; set; }\n    public bool? ValidateContent { get; set; }\n    public bool? WriteHeader { get; set; }\n    public string? Header { get; set; }\n    public string? UrlPrefix { get; set; }\n    public int? TocLevel { get; set; }\n    public int? MaxWidth { get; set; }\n    public LinkFormat? LinkFormat { get; set; }\n    public DocumentConvention? Convention { get; set; }\n    public List<string> ExcludeDirs { get; set; } = [];\n    public List<string> ExcludeMarkdownDirs { get; set; } = [];\n    public List<string> ExcludeSnippetDirs { get; set; } = [];\n    public List<string> TocExcludes { get; set; } = [];\n    public List<string> UrlsAsSnippets { get; set; } = [];\n    public bool? TreatMissingAsWarning { get; set; }\n    public bool? OmitSnippetLinks { get; set; }\n    public string? PackageOutputPath { get; set; }\n\n    public override bool Execute()\n    {\n        var stopwatch = Stopwatch.StartNew();\n        var root = GitRepoDirectoryFinder.FindForDirectory(ProjectDirectory);\n\n        if (!string.IsNullOrWhiteSpace(PackageOutputPath))\n        {\n            var resolved = Path.GetFullPath(Path.Combine(ProjectDirectory, PackageOutputPath));\n            ExcludeDirs.Add(resolved);\n        }\n\n        var (fileConfig, configFilePath) = ConfigReader.Read(root);\n\n        var configResult = ConfigDefaults.Convert(\n            fileConfig,\n            new()\n            {\n                ReadOnly = ReadOnly,\n                ValidateContent = ValidateContent,\n                WriteHeader = WriteHeader,\n                Header = Header,\n                UrlPrefix = UrlPrefix,\n                LinkFormat = LinkFormat,\n                Convention = Convention,\n                ExcludeDirectories = ExcludeDirs,\n                ExcludeMarkdownDirectories = ExcludeMarkdownDirs,\n                ExcludeSnippetDirectories = ExcludeSnippetDirs,\n                TocExcludes = TocExcludes,\n                TocLevel = TocLevel,\n                MaxWidth = MaxWidth,\n                UrlsAsSnippets = UrlsAsSnippets,\n                TreatMissingAsWarning = TreatMissingAsWarning,\n                OmitSnippetLinks = OmitSnippetLinks,\n            });\n\n        var message = LogBuilder.BuildConfigLogMessage(root, configResult, configFilePath);\n        Log.LogMessage(message);\n\n        var processor = new DirectoryMarkdownProcessor(\n            root,\n            directoryIncludes: ExcludeToFilterBuilder.ExcludesToFilter(configResult.ExcludeDirectories),\n            markdownDirectoryIncludes: ExcludeToFilterBuilder.ExcludesToFilter(configResult.ExcludeMarkdownDirectories),\n            snippetDirectoryIncludes: ExcludeToFilterBuilder.ExcludesToFilter(configResult.ExcludeSnippetDirectories),\n            convention: configResult.Convention,\n            log: _ => Log.LogMessage(_),\n            writeHeader: configResult.WriteHeader,\n            header: configResult.Header,\n            readOnly: configResult.ReadOnly,\n            linkFormat: configResult.LinkFormat,\n            tocLevel: configResult.TocLevel,\n            tocExcludes: configResult.TocExcludes,\n            treatMissingAsWarning: configResult.TreatMissingAsWarning,\n            maxWidth: configResult.MaxWidth,\n            urlPrefix: configResult.UrlPrefix,\n            validateContent: configResult.ValidateContent,\n            omitSnippetLinks: configResult.OmitSnippetLinks);\n\n        try\n        {\n            var urlsAsSnippets = configResult.UrlsAsSnippets;\n            if (urlsAsSnippets != null && urlsAsSnippets.Count != 0)\n            {\n                var snippets = new List<Snippet>();\n                snippets.AppendUrlsAsSnippets(urlsAsSnippets).GetAwaiter().GetResult();\n                processor.AddSnippets(snippets);\n            }\n            var snippetsInError = processor.Snippets.Where(_ => _.IsInError).ToList();\n            if (snippetsInError.Count != 0)\n            {\n                foreach (var snippet in snippetsInError)\n                {\n                    Log.LogFileError($\"Snippet error: {snippet.Error}. Key: {snippet.Key}\", snippet.Path, snippet.StartLine, 0);\n                }\n\n                return false;\n            }\n\n            processor.Run();\n            return true;\n        }\n        catch (MissingSnippetsException exception)\n        {\n            foreach (var missing in exception.Missing)\n            {\n                if (configResult.TreatMissingAsWarning)\n                {\n                    Log.LogWarning($\"MarkdownSnippets: Missing snippet: {missing.Key}\", missing.File, missing.LineNumber, 0);\n                }\n                else\n                {\n                    Log.LogFileError($\"MarkdownSnippets: Missing snippet: {missing.Key}\", missing.File, missing.LineNumber, 0);\n                }\n            }\n\n            return configResult.TreatMissingAsWarning;\n        }\n        catch (MissingIncludesException exception)\n        {\n            foreach (var missing in exception.Missing)\n            {\n                if (configResult.TreatMissingAsWarning)\n                {\n                    Log.LogWarning($\"MarkdownSnippets: Missing include: {missing.Key}\", missing.File, missing.LineNumber);\n                }\n                else\n                {\n                    Log.LogFileError($\"MarkdownSnippets: Missing include: {missing.Key}\", missing.File, missing.LineNumber, 0);\n                }\n            }\n\n            return configResult.TreatMissingAsWarning;\n        }\n        catch (ContentValidationException exception)\n        {\n            foreach (var error in exception.Errors)\n            {\n                //TODO: add column\n                Log.LogFileError($\"MarkdownSnippets: Content validation: {error.Error}\", error.File, error.Line, error.Column);\n            }\n\n            return configResult.TreatMissingAsWarning;\n        }\n        catch (MarkdownProcessingException exception)\n        {\n            Log.LogFileError($\"MarkdownSnippets: {exception.Message}\", exception.File, exception.LineNumber, 0);\n            return false;\n        }\n        catch (SnippetException exception)\n        {\n            Log.LogError($\"MarkdownSnippets: {exception}\");\n            return false;\n        }\n        finally\n        {\n            Log.LogMessageFromText($\"Finished MarkdownSnippets {stopwatch.ElapsedMilliseconds}ms\", MessageImportance.Normal);\n        }\n    }\n\n    public void Cancel()\n    {\n    }\n}"
  },
  {
    "path": "src/MarkdownSnippets.MsBuild/LoggingHelper.cs",
    "content": "using Microsoft.Build.Utilities;\n\nstatic class LoggingHelper\n{\n    public static void LogFileError(this TaskLoggingHelper loggingHelper, string message, string? file, int line, int column) =>\n        loggingHelper.LogError(null, null, null, file, line, column, 0, 0, message);\n}"
  },
  {
    "path": "src/MarkdownSnippets.MsBuild/MarkdownSnippets.MsBuild.csproj",
    "content": "\n<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFrameworks>netstandard2.0;net10.0</TargetFrameworks>\n    <Description>Extract code snippets from any language to be used when building documentation.</Description>\n    <DevelopmentDependency>true</DevelopmentDependency>\n    <IncludeBuildOutput>false</IncludeBuildOutput>\n  </PropertyGroup>\n\n  <PropertyGroup>\n    <!-- PackageShader configuration -->\n    <Shader_Internalize>false</Shader_Internalize>\n    <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>\n\n    <!-- Strong name signing -->\n    <SignAssembly>true</SignAssembly>\n    <AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)..\\key.snk</AssemblyOriginatorKeyFile>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <Compile Include=\"..\\ConfigReader\\*.cs\" />\n\n    <TfmSpecificPackageFile Include=\".\\bin\\$(Configuration)\\$(TargetFramework)\\MarkdownSnippets.MsBuild.dll\">\n      <Pack>true</Pack>\n      <PackagePath>task\\$(TargetFramework)</PackagePath>\n    </TfmSpecificPackageFile>\n    <Content Include=\"MarkdownSnippets.MsBuild.targets\">\n      <Pack>true</Pack>\n      <PackagePath>build</PackagePath>\n    </Content>\n\n    <PackageReference Include=\"Microsoft.Build.Tasks.Core\" PrivateAssets=\"All\" />\n    <ProjectReference Include=\"..\\MarkdownSnippets\\MarkdownSnippets.csproj\" Shade=\"true\" PrivateAssets=\"All\" />\n    <PackageReference Include=\"ProjectDefaults\" PrivateAssets=\"all\" />\n    <PackageReference Include=\"PackageShader.MsBuild\" PrivateAssets=\"all\" />\n    <!--  Avoid transitive CVE from Microsoft.Build.Tasks.Core-->\n    <PackageReference Include=\"System.Security.Cryptography.Xml\" PrivateAssets=\"All\" />\n\n    <!-- Packages to shade to avoid version conflicts in MSBuild -->\n    <PackageReference Include=\"System.Collections.Immutable\" Shade=\"true\" Condition=\"'$(TargetFramework)' != 'net10.0'\" />\n    <PackageReference Include=\"System.Memory\" Shade=\"true\" Condition=\"'$(TargetFramework)' == 'netstandard2.0'\" />\n    <PackageReference Include=\"System.Buffers\" Shade=\"true\" Condition=\"'$(TargetFramework)' == 'netstandard2.0'\" />\n    <PackageReference Include=\"System.Runtime.CompilerServices.Unsafe\" Shade=\"true\" Condition=\"'$(TargetFramework)' == 'netstandard2.0'\" />\n    <PackageReference Include=\"System.Numerics.Vectors\" Shade=\"true\" Condition=\"'$(TargetFramework)' == 'netstandard2.0'\" />\n    <PackageReference Include=\"System.Threading.Tasks.Extensions\" Shade=\"true\" Condition=\"'$(TargetFramework)' == 'netstandard2.0'\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/MarkdownSnippets.MsBuild/MarkdownSnippets.MsBuild.targets",
    "content": "﻿<Project ToolsVersion=\"4.0\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">\n  <PropertyGroup>\n    <MarkdownSnippetsAssembly Condition=\"$(MSBuildRuntimeType) == 'Core'\">$(MSBuildThisFileDirectory)..\\task\\net10.0\\MarkdownSnippets.MsBuild.dll</MarkdownSnippetsAssembly>\n    <MarkdownSnippetsAssembly Condition=\"$(MSBuildRuntimeType) != 'Core'\">$(MSBuildThisFileDirectory)..\\task\\netstandard2.0\\MarkdownSnippets.MsBuild.dll</MarkdownSnippetsAssembly>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <CustomAdditionalCompileInputs Include=\"$(MdTargetDir)**\\*.source.md\"/>\n  </ItemGroup>\n\n  <UsingTask\n    TaskName=\"MarkdownSnippets.DocoTask\"\n    AssemblyFile=\"$(MarkdownSnippetsAssembly)\" />\n\n  <Target\n    Name=\"MarkdownSnippetsTarget\"\n    AfterTargets=\"AfterCompile\"\n    Condition=\"$(DesignTimeBuild) != true AND '$(NCrunch)' != '1' AND ('$(TargetFrameworks)' == '' OR $(TargetFramework) == $([MSBuild]::ValueOrDefault('$(TargetFrameworks)', '').Split(';')[0]))\" >\n    <MarkdownSnippets.DocoTask\n      ProjectDirectory=\"$(MSBuildProjectDirectory)\"\n      ReadOnly=\"$(MarkdownSnippetsReadOnly)\"\n      WriteHeader=\"$(MarkdownSnippetsWriteHeader)\"\n      PackageOutputPath=\"$(PackageOutputPath)\" />\n  </Target>\n</Project>"
  },
  {
    "path": "src/MarkdownSnippets.Tool/AssemblyInfo.cs",
    "content": "﻿[assembly: InternalsVisibleTo(\"MarkdownSnippets.Tool.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e191859fcd1deee68b96927c170783ced0c9a471a6424a0a011cfd31156a49dd73c4ad4a88b995fb918c0b43e0c005ef5fb72d53a328a64bde825cb5f2e4c53d66f69fcbb87d6737128b98e677a42091974b5f56093123a2dd6bc738af751b101d41c4f7a996e217b61967a3aa1ae7bc791d19c1cbeef47f0cdd20d288dff1a3\")]\n"
  },
  {
    "path": "src/MarkdownSnippets.Tool/CommandLineException.cs",
    "content": "﻿class CommandLineException(string message) :\n    Exception(message);"
  },
  {
    "path": "src/MarkdownSnippets.Tool/CommandRunner.cs",
    "content": "﻿static class CommandRunner\n{\n    public static Task RunCommand(Invoke invoke, params string[] args)\n    {\n        if (args.Length == 1)\n        {\n            var firstArg = args[0];\n            if (!firstArg.StartsWith('-'))\n            {\n                return invoke(firstArg, new());\n            }\n        }\n\n        return Parser.Default.ParseArguments<Options>(args)\n            .WithParsedAsync(\n                options =>\n                {\n                    ValidateAndApplyDefaults(options);\n                    var input = new ConfigInput\n                    {\n                        ReadOnly = options.ReadOnly,\n                        ValidateContent = options.ValidateContent,\n                        WriteHeader = options.WriteHeader,\n                        Header = options.Header,\n                        UrlPrefix = options.UrlPrefix,\n                        LinkFormat = options.LinkFormat,\n                        TocLevel = options.TocLevel,\n                        MaxWidth = options.MaxWidth,\n                        ExcludeDirectories = options.ExcludeDirectories.ToList(),\n                        ExcludeMarkdownDirectories = options.ExcludeMarkdownDirectories.ToList(),\n                        ExcludeSnippetDirectories = options.ExcludeSnippetDirectories.ToList(),\n                        TocExcludes = options.TocExcludes.ToList(),\n                        UrlsAsSnippets = options.UrlsAsSnippets.ToList(),\n                        TreatMissingAsWarning = options.TreatMissingAsWarning,\n                        Convention = options.Convention,\n                        OmitSnippetLinks = options.OmitSnippetLinks\n                    };\n                    return invoke(options.TargetDirectory!, input);\n                });\n    }\n\n    static void ValidateAndApplyDefaults(Options options)\n    {\n        if (options.Header != null && string.IsNullOrWhiteSpace(options.Header))\n        {\n            throw new CommandLineException(\"Empty Header is not allowed.\");\n        }\n\n        if (options.UrlPrefix != null && string.IsNullOrWhiteSpace(options.UrlPrefix))\n        {\n            throw new CommandLineException(\"Empty UrlPrefix is not allowed.\");\n        }\n\n        if (options.TargetDirectory == null)\n        {\n            options.TargetDirectory = Environment.CurrentDirectory;\n            if (!GitRepoDirectoryFinder.IsInGitRepository(options.TargetDirectory))\n            {\n                throw new CommandLineException($\"The current directory does no exist with a .git repository. Pass in a target directory instead. Current directory: {options.TargetDirectory}\");\n            }\n        }\n        else\n        {\n            if (!Directory.Exists(options.TargetDirectory))\n            {\n                throw new CommandLineException(\"target-directory does not exist.\");\n            }\n            options.TargetDirectory = Path.GetFullPath(options.TargetDirectory);\n        }\n\n        if (options.TocLevel <= 0)\n        {\n            throw new CommandLineException(\"toc-level must be positive.\");\n        }\n\n        if (options.MaxWidth <= 0)\n        {\n            throw new CommandLineException(\"max-width must be positive.\");\n        }\n\n        ValidateItems(\"exclude\", options.ExcludeDirectories);\n        ValidateItems(\"exclude-markdown-directories\", options.ExcludeMarkdownDirectories);\n        ValidateItems(\"exclude-snippet-directories\", options.ExcludeSnippetDirectories);\n        ValidateItems(\"toc-excludes\", options.TocExcludes);\n        ValidateItems(\"urls-as-snippets\", options.UrlsAsSnippets);\n    }\n\n    static void ValidateItems(string name, IList<string> items)\n    {\n        if (items.Distinct().Count() != items.Count)\n        {\n            throw new CommandLineException($\"duplicates found in {name}.\");\n        }\n\n        if (items.Any(string.IsNullOrWhiteSpace))\n        {\n            throw new CommandLineException($\"Empty items found in `{name}`.\");\n        }\n    }\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool/GlobalUsings.cs",
    "content": "﻿global using CommandLine;\nglobal using MarkdownSnippets;\nglobal using Polyfills;"
  },
  {
    "path": "src/MarkdownSnippets.Tool/Invoke.cs",
    "content": "﻿public delegate Task Invoke(string directory, ConfigInput config);"
  },
  {
    "path": "src/MarkdownSnippets.Tool/MarkdownSnippets.Tool.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <OutputType>Exe</OutputType>\n    <TargetFramework>net10.0</TargetFramework>\n    <ToolCommandName>mdsnippets</ToolCommandName>\n    <AssemblyName>mdsnippets</AssemblyName>\n    <PackageId>MarkdownSnippets.Tool</PackageId>\n    <PackAsTool>True</PackAsTool>\n    <Description>.NET Core Global Tool for merging code snippets with markdown documents</Description>\n    <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>\n    <RollForward>LatestMajor</RollForward>\n  </PropertyGroup>\n  <ItemGroup>\n    <Compile Include=\"..\\ConfigReader\\*.cs\" />\n\n    <ProjectReference Include=\"..\\MarkdownSnippets\\MarkdownSnippets.csproj\" />\n    <PackageReference Include=\"CommandLineParser\" />\n    <PackageReference Include=\"Microsoft.Sbom.Targets\" PrivateAssets=\"all\" Condition=\"'$(CI)' == 'true'\" />\n    <PackageReference Include=\"ProjectDefaults\" PrivateAssets=\"all\" />\n  </ItemGroup>\n</Project>"
  },
  {
    "path": "src/MarkdownSnippets.Tool/Options.cs",
    "content": "﻿public class Options\n{\n    [Option('t', \"target-directory\",\n        Required = false,\n        HelpText = \"The target directory to run against. Optional. If no directory is passed the current directory will be used, but only if it exists with a git repository directory tree. If not an error is returned.\")]\n    public string? TargetDirectory { get; set; }\n\n    [Option('e', \"exclude-directories\",\n        Separator = ':',\n        Required = false,\n        HelpText = \"Directories to be excluded. Optional. Colon ':' separated for multiple values.\")]\n    public IList<string> ExcludeDirectories { get; set; } = null!;\n\n    [Option(\"exclude-markdown-directories\",\n        Separator = ':',\n        Required = false,\n        HelpText = \"Directories to be excluded from markdown searching. Optional. Colon ':' separated for multiple values.\")]\n    public IList<string> ExcludeMarkdownDirectories { get; set; } = null!;\n\n    [Option(\"exclude-snippet-directories\",\n        Separator = ':',\n        Required = false,\n        HelpText = \"Directories to be excluded from snippet searching. Optional. Colon ':' separated for multiple values.\")]\n    public IList<string> ExcludeSnippetDirectories { get; set; } = null!;\n\n    [Option(\"toc-excludes\",\n        Separator = ':',\n        Required = false,\n        HelpText = \"Headings to be excluded from table of contents. Optional. Colon ':' separated for multiple values.\")]\n    public IList<string> TocExcludes { get; set; } = null!;\n\n    [Option('u', \"urls-as-snippets\",\n        Separator = ' ',\n        Required = false,\n        HelpText = \"\"\"\n                   Urls to files to be included as snippets. Optional. Space ' ' separated for multiple values.\n                   Each url will be accessible using the file name as a key. Any snippets within the files will be extracted and accessible as individual keyed snippets.\n                   \"\"\")]\n    public IList<string> UrlsAsSnippets { get; set; } = null!;\n\n    [Option('r', \"read-only\",\n        Required = false,\n        HelpText = \"Set resultant md files as read-only. Optional. Defaults to false.\")]\n    public bool? ReadOnly { get; set; }\n\n    [Option('v', \"validate-content\",\n        Required = false,\n        HelpText = \"Validate the content. Optional. Defaults to false.\")]\n    public bool? ValidateContent { get; set; }\n\n    [Option(\"write-header\",\n        Required = false,\n        HelpText = \"Write a header at the top of each resultant md file. Optional. Defaults to true\")]\n    public bool? WriteHeader { get; set; }\n\n    [Option(\"missing-as-warning\",\n        Required = false,\n        HelpText = \"The default behavior for a missing snippet/include is to log an error (or throw an exception). To change that behavior to a warning set TreatMissingAsWarning to true. Optional. Defaults to false\")]\n    public bool? TreatMissingAsWarning { get; set; }\n\n    [Option(\"omit-snippet-links\",\n        Required = false,\n        HelpText = \"The default behavior snippet links is to have both an anchor and a link to the snippet source. Optional. Defaults to false\")]\n    public bool? OmitSnippetLinks { get; set; }\n\n    [Option(\"header\",\n        Required = false,\n        HelpText = \"\"\"\n                   The header to write. `{relativePath}` is replaced with the current .source.md file. Optional. Defaults to:\n\n                   \"\"\" + HeaderWriter.DefaultHeader)]\n    public string? Header { get; set; }\n\n    [Option(\"url-prefix\",\n        Required = false,\n        HelpText = \"The prefix to add to all the snippet URLs. Optional. Defaults to: null\")]\n    public string? UrlPrefix { get; set; }\n\n    [Option('l', \"link-format\",\n        Required = false,\n        HelpText = \"Controls the format of the link under each snippet. Optional. Supported values: GitHub, Tfs. Defaults to GitHub.\")]\n    public LinkFormat? LinkFormat { get; set; }\n\n    [Option('c', \"convention\",\n        Required = false,\n        HelpText = \"Controls the target document convention. Optional. Supported values: SourceTransform, InPlaceOverwrite. Defaults to SourceTransform.\")]\n    public DocumentConvention? Convention { get; set; }\n\n    [Option(\"toc-level\",\n        Required = false,\n        HelpText = \"Controls how many header levels to write in the table of contents. Optional. Defaults to 2. Must be positive.\")]\n    public int? TocLevel { get; set; }\n\n    [Option(\"max-width\",\n        Required = false,\n        HelpText = \"Controls the maximum character width for snippets. Optional. Defaults to ignore. Must be positive.\")]\n    public int? MaxWidth { get; set; }\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool/Program.cs",
    "content": "﻿var stopwatch = Stopwatch.StartNew();\ntry\n{\n    await CommandRunner.RunCommand(Inner, args);\n}\ncatch (CommandLineException exception)\n{\n    Console.WriteLine($\"Failed: {exception.Message}\");\n    Environment.Exit(1);\n}\ncatch (ConfigurationException exception)\n{\n    Console.WriteLine($\"Failed: {exception.Message}\");\n    Environment.Exit(1);\n}\ncatch (SnippetException exception)\n{\n    Console.WriteLine($\"Failed: {exception.Message}\");\n    Environment.Exit(1);\n}\n\nfinally\n{\n    Console.WriteLine($\"Finished {stopwatch.ElapsedMilliseconds}ms\");\n}\n\nstatic async Task Inner(string targetDirectory, ConfigInput configInput)\n{\n    targetDirectory = Path.GetFullPath(targetDirectory);\n    var (fileConfig, configFilePath) = ConfigReader.Read(targetDirectory);\n    var configResult = ConfigDefaults.Convert(fileConfig, configInput);\n\n    var message = LogBuilder.BuildConfigLogMessage(targetDirectory, configResult, configFilePath);\n    Console.WriteLine(message);\n\n    var processor = new DirectoryMarkdownProcessor(\n        targetDirectory,\n        directoryIncludes: ExcludeToFilterBuilder.ExcludesToFilter(configResult.ExcludeDirectories),\n        markdownDirectoryIncludes: ExcludeToFilterBuilder.ExcludesToFilter(configResult.ExcludeMarkdownDirectories),\n        snippetDirectoryIncludes: ExcludeToFilterBuilder.ExcludesToFilter(configResult.ExcludeSnippetDirectories),\n        convention: configResult.Convention,\n        log: Console.WriteLine,\n        writeHeader: configResult.WriteHeader,\n        header: configResult.Header,\n        readOnly: configResult.ReadOnly,\n        linkFormat: configResult.LinkFormat,\n        tocLevel: configResult.TocLevel,\n        tocExcludes: configResult.TocExcludes,\n        treatMissingAsWarning: configResult.TreatMissingAsWarning,\n        maxWidth: configResult.MaxWidth,\n        urlPrefix: configResult.UrlPrefix,\n        validateContent: configResult.ValidateContent,\n        omitSnippetLinks: configResult.OmitSnippetLinks,\n        snippetFileIncludes: ExcludeToFilterBuilder.FileExcludesToFilter(configResult.ExcludeSnippetFiles));\n\n    var urlsAsSnippets = configResult.UrlsAsSnippets;\n    if (urlsAsSnippets != null && urlsAsSnippets.Count != 0)\n    {\n        var snippets = new List<Snippet>();\n\n        await snippets.AppendUrlsAsSnippets(urlsAsSnippets);\n        processor.AddSnippets(snippets);\n    }\n\n    processor.Run();\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/CommandRunnerTests.ConventionLong.verified.txt",
    "content": "﻿{\n  targetDirectory: {CurrentDirectory},\n  configInput: {\n    Convention: InPlaceOverwrite\n  }\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/CommandRunnerTests.ConventionShort.verified.txt",
    "content": "﻿{\n  targetDirectory: {CurrentDirectory},\n  configInput: {\n    Convention: InPlaceOverwrite\n  }\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/CommandRunnerTests.Empty.verified.txt",
    "content": "﻿{\n  targetDirectory: {CurrentDirectory},\n  configInput: {}\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/CommandRunnerTests.ExcludeLong.verified.txt",
    "content": "﻿{\n  targetDirectory: {CurrentDirectory},\n  configInput: {\n    ExcludeDirectories: [\n      dir\n    ]\n  }\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/CommandRunnerTests.ExcludeMarkdownDirectoriesLong.verified.txt",
    "content": "﻿{\n  targetDirectory: {CurrentDirectory},\n  configInput: {\n    ExcludeMarkdownDirectories: [\n      dir\n    ]\n  }\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/CommandRunnerTests.ExcludeMultiple.verified.txt",
    "content": "﻿{\n  targetDirectory: {CurrentDirectory},\n  configInput: {\n    ExcludeDirectories: [\n      dir1,\n      dir2\n    ]\n  }\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/CommandRunnerTests.ExcludeShort.verified.txt",
    "content": "﻿{\n  targetDirectory: {CurrentDirectory},\n  configInput: {\n    ExcludeDirectories: [\n      dir\n    ]\n  }\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/CommandRunnerTests.ExcludeSnippetDirectoriesLong.verified.txt",
    "content": "﻿{\n  targetDirectory: {CurrentDirectory},\n  configInput: {\n    ExcludeSnippetDirectories: [\n      dir\n    ]\n  }\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/CommandRunnerTests.Header.verified.txt",
    "content": "﻿{\n  targetDirectory: {CurrentDirectory},\n  configInput: {\n    Header: the header\n  }\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/CommandRunnerTests.LinkFormatLong.verified.txt",
    "content": "﻿{\n  targetDirectory: {CurrentDirectory},\n  configInput: {\n    LinkFormat: Tfs\n  }\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/CommandRunnerTests.LinkFormatShort.verified.txt",
    "content": "﻿{\n  targetDirectory: {CurrentDirectory},\n  configInput: {\n    LinkFormat: Tfs\n  }\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/CommandRunnerTests.MaxWidthLong.verified.txt",
    "content": "﻿{\n  targetDirectory: {CurrentDirectory},\n  configInput: {\n    MaxWidth: 5\n  }\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/CommandRunnerTests.OmitSnippetLinks.verified.txt",
    "content": "﻿{\n  targetDirectory: {CurrentDirectory},\n  configInput: {\n    OmitSnippetLinks: true\n  }\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/CommandRunnerTests.ReadOnlyLong.verified.txt",
    "content": "﻿{\n  targetDirectory: {CurrentDirectory},\n  configInput: {\n    ReadOnly: false\n  }\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/CommandRunnerTests.ReadOnlyShort.verified.txt",
    "content": "﻿{\n  targetDirectory: {CurrentDirectory},\n  configInput: {\n    ReadOnly: false\n  }\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/CommandRunnerTests.SingleUnNamedArg.verified.txt",
    "content": "﻿{\n  targetDirectory: dir,\n  configInput: {}\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/CommandRunnerTests.TargetDirectoryLong.verified.txt",
    "content": "﻿{\n  targetDirectory: {ProjectDirectory}bin/,\n  configInput: {}\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/CommandRunnerTests.TargetDirectoryShort.verified.txt",
    "content": "﻿{\n  targetDirectory: {ProjectDirectory}bin/,\n  configInput: {}\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/CommandRunnerTests.TocLevelLong.verified.txt",
    "content": "﻿{\n  targetDirectory: {CurrentDirectory},\n  configInput: {\n    TocLevel: 5\n  }\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/CommandRunnerTests.UrlPrefix.verified.txt",
    "content": "﻿{\n  targetDirectory: {CurrentDirectory},\n  configInput: {\n    UrlPrefix: the prefix\n  }\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/CommandRunnerTests.UrlsAsSnippetsLong.verified.txt",
    "content": "﻿{\n  targetDirectory: {CurrentDirectory},\n  configInput: {\n    UrlsAsSnippets: [\n      url\n    ]\n  }\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/CommandRunnerTests.UrlsAsSnippetsMultiple.verified.txt",
    "content": "﻿{\n  targetDirectory: {CurrentDirectory},\n  configInput: {\n    UrlsAsSnippets: [\n      url1,\n      url2\n    ]\n  }\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/CommandRunnerTests.UrlsAsSnippetsShort.verified.txt",
    "content": "﻿{\n  targetDirectory: {CurrentDirectory},\n  configInput: {\n    UrlsAsSnippets: [\n      url\n    ]\n  }\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/CommandRunnerTests.ValidateContentLong.verified.txt",
    "content": "﻿{\n  targetDirectory: {CurrentDirectory},\n  configInput: {\n    ValidateContent: false\n  }\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/CommandRunnerTests.ValidateContentShort.verified.txt",
    "content": "﻿{\n  targetDirectory: {CurrentDirectory},\n  configInput: {\n    ValidateContent: false\n  }\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/CommandRunnerTests.VerifyContentLong.verified.txt",
    "content": ""
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/CommandRunnerTests.VerifyContentShort.verified.txt",
    "content": ""
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/CommandRunnerTests.WriteHeader.verified.txt",
    "content": "﻿{\n  targetDirectory: {CurrentDirectory},\n  configInput: {\n    WriteHeader: false\n  }\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/CommandRunnerTests.cs",
    "content": "﻿public class CommandRunnerTests\n{\n    string? targetDirectory;\n    ConfigInput? configInput;\n\n    [Fact]\n    public async Task Empty()\n    {\n        await CommandRunner.RunCommand(Capture);\n        await VerifyResult();\n    }\n\n    [Fact]\n    public async Task SingleUnNamedArg()\n    {\n        await CommandRunner.RunCommand(Capture, \"dir\");\n        await VerifyResult();\n    }\n\n    [Fact]\n    public async Task Header()\n    {\n        await CommandRunner.RunCommand(Capture, \"--header\", \"the header\");\n        await VerifyResult();\n    }\n\n    [Fact]\n    public async Task UrlPrefix()\n    {\n        await CommandRunner.RunCommand(Capture, \"--url-prefix\", \"the prefix\");\n        await VerifyResult();\n    }\n\n    [Fact]\n    public async Task WriteHeader()\n    {\n        await CommandRunner.RunCommand(Capture, \"--write-header\", \"false\");\n        await VerifyResult();\n    }\n\n    [Fact]\n    public async Task OmitSnippetLinks()\n    {\n        await CommandRunner.RunCommand(Capture, \"--omit-snippet-links\", \"true\");\n        await VerifyResult();\n    }\n\n    [Fact]\n    public async Task ValidateContentShort()\n    {\n        await CommandRunner.RunCommand(Capture, \"-v\", \"false\");\n        await VerifyResult();\n    }\n\n    [Fact]\n    public async Task ValidateContentLong()\n    {\n        await CommandRunner.RunCommand(Capture, \"--validate-content\", \"false\");\n        await VerifyResult();\n    }\n\n    [Fact]\n    public async Task ConventionShort()\n    {\n        await CommandRunner.RunCommand(Capture, \"-c\", \"InPlaceOverwrite\");\n        await VerifyResult();\n    }\n\n    [Fact]\n    public async Task ConventionLong()\n    {\n        await CommandRunner.RunCommand(Capture, \"--convention\", \"InPlaceOverwrite\");\n        await VerifyResult();\n    }\n\n    [Fact]\n    public async Task ReadOnlyShort()\n    {\n        await CommandRunner.RunCommand(Capture, \"-r\", \"false\");\n        await VerifyResult();\n    }\n\n    [Fact]\n    public async Task ReadOnlyLong()\n    {\n        await CommandRunner.RunCommand(Capture, \"--read-only\", \"false\");\n        await VerifyResult();\n    }\n\n    [Fact]\n    public async Task LinkFormatShort()\n    {\n        await CommandRunner.RunCommand(Capture, \"-l\", \"tfs\");\n        await VerifyResult();\n    }\n\n    [Fact]\n    public async Task LinkFormatLong()\n    {\n        await CommandRunner.RunCommand(Capture, \"--link-format\", \"tfs\");\n        await VerifyResult();\n    }\n\n    [Fact]\n    public async Task TargetDirectoryShort()\n    {\n        await CommandRunner.RunCommand(Capture, \"-t\", \"../../\");\n        await VerifyResult();\n    }\n\n    [Fact]\n    public async Task TargetDirectoryLong()\n    {\n        await CommandRunner.RunCommand(Capture, \"--target-directory\", \"../../\");\n        await VerifyResult();\n    }\n\n    [Fact]\n    public async Task MaxWidthLong()\n    {\n        await CommandRunner.RunCommand(Capture, \"--max-width\", \"5\");\n        await VerifyResult();\n    }\n\n    [Fact]\n    public async Task TocLevelLong()\n    {\n        await CommandRunner.RunCommand(Capture, \"--toc-level\", \"5\");\n        await VerifyResult();\n    }\n\n    [Fact]\n    public async Task ExcludeShort()\n    {\n        await CommandRunner.RunCommand(Capture, \"-e\", \"dir\");\n        await VerifyResult();\n    }\n\n    [Fact]\n    public async Task ExcludeMultiple()\n    {\n        await CommandRunner.RunCommand(Capture, \"-e\", \"dir1:dir2\");\n        await VerifyResult();\n    }\n\n    [Fact]\n    public Task ExcludeDuplicates() =>\n        Assert.ThrowsAsync<CommandLineException>(() => CommandRunner.RunCommand(Capture, \"-e\", \"dir:dir\"));\n\n    [Fact]\n    public Task ExcludeWhitespace() =>\n        Assert.ThrowsAsync<CommandLineException>(() => CommandRunner.RunCommand(Capture, \"-e\", \": :\"));\n\n    [Fact]\n    public async Task ExcludeLong()\n    {\n        await CommandRunner.RunCommand(Capture, \"--exclude-directories\", \"dir\");\n        await VerifyResult();\n    }\n\n    [Fact]\n    public async Task ExcludeMarkdownDirectoriesLong()\n    {\n        await CommandRunner.RunCommand(Capture, \"--exclude-markdown-directories\", \"dir\");\n        await VerifyResult();\n    }\n\n    [Fact]\n    public async Task ExcludeSnippetDirectoriesLong()\n    {\n        await CommandRunner.RunCommand(Capture, \"--exclude-snippet-directories\", \"dir\");\n        await VerifyResult();\n    }\n\n    [Fact]\n    public async Task UrlsAsSnippetsShort()\n    {\n        await CommandRunner.RunCommand(Capture, \"-u\", \"url\");\n        await VerifyResult();\n    }\n\n    [Fact]\n    public async Task UrlsAsSnippetsMultiple()\n    {\n        await CommandRunner.RunCommand(Capture, \"-u\", \"url1 url2\");\n        await VerifyResult();\n    }\n\n    [Fact]\n    public Task UrlsAsSnippetsDuplicates() =>\n        Assert.ThrowsAsync<CommandLineException>(() => CommandRunner.RunCommand(Capture, \"-u\", \"url url\"));\n\n    [Fact]\n    public Task UrlsAsSnippetsWhitespace() => Assert.ThrowsAsync<CommandLineException>(() => CommandRunner.RunCommand(Capture, \"-u\", \": :\"));\n\n    [Fact]\n    public async Task UrlsAsSnippetsLong()\n    {\n        await CommandRunner.RunCommand(Capture, \"--urls-as-snippets\", \"url\");\n        await VerifyResult();\n    }\n\n    Task Capture(string targetDirectory, ConfigInput configInput)\n    {\n        this.targetDirectory = targetDirectory;\n        this.configInput = configInput;\n        return Task.CompletedTask;\n    }\n\n    Task VerifyResult() =>\n        Verify(\n            new\n            {\n                targetDirectory,\n                configInput\n            });\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/GlobalUsings.cs",
    "content": "﻿global using MarkdownSnippets;\nglobal using VerifyTests.DiffPlex;"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/LogBuilderTests.BuildConfigLogMessage.DotNet10_0.verified.txt",
    "content": "﻿Config:\n    TargetDirectory: theRoot\n    UrlPrefix: \n    LinkFormat: Tfs\n    Convention: InPlaceOverwrite\n    TocLevel: 5\n    ValidateContent: False\n    OmitSnippetLinks: False\n    TreatMissingAsWarning: False\n    FileConfigPath: theConfigFilePath (exists:False)\n    MaxWidth: 80\n    ExcludeDirectories:\n        Dir1\n        Dir2\n    ExcludeMarkdownDirectories:\n        Dir3\n        Dir4\n    ExcludeSnippetDirectories:\n        Dir5\n        Dir6\n    UrlsAsSnippets:\n        Url1\n        Url2"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/LogBuilderTests.BuildConfigLogMessage.DotNet9_0.verified.txt",
    "content": "﻿Config:\n    TargetDirectory: theRoot\n    UrlPrefix: \n    LinkFormat: Tfs\n    Convention: InPlaceOverwrite\n    TocLevel: 5\n    ValidateContent: False\n    OmitSnippetLinks: False\n    TreatMissingAsWarning: False\n    FileConfigPath: theConfigFilePath (exists:False)\n    MaxWidth: 80\n    ExcludeDirectories:\n        Dir1\n        Dir2\n    ExcludeMarkdownDirectories:\n        Dir3\n        Dir4\n    ExcludeSnippetDirectories:\n        Dir5\n        Dir6\n    UrlsAsSnippets:\n        Url1\n        Url2\n    TargetFramework: net9.0"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/LogBuilderTests.BuildConfigLogMessageMinimal.DotNet10_0.verified.txt",
    "content": "﻿Config:\n    TargetDirectory: theRoot\n    UrlPrefix: \n    LinkFormat: GitHub\n    Convention: SourceTransform\n    TocLevel: 0\n    ValidateContent: False\n    OmitSnippetLinks: False\n    TreatMissingAsWarning: False\n    FileConfigPath: theConfigFilePath (exists:False)\n    ReadOnly: \n    WriteHeader: \n    Header:"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/LogBuilderTests.BuildConfigLogMessageMinimal.DotNet9_0.verified.txt",
    "content": "﻿Config:\n    TargetDirectory: theRoot\n    UrlPrefix: \n    LinkFormat: GitHub\n    Convention: SourceTransform\n    TocLevel: 0\n    ValidateContent: False\n    OmitSnippetLinks: False\n    TreatMissingAsWarning: False\n    FileConfigPath: theConfigFilePath (exists:False)\n    ReadOnly: \n    WriteHeader: \n    Header: \n    TargetFramework: net9.0"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/LogBuilderTests.BuildConfigLogMessageSourceTransform.DotNet10_0.verified.txt",
    "content": "﻿Config:\n    TargetDirectory: theRoot\n    UrlPrefix: \n    LinkFormat: Tfs\n    Convention: SourceTransform\n    TocLevel: 5\n    ValidateContent: False\n    OmitSnippetLinks: False\n    TreatMissingAsWarning: False\n    FileConfigPath: theConfigFilePath (exists:False)\n    ReadOnly: True\n    WriteHeader: True\n    Header: \n        line1\n        line2\n    MaxWidth: 80\n    ExcludeDirectories:\n        Dir1\n        Dir2\n    ExcludeMarkdownDirectories:\n        Dir3\n        Dir4\n    ExcludeSnippetDirectories:\n        Dir5\n        Dir6\n    UrlsAsSnippets:\n        Url1\n        Url2"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/LogBuilderTests.BuildConfigLogMessageSourceTransform.DotNet9_0.verified.txt",
    "content": "﻿Config:\n    TargetDirectory: theRoot\n    UrlPrefix: \n    LinkFormat: Tfs\n    Convention: SourceTransform\n    TocLevel: 5\n    ValidateContent: False\n    OmitSnippetLinks: False\n    TreatMissingAsWarning: False\n    FileConfigPath: theConfigFilePath (exists:False)\n    ReadOnly: True\n    WriteHeader: True\n    Header: \n        line1\n        line2\n    MaxWidth: 80\n    ExcludeDirectories:\n        Dir1\n        Dir2\n    ExcludeMarkdownDirectories:\n        Dir3\n        Dir4\n    ExcludeSnippetDirectories:\n        Dir5\n        Dir6\n    UrlsAsSnippets:\n        Url1\n        Url2\n    TargetFramework: net9.0"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/LogBuilderTests.cs",
    "content": "﻿public class LogBuilderTests\n{\n    [Fact]\n    public Task BuildConfigLogMessage()\n    {\n        var config = new ConfigResult\n        {\n            WriteHeader = true,\n            Header = \"\"\"\n                     line1\n                     line2\n                     \"\"\",\n            ExcludeDirectories = [\"Dir1\", \"Dir2\"],\n            ExcludeMarkdownDirectories = [\"Dir3\", \"Dir4\"],\n            ExcludeSnippetDirectories = [\"Dir5\", \"Dir6\"],\n            ReadOnly = true,\n            LinkFormat = LinkFormat.Tfs,\n            UrlsAsSnippets = [\"Url1\", \"Url2\"],\n            TocLevel = 5,\n            MaxWidth = 80,\n            Convention = DocumentConvention.InPlaceOverwrite,\n        };\n        var message = LogBuilder.BuildConfigLogMessage(\"theRoot\", config, \"theConfigFilePath\");\n        return Verify(message)\n            .UniqueForTargetFrameworkAndVersion();\n    }\n\n    [Fact]\n    public Task BuildConfigLogMessageSourceTransform()\n    {\n        var config = new ConfigResult\n        {\n            WriteHeader = true,\n            Header = \"\"\"\n                     line1\n                     line2\n                     \"\"\",\n            ExcludeDirectories = [\"Dir1\", \"Dir2\"],\n            ExcludeMarkdownDirectories = [\"Dir3\", \"Dir4\"],\n            ExcludeSnippetDirectories = [\"Dir5\", \"Dir6\"],\n            ReadOnly = true,\n            LinkFormat = LinkFormat.Tfs,\n            UrlsAsSnippets = [\"Url1\", \"Url2\"],\n            TocLevel = 5,\n            MaxWidth = 80,\n            Convention = DocumentConvention.SourceTransform,\n        };\n        var message = LogBuilder.BuildConfigLogMessage(\"theRoot\", config, \"theConfigFilePath\");\n        return Verify(message)\n            .UniqueForTargetFrameworkAndVersion();\n    }\n\n    [Fact]\n    public Task BuildConfigLogMessageMinimal()\n    {\n        var config = new ConfigResult();\n        var message = LogBuilder.BuildConfigLogMessage(\"theRoot\", config, \"theConfigFilePath\");\n        return Verify(message)\n            .UniqueForTargetFrameworkAndVersion();\n    }\n}"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/MarkdownSnippets.Tool.Tests.csproj",
    "content": "\n<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <OutputType>Exe</OutputType>\n    <NoWarn>$(NoWarn);xUnit1051</NoWarn>\n  </PropertyGroup>\n  <ItemGroup>\n    <PackageReference Include=\"Argon\" />\n    <PackageReference Include=\"ProjectFiles\" />\n    <PackageReference Include=\"Verify.DiffPlex\" />\n    <PackageReference Include=\"Verify.XunitV3\" />\n    <PackageReference Include=\"DiffEngine\" />\n    <PackageReference Include=\"EmptyFiles\" />\n    <PackageReference Include=\"SimpleInfoName\" />\n    <PackageReference Include=\"Verify\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\" PrivateAssets=\"all\" />\n    <PackageReference Include=\"xunit.v3\" />\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" />\n    <ProjectReference Include=\"..\\MarkdownSnippets.Tool\\MarkdownSnippets.Tool.csproj\" />\n    <ProjectReference Include=\"..\\MarkdownSnippets\\MarkdownSnippets.csproj\" />\n    <PackageReference Include=\"ProjectDefaults\" PrivateAssets=\"all\" />\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/MarkdownSnippets.Tool.Tests/ModuleInitializer.cs",
    "content": "﻿public static class ModuleInitializer\n{\n    [ModuleInitializer]\n    public static void Initialize()\n    {\n        VerifyDiffPlex.Initialize(OutputType.Compact);\n        VerifierSettings.IgnoreStackTrace();\n        VerifierSettings.AddScrubber(_ => _.Replace('\\\\', '/'));\n    }\n}"
  },
  {
    "path": "src/MarkdownSnippets.slnx",
    "content": "<Solution>\n  <Folder Name=\"/Solution Items/\">\n    <File Path=\"../readme.source.md\" />\n    <File Path=\"../schema.json\" />\n    <File Path=\"appveyor.yml\" />\n    <File Path=\"Directory.Build.props\" />\n    <File Path=\"Directory.Packages.props\" />\n    <File Path=\"global.json\" />\n  </Folder>\n  <Project Path=\"ConfigReader.Tests/ConfigReader.Tests.csproj\" />\n  <Project Path=\"ConfigReader/ConfigReader.csproj\" />\n  <Project Path=\"MarkdownSnippets.MsBuild/MarkdownSnippets.MsBuild.csproj\" />\n  <Project Path=\"MarkdownSnippets.Tool.Tests/MarkdownSnippets.Tool.Tests.csproj\" />\n  <Project Path=\"MarkdownSnippets.Tool/MarkdownSnippets.Tool.csproj\" />\n  <Project Path=\"MarkdownSnippets/MarkdownSnippets.csproj\" />\n  <Project Path=\"Tests/Tests.csproj\" />\n</Solution>\n"
  },
  {
    "path": "src/MarkdownSnippets.slnx.DotSettings",
    "content": "﻿<wpf:ResourceDictionary xml:space=\"preserve\" xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\" xmlns:s=\"clr-namespace:System;assembly=mscorlib\" xmlns:ss=\"urn:shemas-jetbrains-com:settings-storage-xaml\" xmlns:wpf=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\">\n\t<s:String x:Key=\"/Default/Environment/InjectedLayers/FileInjectedLayer/=BC2D76E54BBA65499DDED823A15FC688/RelativePath/@EntryValue\">..\\Shared.sln.DotSettings</s:String>\n\t<s:Boolean x:Key=\"/Default/Environment/InjectedLayers/FileInjectedLayer/=BC2D76E54BBA65499DDED823A15FC688/@KeyIndexDefined\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/Environment/InjectedLayers/InjectedLayerCustomization/=FileBC2D76E54BBA65499DDED823A15FC688/@KeyIndexDefined\">True</s:Boolean>\n\t<s:Double x:Key=\"/Default/Environment/InjectedLayers/InjectedLayerCustomization/=FileBC2D76E54BBA65499DDED823A15FC688/RelativePriority/@EntryValue\">1</s:Double>\n</wpf:ResourceDictionary>"
  },
  {
    "path": "src/Shared.sln.DotSettings",
    "content": "﻿<wpf:ResourceDictionary xml:space=\"preserve\" xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\" xmlns:s=\"clr-namespace:System;assembly=mscorlib\" xmlns:ss=\"urn:shemas-jetbrains-com:settings-storage-xaml\" xmlns:wpf=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\">\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=InconsistentNaming/@EntryIndexedValue\">DO_NOT_SHOW</s:String>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/ApplyAutoDetectedRules/@EntryValue\">False</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/Environment/Hierarchy/Build/SolutionBuilderNext/LogToFile/@EntryValue\">False</s:Boolean>\n\t<s:String x:Key=\"/Default/Environment/Hierarchy/Build/SolutionBuilderNext/OutputVerbosityLevel/@EntryValue\">Quiet</s:String>\n\t<s:Boolean x:Key=\"/Default/CodeEditing/ContextActionTable/DisabledContextActions/=JetBrains_002EReSharper_002EIntentions_002ECSharp_002EContextActions_002EUseConfigureAwaitFalseAction/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002AMigrations_002A/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002EDesigner_002Ecs/@EntryIndexedValue\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=AllUnderscoreLocalParameterName/@EntryIndexedValue\">DO_NOT_SHOW</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeAccessorOwnerBody/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeConstructorOrDestructorBody/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeLocalFunctionBody/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeMethodOrOperatorBody/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeNamespaceBody/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=CanSimplifyDictionaryTryGetValueWithGetValueOrDefault/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ConditionIsAlwaysTrueOrFalse/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertToCompoundAssignment/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertToNullCoalescingCompoundAssignment/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertToPrimaryConstructor/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ExpressionIsAlwaysNull/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=LocalFunctionCanBeMadeStatic/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MemberCanBeMadeStatic_002EGlobal/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MemberCanBeMadeStatic_002ELocal/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MergeAndPattern/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MergeCastWithTypeCheck/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MergeConditionalExpression/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MergeIntoLogicalPattern/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MergeIntoNegatedPattern/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MergeIntoPattern/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MergeNestedPropertyPatterns/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MergeSequentialChecks/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=NotAccessedField_002ELocal/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=NotAccessedPositionalProperty_002ELocal/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantPatternParentheses/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=TailRecursiveCall/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=PatternIsAlwaysTrueOrFalse/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantAssignment/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantBoolCompare/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantStringInterpolation/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RawStringCanBeSimplified/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantTypeDeclarationBody/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantUsingDirective_002EGlobal/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceAsyncWithTaskReturn/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=SeparateLocalFunctionsWithJumpStatement/@EntryIndexedValue\">DO_NOT_SHOW</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=SimplifyConditionalTernaryExpression/@EntryIndexedValue\">DO_NOT_SHOW</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=StringLiteralAsInterpolationArgument/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=TryStatementsCanBeMerged/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedParameter_002EGlobal/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedParameter_002ELocal/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=TabsAndSpacesMismatch/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=TabsAreDisallowed/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UseCollectionExpression/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UseIndexFromEndExpression/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UseRawString/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UseSwitchCasePatternVariable/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UseSymbolAlias/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UseUtf8StringLiteral/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Browsers/Browsers/@EntryValue\">C90+,E79+,S14+</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ConditionalAccessQualifierIsNonNullableAccordingToAPIContract/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ConstantConditionalAccessQualifier/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=EmptyConstructor/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=EmptyDestructor/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=EmptyNamespace/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=EntityNameCapturedOnly_002EGlobal/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=EntityNameCapturedOnly_002ELocal/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=FormatStringProblem/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=InlineOutVariableDeclaration/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=IsExpressionAlwaysTrue/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MethodHasAsyncOverloadWithCancellation/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MethodSupportsCancellation/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=NotAccessedVariable/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=NotAccessedVariable_002ECompiler/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=PartialMethodWithSinglePart/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=PartialTypeWithSinglePart/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=PatternAlwaysMatches/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=PatternNeverMatches/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=PossibleMultipleEnumeration/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=PrivateFieldCanBeConvertedToLocalVariable/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantAbstractModifier/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantAttributeParentheses/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantAttributeSuffix/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantBaseConstructorCall/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantBaseQualifier/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantCaseLabel/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantCast/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantCatchClause/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantDefaultMemberInitializer/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantEmptyObjectOrCollectionInitializer/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantEnumerableCastCall/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantExplicitArrayCreation/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantExtendsListEntry/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantIfElseBlock/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantIsBeforeRelationalPattern/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantNotNullConstraint/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantNullableFlowAttribute/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantNullableTypeMark/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantOverload_002EGlobal/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantOverload_002ELocal/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantOverriddenMember/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantRecordClassKeyword/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantStringFormatCall/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantStringToCharArrayCall/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantToStringCall/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantToStringCallForValueType/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantTypeArgumentsOfMethod/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantTypeCheckInPattern/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantUnsafeContext/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantUsingDirective/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantVerbatimPrefix/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantVerbatimStringPrefix/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=SealedMemberInSealedClass/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ThreadStaticFieldHasInitializer/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UnnecessaryWhitespace/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedLocalFunction_002ECompiler/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedLocalFunction/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedLocalFunctionReturnValue/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedTupleComponentInReturnValue/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedTypeParameter/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedVariable/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedVariable_002ECompiler/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UseCancellationTokenForIAsyncEnumerable/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UseConfigureAwaitFalse/@EntryIndexedValue\">DO_NOT_SHOW</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/GeneratedCode/GeneratedFileMasks/=_002A_002Ereceived_002E_002A/@EntryIndexedValue\">*.received.*</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/GeneratedCode/GeneratedFileMasks/=_002A_002Everified_002E_002A/@EntryIndexedValue\">*.verified.*</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantSuppressNullableWarningExpression/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UseObjectOrCollectionInitializer/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=VariableHidesOuterVariable/@EntryIndexedValue\">DO_NOT_SHOW</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/JsInspections/LanguageLevel/@EntryValue\">ECMAScript 2016</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeCleanup/Profiles/=c_0023_0020Cleanup/@EntryIndexedValue\">&lt;?xml version=\"1.0\" encoding=\"utf-16\"?&gt;&lt;Profile name=\"c# Cleanup\"&gt;&lt;AspOptimizeRegisterDirectives&gt;True&lt;/AspOptimizeRegisterDirectives&gt;&lt;CSCodeStyleAttributes ArrangeVarStyle=\"True\" ArrangeTypeAccessModifier=\"True\" ArrangeTypeMemberAccessModifier=\"True\" SortModifiers=\"True\" RemoveRedundantParentheses=\"True\" AddMissingParentheses=\"True\" ArrangeBraces=\"True\" ArrangeAttributes=\"True\" ArrangeCodeBodyStyle=\"True\" ArrangeTrailingCommas=\"True\" ArrangeObjectCreation=\"True\" ArrangeDefaultValue=\"True\" ArrangeNamespaces=\"True\" /&gt;&lt;CssAlphabetizeProperties&gt;True&lt;/CssAlphabetizeProperties&gt;&lt;JSStringLiteralQuotesDescriptor&gt;True&lt;/JSStringLiteralQuotesDescriptor&gt;&lt;CorrectVariableKindsDescriptor&gt;True&lt;/CorrectVariableKindsDescriptor&gt;&lt;VariablesToInnerScopesDescriptor&gt;True&lt;/VariablesToInnerScopesDescriptor&gt;&lt;StringToTemplatesDescriptor&gt;True&lt;/StringToTemplatesDescriptor&gt;&lt;JsInsertSemicolon&gt;True&lt;/JsInsertSemicolon&gt;&lt;RemoveRedundantQualifiersTs&gt;True&lt;/RemoveRedundantQualifiersTs&gt;&lt;OptimizeImportsTs&gt;True&lt;/OptimizeImportsTs&gt;&lt;OptimizeReferenceCommentsTs&gt;True&lt;/OptimizeReferenceCommentsTs&gt;&lt;PublicModifierStyleTs&gt;True&lt;/PublicModifierStyleTs&gt;&lt;ExplicitAnyTs&gt;True&lt;/ExplicitAnyTs&gt;&lt;TypeAnnotationStyleTs&gt;True&lt;/TypeAnnotationStyleTs&gt;&lt;RelativePathStyleTs&gt;True&lt;/RelativePathStyleTs&gt;&lt;AsInsteadOfCastTs&gt;True&lt;/AsInsteadOfCastTs&gt;&lt;RemoveCodeRedundancies&gt;True&lt;/RemoveCodeRedundancies&gt;&lt;CSUseAutoProperty&gt;True&lt;/CSUseAutoProperty&gt;&lt;CSMakeFieldReadonly&gt;True&lt;/CSMakeFieldReadonly&gt;&lt;CSMakeAutoPropertyGetOnly&gt;True&lt;/CSMakeAutoPropertyGetOnly&gt;&lt;CSArrangeQualifiers&gt;True&lt;/CSArrangeQualifiers&gt;&lt;CSFixBuiltinTypeReferences&gt;True&lt;/CSFixBuiltinTypeReferences&gt;&lt;CssReformatCode&gt;True&lt;/CssReformatCode&gt;&lt;JsReformatCode&gt;True&lt;/JsReformatCode&gt;&lt;JsFormatDocComments&gt;True&lt;/JsFormatDocComments&gt;&lt;CSOptimizeUsings&gt;&lt;OptimizeUsings&gt;True&lt;/OptimizeUsings&gt;&lt;/CSOptimizeUsings&gt;&lt;CSShortenReferences&gt;True&lt;/CSShortenReferences&gt;&lt;CSReformatCode&gt;True&lt;/CSReformatCode&gt;&lt;CSharpFormatDocComments&gt;True&lt;/CSharpFormatDocComments&gt;&lt;FormatAttributeQuoteDescriptor&gt;True&lt;/FormatAttributeQuoteDescriptor&gt;&lt;HtmlReformatCode&gt;True&lt;/HtmlReformatCode&gt;&lt;XAMLCollapseEmptyTags&gt;False&lt;/XAMLCollapseEmptyTags&gt;&lt;IDEA_SETTINGS&gt;&amp;lt;profile version=\"1.0\"&amp;gt;&#xD;\n  &amp;lt;option name=\"myName\" value=\"c# Cleanup\" /&amp;gt;&#xD;\n&amp;lt;/profile&amp;gt;&lt;/IDEA_SETTINGS&gt;&lt;RIDER_SETTINGS&gt;&amp;lt;profile&amp;gt;&#xD;\n  &amp;lt;Language id=\"EditorConfig\"&amp;gt;&#xD;\n    &amp;lt;Reformat&amp;gt;false&amp;lt;/Reformat&amp;gt;&#xD;\n  &amp;lt;/Language&amp;gt;&#xD;\n  &amp;lt;Language id=\"HTML\"&amp;gt;&#xD;\n    &amp;lt;OptimizeImports&amp;gt;false&amp;lt;/OptimizeImports&amp;gt;&#xD;\n    &amp;lt;Reformat&amp;gt;false&amp;lt;/Reformat&amp;gt;&#xD;\n    &amp;lt;Rearrange&amp;gt;false&amp;lt;/Rearrange&amp;gt;&#xD;\n  &amp;lt;/Language&amp;gt;&#xD;\n  &amp;lt;Language id=\"JSON\"&amp;gt;&#xD;\n    &amp;lt;Reformat&amp;gt;false&amp;lt;/Reformat&amp;gt;&#xD;\n  &amp;lt;/Language&amp;gt;&#xD;\n  &amp;lt;Language id=\"RELAX-NG\"&amp;gt;&#xD;\n    &amp;lt;Reformat&amp;gt;false&amp;lt;/Reformat&amp;gt;&#xD;\n  &amp;lt;/Language&amp;gt;&#xD;\n  &amp;lt;Language id=\"XML\"&amp;gt;&#xD;\n    &amp;lt;OptimizeImports&amp;gt;false&amp;lt;/OptimizeImports&amp;gt;&#xD;\n    &amp;lt;Reformat&amp;gt;false&amp;lt;/Reformat&amp;gt;&#xD;\n    &amp;lt;Rearrange&amp;gt;false&amp;lt;/Rearrange&amp;gt;&#xD;\n  &amp;lt;/Language&amp;gt;&#xD;\n&amp;lt;/profile&amp;gt;&lt;/RIDER_SETTINGS&gt;&lt;/Profile&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/CONSTRUCTOR_OR_DESTRUCTOR_BODY/@EntryValue\">ExpressionBody</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/LOCAL_FUNCTION_BODY/@EntryValue\">ExpressionBody</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/METHOD_OR_OPERATOR_BODY/@EntryValue\">ExpressionBody</s:String>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/USE_HEURISTICS_FOR_BODY_STYLE/@EntryValue\">False</s:Boolean>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_ACCESSORHOLDER_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue\">NEVER</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_FIELD_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue\">NEVER</s:String>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_SIMPLE_ANONYMOUSMETHOD_ON_SINGLE_LINE/@EntryValue\">False</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_BEFORE_BINARY_OPSIGN/@EntryValue\">False</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_BEFORE_BINARY_PATTERN_OP/@EntryValue\">False</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_BEFORE_FIRST_METHOD_CALL/@EntryValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_LINES/@EntryValue\">False</s:Boolean>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_MULTIPLE_TYPE_PARAMEER_CONSTRAINTS_STYLE/@EntryValue\">CHOP_ALWAYS</s:String>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/JavaScriptCodeFormatting/WRAP_LINES/@EntryValue\">False</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/ProtobufCodeFormatting/WRAP_LINES/@EntryValue\">False</s:Boolean>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/XmlDocFormatter/IndentSubtags/@EntryValue\">RemoveIndent</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/XmlDocFormatter/IndentTagContent/@EntryValue\">RemoveIndent</s:String>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/XmlDocFormatter/WRAP_LINES/@EntryValue\">False</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/Environment/ExcludedFiles/FilesAndFoldersToSkip/=6E616A53_002D9BF6_002D4F71_002D9E8E_002D2D99C3CB85F4_002Fd_003Awwwroot_002Fd_003A_005Fcontent_002Fd_003Atinymce/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=SimplifyLinqExpressionUseMinByAndMaxBy/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/Housekeeping/UnitTestingMru/UnitTestRunner/SpawnedProcessesResponse/@EntryValue\">DoNothing</s:String>\n\t</wpf:ResourceDictionary>"
  },
  {
    "path": "src/Tests/ContentValidationTest.CheckInvalidWord.verified.txt",
    "content": "﻿[\n  {\n    Item1: Invalid word detected: 'you',\n    Item2: 1\n  }\n]"
  },
  {
    "path": "src/Tests/ContentValidationTest.CheckInvalidWordIndicatesAllViolationsInTheExceptionMessage.verified.txt",
    "content": "﻿[\n  {\n    Item1: No exclamation marks. If a statement is important make it bold. https://www.technicalcommunicationcenter.com/2011/12/30/the-discipline-of-punctuation-in-technical-writing/. ,\n    Item2: 20\n  },\n  {\n    Item1: Invalid word detected: 'you',\n    Item2: 1\n  },\n  {\n    Item1: Invalid word detected: 'yourself',\n    Item2: 27\n  }\n]"
  },
  {
    "path": "src/Tests/ContentValidationTest.CheckInvalidWordIndicatesAllViolationsInTheExceptionMessageIgnoringCase.verified.txt",
    "content": "﻿[\n  {\n    Item1: No exclamation marks. If a statement is important make it bold. https://www.technicalcommunicationcenter.com/2011/12/30/the-discipline-of-punctuation-in-technical-writing/. ,\n    Item2: 20\n  },\n  {\n    Item1: Invalid word detected: 'you',\n    Item2: 1\n  },\n  {\n    Item1: Invalid word detected: 'yourself',\n    Item2: 27\n  },\n  {\n    Item1: Invalid word detected: 'us',\n    Item2: 37\n  }\n]"
  },
  {
    "path": "src/Tests/ContentValidationTest.CheckInvalidWordSentenceEnd.verified.txt",
    "content": "﻿[\n  {\n    Item1: Invalid word detected: 'you',\n    Item2: 1\n  }\n]"
  },
  {
    "path": "src/Tests/ContentValidationTest.CheckInvalidWordSentenceStart.verified.txt",
    "content": "﻿[\n  {\n    Item1: Invalid word detected: 'you'\n  }\n]"
  },
  {
    "path": "src/Tests/ContentValidationTest.CheckInvalidWordStringEnd.verified.txt",
    "content": "﻿[\n  {\n    Item1: Invalid word detected: 'you',\n    Item2: 4\n  }\n]"
  },
  {
    "path": "src/Tests/ContentValidationTest.CheckInvalidWordWithComma.verified.txt",
    "content": "﻿[\n  {\n    Item1: Invalid word detected: 'you',\n    Item2: 1\n  }\n]"
  },
  {
    "path": "src/Tests/ContentValidationTest.CheckInvalidWordWithQuestionMark.verified.txt",
    "content": "﻿[\n  {\n    Item1: Invalid word detected: 'you',\n    Item2: 1\n  }\n]"
  },
  {
    "path": "src/Tests/ContentValidationTest.cs",
    "content": "public class ContentValidationTest\n{\n    [Fact]\n    public Task CheckInvalidWord() => Verify(ContentValidation.Verify(\" you \"));\n\n    [Fact]\n    public Task CheckInvalidWordIndicatesAllViolationsInTheExceptionMessage() =>\n        Verify(ContentValidation.Verify(\" you, and you again! Still yourself? \"));\n\n    [Fact]\n    public Task CheckInvalidWordIndicatesAllViolationsInTheExceptionMessageIgnoringCase() =>\n        Verify(ContentValidation.Verify(\" you, and you again! Still Yourself? Us\"));\n\n    [Fact]\n    public Task CheckInvalidWordWithQuestionMark() =>\n        Verify(ContentValidation.Verify(\" you? \"));\n\n    [Fact]\n    public Task CheckInvalidWordWithComma() =>\n        Verify(ContentValidation.Verify(\" you, \"));\n\n    [Fact]\n    public Task CheckInvalidWordSentenceEnd() =>\n        Verify(ContentValidation.Verify(\" you. \"));\n\n    [Fact]\n    public Task CheckInvalidWordSentenceStart() =>\n        Verify(ContentValidation.Verify(\"you \"));\n\n    [Fact]\n    public Task CheckInvalidWordStringEnd() =>\n        Verify(ContentValidation.Verify(\"the you\"));\n\n    [Fact]\n    public void CheckInvalidWordDoesNotThrowWhenNoMatch() =>\n        Assert.Empty(ContentValidation.Verify(\" some random content which doesn't contain invalid words. \"));\n\n    [Fact]\n    public void CheckInvalidWordDoesNotThrowWhenIsQuote() =>\n        Assert.Empty(ContentValidation.Verify(\"> you \"));\n\n    [Fact]\n    public void CheckInvalidWordInUrl()\n    {\n        Assert.Empty(ContentValidation.Verify(\"some random content containing links /us/allowed/\"));\n        Assert.Empty(ContentValidation.Verify(\"some random content containing links /yourself/us/\"));\n        Assert.Empty(ContentValidation.Verify(\" /us/ \"));\n        Assert.Empty(ContentValidation.Verify(\"/us-\"));\n    }\n}"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/BinaryFileSnippet/one.source.md",
    "content": "snippet: sourceFile.dot"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/BinaryFileSnippet/sourceFile.dot",
    "content": "From Source File"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/Convention/mdsource/two.source.md",
    "content": "snippet: snippet2"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/Convention/one.source.md",
    "content": "snippet: snippet1"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/ConventionWithNestedDir/mdsource/Nested/one.source.md",
    "content": "snippet: snippet1"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/ExplicitFileInclude/Nested/fileToInclude3.txt",
    "content": "The include text 3"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/ExplicitFileInclude/Nested/fileToInclude4.txt",
    "content": "The include text 4"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/ExplicitFileInclude/Nested/fileToInclude5.txt",
    "content": "The include text 5"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/ExplicitFileInclude/Nested/fileToInclude6.txt",
    "content": "The include text 6"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/ExplicitFileInclude/fileToInclude1.txt",
    "content": "The include text 1"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/ExplicitFileInclude/fileToInclude2.txt",
    "content": "The include text 2"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/ExplicitFileInclude/one.source.md",
    "content": "include: fileToInclude1.txt\n\ninclude: /fileToInclude2.txt\n\ninclude: fileToInclude3.txt\n\ninclude: /fileToInclude4.txt\n\ninclude: Nested/fileToInclude5.txt\n\ninclude: /Nested/fileToInclude6.txt"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/ExplicitFileIncludeWithMergedSnippet/fileToInclude.txt",
    "content": "The include text\n\n<!-- snippet: snippet1 -->\n<a id='snippet-snippet1'></a>\n```.cs\nthe code from snippet1\n```\n<sup><a href='#snippet-snippet1' title='Navigate to start of snippet `snippet1`'>anchor</a></sup>\n<!-- endSnippet -->\n"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/ExplicitFileIncludeWithMergedSnippet/one.source.md",
    "content": "some content\n\ninclude: fileToInclude.txt"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/ExplicitFileIncludeWithSnippetAtEnd/fileToInclude.txt",
    "content": "The include text\n\nsnippet: snippet1"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/ExplicitFileIncludeWithSnippetAtEnd/one.source.md",
    "content": "include: fileToInclude.txt"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/FileSnippet/one.source.md",
    "content": "Local\n\nsnippet: sourceFile.txt\n\nRelative Local\n\nsnippet: ./sourceFile.txt\n\nRooted\n\nsnippet: /sourceFile.txt\n\n"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/FileSnippet/sourceFile.txt",
    "content": "From Source File"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/FileSnippetMissing/one.source.md",
    "content": "snippet: missing.txt"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/FileSnippetWithHash/one.source.md",
    "content": "Local\n\nsnippet: source#File.txt\n\nRelative Local\n\nsnippet: ./source#File.txt\n\nRooted\n\nsnippet: /source#File.txt\n\n"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/FileSnippetWithHash/source#File.txt",
    "content": "From Source File"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/FileSnippetWithWhiteSpace/one.source.md",
    "content": "Local\n\nsnippet: sourceFile.txt\n\nRelative Local\n\nsnippet: ./sourceFile.txt\n\nRooted\n\nsnippet: /sourceFile.txt\n\n"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/FileSnippetWithWhiteSpace/sourceFile.txt",
    "content": "\n\n\nFrom Source File\n\n\n\n"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/InPlaceOverwriteExists/file.md",
    "content": "<!-- snippet: snippet1 -->\n<a id='snippet-snippet1'></a>\n```.cs\nthe code from snippet1\n```\n<sup><a href='#snippet-snippet1' title='Navigate to start of snippet `snippet1`'>anchor</a></sup>\n<!-- endSnippet -->\n\nBad text <!-- singleLineInclude: fileToInclude.txt. path: /fileToInclude.txt -->\n\nBad Line 1 <!-- include: multiLineFileToInclude.txt. path: /multiLineFileToInclude.txt -->\n\nBad Line  2 <!-- endInclude -->\n\n<!-- include: includeWithCode.txt. path: /includeWithCode.txt -->\n```\nBad Code\n```\n<!-- endInclude -->\n"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/InPlaceOverwriteExists/fileToInclude.txt",
    "content": "The include text"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/InPlaceOverwriteExists/includeWithCode.txt",
    "content": "```\nThe Code\n```"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/InPlaceOverwriteExists/multiLineFileToInclude.txt",
    "content": "Line 1\n\nLine 2"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/InPlaceOverwriteExistsMdx/file.mdx",
    "content": "<!-- snippet: snippet1 -->\n<a id='snippet-snippet1'></a>\n```.cs\nthe code from snippet1\n```\n<sup><a href='#snippet-snippet1' title='Navigate to start of snippet `snippet1`'>anchor</a></sup>\n<!-- endSnippet -->\n\nBad text <!-- singleLineInclude: fileToInclude.txt. path: /fileToInclude.txt -->\n\nBad Line 1 <!-- include: multiLineFileToInclude.txt. path: /multiLineFileToInclude.txt -->\n\nBad Line  2 <!-- endInclude -->\n\n<!-- include: includeWithCode.txt. path: /includeWithCode.txt -->\n```\nBad Code\n```\n<!-- endInclude -->\n"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/InPlaceOverwriteExistsMdx/fileToInclude.txt",
    "content": "The include text"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/InPlaceOverwriteExistsMdx/includeWithCode.txt",
    "content": "```\nThe Code\n```"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/InPlaceOverwriteExistsMdx/multiLineFileToInclude.txt",
    "content": "Line 1\n\nLine 2"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/InPlaceOverwriteNotExists/file.md",
    "content": "snippet: snippet1\n\ninclude: fileToInclude.txt\n\ninclude: multiLineFileToInclude.txt\n\ninclude: includeWithCode.txt"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/InPlaceOverwriteNotExists/fileToInclude.txt",
    "content": "The include text"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/InPlaceOverwriteNotExists/includeWithCode.txt",
    "content": "```\nThe Code\n```"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/InPlaceOverwriteNotExists/multiLineFileToInclude.txt",
    "content": "Line 1\n\nLine 2"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/InPlaceOverwriteUrlInclude/one.md",
    "content": "BAD <!-- include: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/license.txt. path:  -->\nBAD\nBAD <!-- endInclude -->"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/InPlaceOverwriteUrlSnippet/one.md",
    "content": "<!-- snippet: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/license.txt -->\n<a id='snippet-https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/license.txt'></a>\nBAD\n<!-- endSnippet -->"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/InPlaceOverwriteWithFileSnippetMissing/file.md",
    "content": "<!-- snippet: missing.txt -->\n<!-- endSnippet -->"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/Mdx/one.source.mdx",
    "content": "Local\n\nsnippet: sourceFile.txt\n\nRelative Local\n\nsnippet: ./sourceFile.txt\n\nRooted\n\nsnippet: /sourceFile.txt\n\n"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/Mdx/sourceFile.txt",
    "content": "From Source File"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/MissingInclude/one.md",
    "content": "<!--\nGENERATED FILE - DO NOT EDIT\nThis file was generated by [MarkdownSnippets](https://github.com/SimonCropp/MarkdownSnippets).\nSource File: /src/Tests/DirectoryMarkdownProcessor/ReadOnly/one.source.md\nTo change this file edit the source file and then run MarkdownSnippets.\n-->\n\n<!-- snippet: snippet1 -->\n<a id='snippet-snippet1'/></a>\n```txt\nthis is some text to import\n```\n<sup><a href='/src/Tests/badsnippets/code.txt#L3-L5' title='File snippet `snippet1` was extracted from'>snippet source</a> | <a href='#snippet-snippet1' title='Navigate to start of snippet `snippet1`'>anchor</a></sup>\n<a id='snippet-snippet1-1'/></a>\n```txt\nthis is some text to import\n```\n<sup><a href='/src/Tests/badsnippets/code.txt#L6-L8' title='File snippet `snippet1` was extracted from'>snippet source</a> | <a href='#snippet-snippet1-1' title='Navigate to start of snippet `snippet1`'>anchor</a></sup>\n<a id='snippet-snippet1-2'/></a>\n```txt\nSome code\n```\n<sup><a href='/src/Tests/DirectorySnippetExtractor/Simple/code2.txt#L1-L3' title='File snippet `snippet1` was extracted from'>snippet source</a> | <a href='#snippet-snippet1-2' title='Navigate to start of snippet `snippet1`'>anchor</a></sup>\n<a id='snippet-snippet1-3'/></a>\n```txt\nSome code\n```\n<sup><a href='/src/Tests/DirectorySnippetExtractor/Simple/code4.txt#L1-L3' title='File snippet `snippet1` was extracted from'>snippet source</a> | <a href='#snippet-snippet1-3' title='Navigate to start of snippet `snippet1`'>anchor</a></sup>\n<a id='snippet-snippet1-4'/></a>\n```txt\nSome code\n```\n<sup><a href='/src/Tests/DirectorySnippetExtractor/VerifyLambdasAreCalled/subpath/code4.txt#L1-L3' title='File snippet `snippet1` was extracted from'>snippet source</a> | <a href='#snippet-snippet1-4' title='Navigate to start of snippet `snippet1`'>anchor</a></sup>\n<a id='snippet-snippet1-5'/></a>\n```txt\nSome code\n```\n<sup><a href='/src/Tests/SnippetFileFinder/Simple/code2.txt#L1-L3' title='File snippet `snippet1` was extracted from'>snippet source</a> | <a href='#snippet-snippet1-5' title='Navigate to start of snippet `snippet1`'>anchor</a></sup>\n<a id='snippet-snippet1-6'/></a>\n```txt\nSome code\n```\n<sup><a href='/src/Tests/SnippetFileFinder/Simple/code4.txt#L1-L3' title='File snippet `snippet1` was extracted from'>snippet source</a> | <a href='#snippet-snippet1-6' title='Navigate to start of snippet `snippet1`'>anchor</a></sup>\n<a id='snippet-snippet1-7'/></a>\n```txt\nSome code\n```\n<sup><a href='/src/Tests/SnippetFileFinder/VerifyLambdasAreCalled/subpath/code4.txt#L1-L3' title='File snippet `snippet1` was extracted from'>snippet source</a> | <a href='#snippet-snippet1-7' title='Navigate to start of snippet `snippet1`'>anchor</a></sup>\n<!-- endSnippet -->\n"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/MissingInclude/one.source.md",
    "content": "include: include1"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/MixedCaseInclude/fileToInclude.txt",
    "content": "The include text"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/MixedCaseInclude/one.source.md",
    "content": "include: fIletoinClude.txt"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/NonMd/mdsource/two.source.txt",
    "content": "snippet: snippet2"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/NonMd/one.source.txt",
    "content": "snippet: snippet1"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/ReadOnly/one.source.md",
    "content": "snippet: snippet1"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/UrlInclude/one.source.md",
    "content": "include: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/license.txt"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/UrlIncludeMissing/one.source.md",
    "content": "include: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/missing.txt"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/UrlSnippet/one.source.md",
    "content": "snippet: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/license.txt"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/UrlSnippetMissing/one.source.md",
    "content": "snippet: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/missing.txt"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/ValidationErrors/one.md",
    "content": "<!--\nGENERATED FILE - DO NOT EDIT\nThis file was generated by [MarkdownSnippets](https://github.com/SimonCropp/MarkdownSnippets).\nSource File: /src/Tests/DirectoryMarkdownProcessor/ReadOnly/one.source.md\nTo change this file edit the source file and then run MarkdownSnippets.\n-->\n\n<!-- snippet: snippet1 -->\n<a id='snippet-snippet1'/></a>\n```txt\nthis is some text to import\n```\n<sup><a href='/src/Tests/badsnippets/code.txt#L3-L5' title='File snippet `snippet1` was extracted from'>snippet source</a> | <a href='#snippet-snippet1' title='Navigate to start of snippet `snippet1`'>anchor</a></sup>\n<a id='snippet-snippet1-1'/></a>\n```txt\nthis is some text to import\n```\n<sup><a href='/src/Tests/badsnippets/code.txt#L6-L8' title='File snippet `snippet1` was extracted from'>snippet source</a> | <a href='#snippet-snippet1-1' title='Navigate to start of snippet `snippet1`'>anchor</a></sup>\n<a id='snippet-snippet1-2'/></a>\n```txt\nSome code\n```\n<sup><a href='/src/Tests/DirectorySnippetExtractor/Simple/code2.txt#L1-L3' title='File snippet `snippet1` was extracted from'>snippet source</a> | <a href='#snippet-snippet1-2' title='Navigate to start of snippet `snippet1`'>anchor</a></sup>\n<a id='snippet-snippet1-3'/></a>\n```txt\nSome code\n```\n<sup><a href='/src/Tests/DirectorySnippetExtractor/Simple/code4.txt#L1-L3' title='File snippet `snippet1` was extracted from'>snippet source</a> | <a href='#snippet-snippet1-3' title='Navigate to start of snippet `snippet1`'>anchor</a></sup>\n<a id='snippet-snippet1-4'/></a>\n```txt\nSome code\n```\n<sup><a href='/src/Tests/DirectorySnippetExtractor/VerifyLambdasAreCalled/subpath/code4.txt#L1-L3' title='File snippet `snippet1` was extracted from'>snippet source</a> | <a href='#snippet-snippet1-4' title='Navigate to start of snippet `snippet1`'>anchor</a></sup>\n<a id='snippet-snippet1-5'/></a>\n```txt\nSome code\n```\n<sup><a href='/src/Tests/SnippetFileFinder/Simple/code2.txt#L1-L3' title='File snippet `snippet1` was extracted from'>snippet source</a> | <a href='#snippet-snippet1-5' title='Navigate to start of snippet `snippet1`'>anchor</a></sup>\n<a id='snippet-snippet1-6'/></a>\n```txt\nSome code\n```\n<sup><a href='/src/Tests/SnippetFileFinder/Simple/code4.txt#L1-L3' title='File snippet `snippet1` was extracted from'>snippet source</a> | <a href='#snippet-snippet1-6' title='Navigate to start of snippet `snippet1`'>anchor</a></sup>\n<a id='snippet-snippet1-7'/></a>\n```txt\nSome code\n```\n<sup><a href='/src/Tests/SnippetFileFinder/VerifyLambdasAreCalled/subpath/code4.txt#L1-L3' title='File snippet `snippet1` was extracted from'>snippet source</a> | <a href='#snippet-snippet1-7' title='Navigate to start of snippet `snippet1`'>anchor</a></sup>\n<!-- endSnippet -->\n"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessor/ValidationErrors/one.source.md",
    "content": "you\n we\n !"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessorTests.BinaryFileSnippet.verified.txt",
    "content": "﻿<!-- snippet: sourceFile.dot -->\n<a id='snippet-sourceFile.dot'></a>\n```dot\nFrom Source File\n```\n<sup><a href='#snippet-sourceFile.dot' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessorTests.Convention.verified.txt",
    "content": "﻿/mdsource/two.source.md\nsnippet: snippet2\n\n/one.md\n<!-- snippet: snippet1 -->\n<a id='snippet-snippet1'></a>\n```cs\nthe code from snippet1\n```\n<sup><a href='#snippet-snippet1' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\n/one.source.md\nsnippet: snippet1\n\n/two.md\n<!-- snippet: snippet2 -->\n<a id='snippet-snippet2'></a>\n```cs\nthe code from snippet2\n```\n<sup><a href='#snippet-snippet2' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\n"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessorTests.ConventionWithNestedDir.verified.txt",
    "content": "﻿/mdsource/Nested/one.source.md\nsnippet: snippet1\n\n/Nested/one.md\n<!-- snippet: snippet1 -->\n<a id='snippet-snippet1'></a>\n```cs\nthe code from snippet1\n```\n<sup><a href='#snippet-snippet1' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\n"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessorTests.ExplicitFileInclude.verified.txt",
    "content": "﻿The include text 1<!-- singleLineInclude: fileToInclude1.txt -->\n\nThe include text 2<!-- singleLineInclude: /fileToInclude2.txt -->\n\nThe include text 3<!-- singleLineInclude: fileToInclude3.txt -->\n\nThe include text 4<!-- singleLineInclude: /fileToInclude4.txt -->\n\nThe include text 5<!-- singleLineInclude: Nested/fileToInclude5.txt -->\n\nThe include text 6<!-- singleLineInclude: /Nested/fileToInclude6.txt -->\n"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessorTests.ExplicitFileIncludeWithMergedSnippet.verified.txt",
    "content": "﻿some content\n\nThe include text<!-- include: fileToInclude.txt -->\n\n<!-- snippet: snippet1 -->\n<a id='snippet-snippet1'></a>\n```.cs\nthe code from snippet1\n```\n<sup><a href='#snippet-snippet1' title='Navigate to start of snippet `snippet1`'>anchor</a></sup>\n<!-- endSnippet -->\n<!-- endInclude -->\n"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessorTests.ExplicitFileIncludeWithSnippetAtEnd.verified.txt",
    "content": "﻿The include text<!-- include: fileToInclude.txt -->\n\n<!-- snippet: snippet1 -->\n<a id='snippet-snippet1'></a>\n```cs\nthe code from snippet1\n```\n<sup><a href='#snippet-snippet1' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n<!-- endInclude -->\n"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessorTests.FileSnippet.verified.txt",
    "content": "﻿Local\n\n<!-- snippet: sourceFile.txt -->\n<a id='snippet-sourceFile.txt'></a>\n```txt\nFrom Source File\n```\n<sup><a href='/sourceFile.txt#L1-L1' title='Snippet source file'>snippet source</a> | <a href='#snippet-sourceFile.txt' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\nRelative Local\n\n<!-- snippet: ./sourceFile.txt -->\n<a id='snippet-./sourceFile.txt'></a>\n```txt\nFrom Source File\n```\n<sup><a href='#snippet-./sourceFile.txt' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\nRooted\n\n<!-- snippet: /sourceFile.txt -->\n<a id='snippet-/sourceFile.txt'></a>\n```txt\nFrom Source File\n```\n<sup><a href='/sourceFile.txt#L1-L1' title='Snippet source file'>snippet source</a> | <a href='#snippet-/sourceFile.txt' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\n"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessorTests.FileSnippetExplicitIncludeBypassesExcludeSnippetFiles.verified.txt",
    "content": "﻿Local\n\n<!-- snippet: sourceFile.txt -->\n<a id='snippet-sourceFile.txt'></a>\n```txt\nFrom Source File\n```\n<sup><a href='#snippet-sourceFile.txt' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\nRelative Local\n\n<!-- snippet: ./sourceFile.txt -->\n<a id='snippet-./sourceFile.txt'></a>\n```txt\nFrom Source File\n```\n<sup><a href='#snippet-./sourceFile.txt' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\nRooted\n\n<!-- snippet: /sourceFile.txt -->\n<a id='snippet-/sourceFile.txt'></a>\n```txt\nFrom Source File\n```\n<sup><a href='#snippet-/sourceFile.txt' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\n"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessorTests.FileSnippetMissing.verified.txt",
    "content": "﻿{\n  Type: MissingSnippetsException,\n  Missing: [\n    {\n      Key: missing.txt,\n      LineNumber: 1,\n      File: {CurrentDirectory}DirectoryMarkdownProcessor/FileSnippetMissing/one.source.md\n    }\n  ],\n  Message:\nMissing snippets:\n  {CurrentDirectory}DirectoryMarkdownProcessor/FileSnippetMissing/one.source.md: missing.txt\n}"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessorTests.FileSnippetWithHash.verified.txt",
    "content": "﻿Local\n\n<!-- snippet: source#File.txt -->\n<a id='snippet-source#File.txt'></a>\n```txt\nFrom Source File\n```\n<sup><a href='/source%23File.txt#L1-L1' title='Snippet source file'>snippet source</a> | <a href='#snippet-source#File.txt' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\nRelative Local\n\n<!-- snippet: ./source#File.txt -->\n<a id='snippet-./source#File.txt'></a>\n```txt\nFrom Source File\n```\n<sup><a href='#snippet-./source#File.txt' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\nRooted\n\n<!-- snippet: /source#File.txt -->\n<a id='snippet-/source#File.txt'></a>\n```txt\nFrom Source File\n```\n<sup><a href='/source%23File.txt#L1-L1' title='Snippet source file'>snippet source</a> | <a href='#snippet-/source#File.txt' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\n"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessorTests.FileSnippetWithWhiteSpace.verified.txt",
    "content": "﻿Local\n\n<!-- snippet: sourceFile.txt -->\n<a id='snippet-sourceFile.txt'></a>\n```txt\nFrom Source File\n```\n<sup><a href='/sourceFile.txt#L1-L7' title='Snippet source file'>snippet source</a> | <a href='#snippet-sourceFile.txt' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\nRelative Local\n\n<!-- snippet: ./sourceFile.txt -->\n<a id='snippet-./sourceFile.txt'></a>\n```txt\nFrom Source File\n```\n<sup><a href='#snippet-./sourceFile.txt' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\nRooted\n\n<!-- snippet: /sourceFile.txt -->\n<a id='snippet-/sourceFile.txt'></a>\n```txt\nFrom Source File\n```\n<sup><a href='/sourceFile.txt#L1-L7' title='Snippet source file'>snippet source</a> | <a href='#snippet-/sourceFile.txt' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\n"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessorTests.InPlaceOverwriteExists.verified.md",
    "content": "﻿<!-- snippet: snippet1 -->\n<a id='snippet-snippet1'></a>\n```cs\nthe code from snippet1\n```\n<sup><a href='#snippet-snippet1' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\nThe include text<!-- singleLineInclude: fileToInclude.txt -->\n\nLine 1<!-- include: multiLineFileToInclude.txt -->\n\nLine 2<!-- endInclude -->\n\n<!-- include: includeWithCode.txt -->\n```\nThe Code\n```\n<!-- endInclude -->\n"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessorTests.InPlaceOverwriteExistsMdx.verified.mdx",
    "content": "﻿<!-- snippet: snippet1 -->\n<a id='snippet-snippet1'></a>\n```cs\nthe code from snippet1\n```\n<sup><a href='#snippet-snippet1' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\nThe include text<!-- singleLineInclude: fileToInclude.txt -->\n\nLine 1<!-- include: multiLineFileToInclude.txt -->\n\nLine 2<!-- endInclude -->\n\n<!-- include: includeWithCode.txt -->\n```\nThe Code\n```\n<!-- endInclude -->\n"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessorTests.InPlaceOverwriteNotExists.verified.md",
    "content": "﻿<!-- snippet: snippet1 -->\n<a id='snippet-snippet1'></a>\n```cs\nthe code from snippet1\n```\n<sup><a href='#snippet-snippet1' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\nThe include text<!-- singleLineInclude: fileToInclude.txt -->\n\nLine 1<!-- include: multiLineFileToInclude.txt -->\n\nLine 2<!-- endInclude -->\n\n<!-- include: includeWithCode.txt -->\n```\nThe Code\n```\n<!-- endInclude -->\n"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessorTests.InPlaceOverwriteUrlInclude.verified.txt",
    "content": "﻿The MIT License (MIT)<!-- include: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/license.txt -->\n\nCopyright (c) 2013 Simon Cropp\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.<!-- endInclude -->\n"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessorTests.InPlaceOverwriteUrlSnippet.verified.txt",
    "content": "﻿<!-- snippet: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/license.txt -->\n<a id='snippet-https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/license.txt'></a>\n```txt\nThe MIT License (MIT)\n\nCopyright (c) 2013 Simon Cropp\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n```\n<sup><a href='#snippet-https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/license.txt' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessorTests.InPlaceOverwriteWithFileSnippetMissing.verified.md",
    "content": "﻿<!-- snippet: missing.txt -->\n```\n** Could not find snippet 'missing.txt' **\n```\n<!-- endSnippet -->\n"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessorTests.Mdx.verified.txt",
    "content": "﻿Local\n\n<!-- snippet: sourceFile.txt -->\n<a id='snippet-sourceFile.txt'></a>\n```txt\nFrom Source File\n```\n<sup><a href='/sourceFile.txt#L1-L1' title='Snippet source file'>snippet source</a> | <a href='#snippet-sourceFile.txt' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\nRelative Local\n\n<!-- snippet: ./sourceFile.txt -->\n<a id='snippet-./sourceFile.txt'></a>\n```txt\nFrom Source File\n```\n<sup><a href='#snippet-./sourceFile.txt' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\nRooted\n\n<!-- snippet: /sourceFile.txt -->\n<a id='snippet-/sourceFile.txt'></a>\n```txt\nFrom Source File\n```\n<sup><a href='/sourceFile.txt#L1-L1' title='Snippet source file'>snippet source</a> | <a href='#snippet-/sourceFile.txt' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n\n"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessorTests.MixedCaseInclude.verified.txt",
    "content": "﻿The include text<!-- singleLineInclude: fIletoinClude.txt -->\n"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessorTests.UrlInclude.verified.txt",
    "content": "﻿The MIT License (MIT)<!-- include: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/license.txt -->\n\nCopyright (c) 2013 Simon Cropp\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.<!-- endInclude -->\n"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessorTests.UrlIncludeMissing.verified.txt",
    "content": "﻿{\n  Type: MissingIncludesException,\n  Missing: [\n    {\n      Key: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/missing.txt,\n      LineNumber: 1,\n      File: {CurrentDirectory}DirectoryMarkdownProcessor/UrlIncludeMissing/one.source.md\n    }\n  ],\n  Message: Missing includes: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/missing.txt\n}"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessorTests.UrlSnippet.verified.txt",
    "content": "﻿<!-- snippet: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/license.txt -->\n<a id='snippet-https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/license.txt'></a>\n```txt\nThe MIT License (MIT)\n\nCopyright (c) 2013 Simon Cropp\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n```\n<sup><a href='#snippet-https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/license.txt' title='Start of snippet'>anchor</a></sup>\n<!-- endSnippet -->\n"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessorTests.UrlSnippetMissing.verified.txt",
    "content": "﻿{\n  Type: MissingSnippetsException,\n  Missing: [\n    {\n      Key: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/missing.txt,\n      LineNumber: 1,\n      File: {CurrentDirectory}DirectoryMarkdownProcessor/UrlSnippetMissing/one.source.md\n    }\n  ],\n  Message:\nMissing snippets:\n  {CurrentDirectory}DirectoryMarkdownProcessor/UrlSnippetMissing/one.source.md: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/missing.txt\n}"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessorTests.ValidationErrors.verified.txt",
    "content": "﻿{\n  Type: ContentValidationException,\n  Errors: [\n    {\n      Error: Invalid word detected: 'you',\n      Line: 1,\n      File: {CurrentDirectory}DirectoryMarkdownProcessor/ValidationErrors/one.source.md\n    },\n    {\n      Error: Invalid word detected: 'we',\n      Line: 2,\n      Column: 1,\n      File: {CurrentDirectory}DirectoryMarkdownProcessor/ValidationErrors/one.source.md\n    },\n    {\n      Error: No exclamation marks. If a statement is important make it bold. https://www.technicalcommunicationcenter.com/2011/12/30/the-discipline-of-punctuation-in-technical-writing/. ,\n      Line: 3,\n      Column: 2,\n      File: {CurrentDirectory}DirectoryMarkdownProcessor/ValidationErrors/one.source.md\n    }\n  ],\n  Message:\nContent validation errors:\nInvalid word detected: 'you'\n  File: {CurrentDirectory}DirectoryMarkdownProcessor/ValidationErrors/one.source.md\n  Line: 1\n  Column: 0\nInvalid word detected: 'we'\n  File: {CurrentDirectory}DirectoryMarkdownProcessor/ValidationErrors/one.source.md\n  Line: 2\n  Column: 1\nNo exclamation marks. If a statement is important make it bold. https://www.technicalcommunicationcenter.com/2011/12/30/the-discipline-of-punctuation-in-technical-writing/. \n  File: {CurrentDirectory}DirectoryMarkdownProcessor/ValidationErrors/one.source.md\n  Line: 3\n  Column: 2\n\n}"
  },
  {
    "path": "src/Tests/DirectoryMarkdownProcessorTests.cs",
    "content": "public class DirectoryMarkdownProcessorTests\n{\n    [Fact]\n    public void Run()\n    {\n        var root = GitRepoDirectoryFinder.FindForFilePath();\n\n        var processor = new DirectoryMarkdownProcessor(\n            targetDirectory: root,\n            directoryIncludes: path => !path.Contains(\"IncludeFileFinder\") &&\n                                       !path.Contains(\"DirectoryMarkdownProcessor\") &&\n                                       !DefaultDirectoryExclusions.ShouldExcludeDirectory(path),\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true,\n            tocLevel: 1,\n            tocExcludes: new List<string> { \"Icon\", \"Credits\", \"Release Notes\" });\n        processor.Run();\n    }\n\n    [Fact]\n    public Task InPlaceOverwriteExists()\n    {\n        var root = Path.GetFullPath(\"DirectoryMarkdownProcessor/InPlaceOverwriteExists\");\n        var processor = new DirectoryMarkdownProcessor(\n            root,\n            convention: DocumentConvention.InPlaceOverwrite,\n            writeHeader: false,\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true);\n        processor.AddSnippets(SnippetBuild(\"snippet1\", \"thePath\"));\n        processor.Run();\n\n        var fileInfo = new FileInfo(Path.Combine(root, \"file.md\"));\n        return VerifyFile(fileInfo);\n    }\n\n    [Fact]\n    public Task InPlaceOverwriteExistsMdx()\n    {\n        var root = Path.GetFullPath(\"DirectoryMarkdownProcessor/InPlaceOverwriteExistsMdx\");\n        var processor = new DirectoryMarkdownProcessor(\n            root,\n            convention: DocumentConvention.InPlaceOverwrite,\n            writeHeader: false,\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true);\n        processor.AddSnippets(SnippetBuild(\"snippet1\", \"thePath\"));\n        processor.Run();\n\n        var fileInfo = new FileInfo(Path.Combine(root, \"file.mdx\"));\n        return VerifyFile(fileInfo);\n    }\n\n    [Fact]\n    public Task InPlaceOverwriteNotExists()\n    {\n        var root = Path.GetFullPath(\"DirectoryMarkdownProcessor/InPlaceOverwriteNotExists\");\n        var processor = new DirectoryMarkdownProcessor(\n            root,\n            convention: DocumentConvention.InPlaceOverwrite,\n            writeHeader: false,\n            readOnly: false,\n            newLine: \"\\r\",\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true);\n        processor.AddSnippets(SnippetBuild(\"snippet1\", \"thePath\"));\n        processor.Run();\n\n        var fileInfo = new FileInfo(Path.Combine(root, \"file.md\"));\n        return VerifyFile(fileInfo);\n    }\n\n    [Fact]\n    public Task InPlaceOverwriteUrlSnippet()\n    {\n        var root = Path.GetFullPath(\"DirectoryMarkdownProcessor/InPlaceOverwriteUrlSnippet\");\n        var processor = new DirectoryMarkdownProcessor(\n            root,\n            convention: DocumentConvention.InPlaceOverwrite,\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true);\n        processor.Run();\n\n        var result = Path.Combine(root, \"one.md\");\n\n        return Verify(File.ReadAllText(result));\n    }\n\n    [Fact]\n    public Task InPlaceOverwriteUrlInclude()\n    {\n        var root = Path.GetFullPath(\"DirectoryMarkdownProcessor/InPlaceOverwriteUrlInclude\");\n        var processor = new DirectoryMarkdownProcessor(\n            root,\n            convention: DocumentConvention.InPlaceOverwrite,\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true);\n        processor.Run();\n\n        var result = Path.Combine(root, \"one.md\");\n\n        return Verify(File.ReadAllText(result));\n    }\n\n    [Fact]\n    public Task InPlaceOverwriteWithFileSnippetMissing()\n    {\n        var root = Path.GetFullPath(\"DirectoryMarkdownProcessor/InPlaceOverwriteWithFileSnippetMissing\");\n        var processor = new DirectoryMarkdownProcessor(\n            root,\n            convention: DocumentConvention.InPlaceOverwrite,\n            writeHeader: false,\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true,\n            treatMissingAsWarning: true);\n\n        processor.Run();\n\n        var fileInfo = new FileInfo(Path.Combine(root, \"file.md\"));\n        return VerifyFile(fileInfo);\n    }\n\n    [Fact]\n    public void ReadOnly()\n    {\n        var root = Path.GetFullPath(\"DirectoryMarkdownProcessor/Readonly\");\n        try\n        {\n            var processor = new DirectoryMarkdownProcessor(root,\n                writeHeader: false,\n                readOnly: true,\n                newLine: \"\\r\",\n                directoryIncludes: _ => true,\n                markdownDirectoryIncludes: _ => true,\n                snippetDirectoryIncludes: _ => true);\n            processor.AddSnippets(\n                SnippetBuild(\"snippet1\"),\n                SnippetBuild(\"snippet2\")\n            );\n            processor.Run();\n\n            var fileInfo = new FileInfo(Path.Combine(root, \"one.md\"));\n            Assert.True(fileInfo.IsReadOnly);\n        }\n        finally\n        {\n            foreach (var file in Directory.EnumerateFiles(root))\n            {\n                FileEx.ClearReadOnly(file);\n            }\n        }\n    }\n\n    [Fact]\n    public Task FileSnippetMissing()\n    {\n        var root = Path.GetFullPath(\"DirectoryMarkdownProcessor/FileSnippetMissing\");\n        var processor = new DirectoryMarkdownProcessor(\n            root,\n            writeHeader: false,\n            newLine: \"\\n\",\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true);\n        return Throws(() => processor.Run());\n    }\n\n    [Fact]\n    public Task UrlSnippetMissing()\n    {\n        var root = Path.GetFullPath(\"DirectoryMarkdownProcessor/UrlSnippetMissing\");\n        var processor = new DirectoryMarkdownProcessor(\n            root,\n            writeHeader: false,\n            newLine: \"\\n\",\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true);\n        return Throws(() => processor.Run());\n    }\n\n    [Fact]\n    public Task ValidationErrors()\n    {\n        var root = Path.GetFullPath(\"DirectoryMarkdownProcessor/ValidationErrors\");\n        var processor = new DirectoryMarkdownProcessor(\n            root,\n            writeHeader: false,\n            validateContent: true,\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true);\n        return Throws(() => processor.Run());\n    }\n\n    [Fact]\n    public Task UrlIncludeMissing()\n    {\n        var root = Path.GetFullPath(\"DirectoryMarkdownProcessor/UrlIncludeMissing\");\n        var processor = new DirectoryMarkdownProcessor(\n            root,\n            writeHeader: false,\n            newLine: \"\\n\",\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true);\n        return Throws(() => processor.Run());\n    }\n\n    [Fact]\n    public Task UrlSnippet()\n    {\n        var root = Path.GetFullPath(\"DirectoryMarkdownProcessor/UrlSnippet\");\n        var processor = new DirectoryMarkdownProcessor(\n            root,\n            writeHeader: false,\n            newLine: \"\\r\",\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true);\n        processor.Run();\n\n        var result = Path.Combine(root, \"one.md\");\n\n        return Verify(File.ReadAllText(result));\n    }\n\n    [Fact]\n    public Task BinaryFileSnippet()\n    {\n        var root = Path.GetFullPath(\"DirectoryMarkdownProcessor/BinaryFileSnippet\");\n        var processor = new DirectoryMarkdownProcessor(\n            root,\n            writeHeader: false,\n            newLine: \"\\r\",\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true);\n        processor.Run();\n\n        var result = Path.Combine(root, \"one.md\");\n\n        return Verify(File.ReadAllText(result));\n    }\n\n    [Fact]\n    public Task FileSnippetWithWhiteSpace()\n    {\n        var root = Path.GetFullPath(\"DirectoryMarkdownProcessor/FileSnippetWithWhiteSpace\");\n        var processor = new DirectoryMarkdownProcessor(\n            root,\n            writeHeader: false,\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true);\n        processor.Run();\n\n        var result = Path.Combine(root, \"one.md\");\n\n        return Verify(File.ReadAllText(result));\n    }\n\n    [Fact]\n    public Task Mdx()\n    {\n        var root = Path.GetFullPath(\"DirectoryMarkdownProcessor/Mdx\");\n        var processor = new DirectoryMarkdownProcessor(\n            root,\n            writeHeader: false,\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true);\n        processor.Run();\n\n        var result = Path.Combine(root, \"one.mdx\");\n\n        return Verify(File.ReadAllText(result));\n    }\n\n    [Fact]\n    public Task FileSnippet()\n    {\n        var root = Path.GetFullPath(\"DirectoryMarkdownProcessor/FileSnippet\");\n        var processor = new DirectoryMarkdownProcessor(\n            root,\n            writeHeader: false,\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true);\n        processor.Run();\n\n        var result = Path.Combine(root, \"one.md\");\n\n        return Verify(File.ReadAllText(result));\n    }\n\n    [Fact]\n    public Task FileSnippetExplicitIncludeBypassesExcludeSnippetFiles()\n    {\n        // `ExcludeSnippetFiles` should stop MarkdownSnippets from scanning the file for\n        // `begin-snippet`/`end-snippet` markers, but an explicit `snippet: sourceFile.txt`\n        // in a markdown file must still resolve to the whole-file contents — that lookup\n        // goes through allFiles, which remains unfiltered.\n        var root = Path.GetFullPath(\"DirectoryMarkdownProcessor/FileSnippet\");\n        var processor = new DirectoryMarkdownProcessor(\n            root,\n            writeHeader: false,\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true,\n            snippetFileIncludes: path => Path.GetFileName(path) != \"sourceFile.txt\");\n        processor.Run();\n\n        var result = Path.Combine(root, \"one.md\");\n\n        return Verify(File.ReadAllText(result));\n    }\n\n    [Fact]\n    public Task FileSnippetWithHash()\n    {\n        var root = Path.GetFullPath(\"DirectoryMarkdownProcessor/FileSnippetWithHash\");\n        var processor = new DirectoryMarkdownProcessor(\n            root,\n            writeHeader: false,\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true);\n        processor.Run();\n\n        var result = Path.Combine(root, \"one.md\");\n\n        return Verify(File.ReadAllText(result));\n    }\n\n    [Fact]\n    public Task MixedCaseInclude()\n    {\n        var root = Path.GetFullPath(\"DirectoryMarkdownProcessor/MixedCaseInclude\");\n        var processor = new DirectoryMarkdownProcessor(\n            root,\n            writeHeader: false,\n            newLine: \"\\r\",\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true);\n        processor.Run();\n\n        var result = Path.Combine(root, \"one.md\");\n\n        return Verify(File.ReadAllText(result));\n    }\n\n    [Fact]\n    public Task ExplicitFileInclude()\n    {\n        var root = Path.GetFullPath(\"DirectoryMarkdownProcessor/ExplicitFileInclude\");\n        var processor = new DirectoryMarkdownProcessor(\n            root,\n            writeHeader: false,\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true);\n        processor.Run();\n\n        var result = Path.Combine(root, \"one.md\");\n\n        return Verify(File.ReadAllText(result));\n    }\n\n    [Fact]\n    public Task ExplicitFileIncludeWithMergedSnippet()\n    {\n        var root = Path.GetFullPath(\"DirectoryMarkdownProcessor/ExplicitFileIncludeWithMergedSnippet\");\n        var processor = new DirectoryMarkdownProcessor(\n            root,\n            writeHeader: false,\n            newLine: \"\\r\",\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true);\n        processor.AddSnippets(SnippetBuild(\"snippet1\"));\n        processor.Run();\n\n        var result = Path.Combine(root, \"one.md\");\n\n        return Verify(File.ReadAllText(result));\n    }\n\n    [Fact]\n    public Task ExplicitFileIncludeWithSnippetAtEnd()\n    {\n        var root = Path.GetFullPath(\"DirectoryMarkdownProcessor/ExplicitFileIncludeWithSnippetAtEnd\");\n        var processor = new DirectoryMarkdownProcessor(\n            root,\n            writeHeader: false,\n            newLine: \"\\r\",\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true);\n        processor.AddSnippets(SnippetBuild(\"snippet1\"));\n        processor.Run();\n\n        var result = Path.Combine(root, \"one.md\");\n\n        return Verify(File.ReadAllText(result));\n    }\n\n    [Fact]\n    public Task UrlInclude()\n    {\n        var root = Path.GetFullPath(\"DirectoryMarkdownProcessor/UrlInclude\");\n        var processor = new DirectoryMarkdownProcessor(root,\n            writeHeader: false,\n            newLine: \"\\r\",\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true);\n        processor.Run();\n\n        var result = Path.Combine(root, \"one.md\");\n\n        return Verify(File.ReadAllText(result));\n    }\n\n    [Fact]\n    public Task Convention()\n    {\n        var root = Path.GetFullPath(\"DirectoryMarkdownProcessor/Convention\");\n        var processor = new DirectoryMarkdownProcessor(\n            root,\n            writeHeader: false,\n            newLine: \"\\r\",\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true);\n        processor.AddSnippets(\n            SnippetBuild(\"snippet1\"),\n            SnippetBuild(\"snippet2\")\n        );\n        processor.Run();\n\n        var builder = new StringBuilder();\n        foreach (var file in Directory.EnumerateFiles(root, \"*.*\", SearchOption.AllDirectories).OrderBy(_ => _))\n        {\n            builder.AppendLineN(file.Replace(root, \"\"));\n            builder.AppendLineN(File.ReadAllText(file));\n            builder.AppendLineN();\n        }\n\n        return Verify(builder.ToString());\n    }\n    [Fact]\n    public Task ConventionWithNestedDir()\n    {\n        var root = Path.GetFullPath(\"DirectoryMarkdownProcessor/ConventionWithNestedDir\");\n        var processor = new DirectoryMarkdownProcessor(\n            root,\n            writeHeader: false,\n            newLine: \"\\r\",\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true);\n        processor.AddSnippets(SnippetBuild(\"snippet1\"));\n        processor.Run();\n\n        var builder = new StringBuilder();\n        foreach (var file in Directory.EnumerateFiles(root, \"*.*\", SearchOption.AllDirectories).OrderBy(_ => _))\n        {\n            builder.AppendLineN(file.Replace(root, \"\"));\n            builder.AppendLineN(File.ReadAllText(file));\n            builder.AppendLineN();\n        }\n\n        return Verify(builder.ToString());\n    }\n\n    [Fact]\n    public void MustErrorByDefaultWhenIncludesAreMissing()\n    {\n        var root = Path.GetFullPath(\"DirectoryMarkdownProcessor/MissingInclude\");\n        var processor = new DirectoryMarkdownProcessor(\n            root,\n            writeHeader: false,\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true,\n            newLine: \"\\n\");\n        Assert.Throws<MissingIncludesException>(() => processor.Run());\n    }\n\n    [Fact]\n    public void MustNotErrorForMissingIncludesIfConfigured()\n    {\n        var root = Path.GetFullPath(\"DirectoryMarkdownProcessor/MissingInclude\");\n        var processor = new DirectoryMarkdownProcessor(\n            root,\n            writeHeader: false,\n            treatMissingAsWarning: true,\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true,\n            newLine: \"\\n\");\n        processor.Run();\n    }\n\n    [Fact]\n    public void MustErrorByDefaultWhenSnippetsAreMissing()\n    {\n        var root = Path.GetFullPath(\"DirectoryMarkdownProcessor/Convention\");\n        var processor = new DirectoryMarkdownProcessor(\n            root,\n            writeHeader: false,\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true,\n            newLine: \"\\n\");\n        Assert.Throws<MissingSnippetsException>(() => processor.Run());\n    }\n\n    [Fact]\n    public void MustNotErrorForMissingSnippetsIfConfigured()\n    {\n        var root = Path.GetFullPath(\"DirectoryMarkdownProcessor/Convention\");\n        var processor = new DirectoryMarkdownProcessor(\n            root,\n            writeHeader: false,\n            treatMissingAsWarning: true,\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true,\n            newLine: \"\\n\");\n        processor.Run();\n    }\n\n    static Snippet SnippetBuild(string key, string? path = null) =>\n        Snippet.Build(\n            language: \"cs\",\n            startLine: 1,\n            endLine: 2,\n            value: \"the code from \" + key,\n            key: key,\n            path: path,\n            expressiveCode: null);\n}\n"
  },
  {
    "path": "src/Tests/DirectorySnippetExtractor/Case/code1.txt",
    "content": "﻿begin-snippet: snipPet\nSome code\nend-snippet"
  },
  {
    "path": "src/Tests/DirectorySnippetExtractor/Case/code2.txt",
    "content": "﻿begin-snippet: Snippet\nSome code\nend-snippet"
  },
  {
    "path": "src/Tests/DirectorySnippetExtractor/Nested/nested/nested/code.txt",
    "content": "﻿begin-snippet: nestedsnippet\nSome code\nend-snippet"
  },
  {
    "path": "src/Tests/DirectorySnippetExtractor/Simple/code1.txt",
    "content": "﻿begin-snippet: snippet2\nSome code\nend-snippet"
  },
  {
    "path": "src/Tests/DirectorySnippetExtractor/Simple/code2.txt",
    "content": "﻿begin-snippet: snippet1\nSome code\nend-snippet"
  },
  {
    "path": "src/Tests/DirectorySnippetExtractor/Simple/code3.txt",
    "content": "﻿begin-snippet: snippet2\nSome code\nend-snippet"
  },
  {
    "path": "src/Tests/DirectorySnippetExtractor/Simple/code4.txt",
    "content": "﻿begin-snippet: snippet1\nSome code\nend-snippet"
  },
  {
    "path": "src/Tests/DirectorySnippetExtractor/VerifyLambdasAreCalled/subpath/code4.txt",
    "content": "﻿begin-snippet: snippet1\nSome code\nend-snippet"
  },
  {
    "path": "src/Tests/DownloaderTests.Valid.verified.txt",
    "content": "﻿{\n  success: true,\n  content:\n# Auto detect text files and normalize line endings to LF\n* text=auto eol=lf\n*.png binary\n*.snk binary\n\n*.verified.txt text eol=lf working-tree-encoding=UTF-8\n*.verified.xml text eol=lf working-tree-encoding=UTF-8\n*.verified.json text eol=lf working-tree-encoding=UTF-8\n\n.editorconfig text eol=lf working-tree-encoding=UTF-8\n*.sln.DotSettings text eol=lf working-tree-encoding=UTF-8\n*.slnx.DotSettings text eol=lf working-tree-encoding=UTF-8\n}"
  },
  {
    "path": "src/Tests/DownloaderTests.cs",
    "content": "public class DownloaderTests\n{\n    [Fact]\n    public async Task Valid()\n    {\n        var content = await Downloader.DownloadContent(\"https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/.gitattributes\");\n        await Verify(new {content.success, content.content});\n    }\n\n    [Fact]\n    public async Task Missing()\n    {\n        var content = await Downloader.DownloadContent(\"https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/missing.txt\");\n        Assert.False(content.success);\n        Assert.Null(content.content);\n    }\n}"
  },
  {
    "path": "src/Tests/FileExTests.cs",
    "content": "public class FileExTests\n{\n    [Fact]\n    public void MakeReadOnly_SetsReadOnlyAttribute()\n    {\n        var tempFile = Path.GetTempFileName();\n        try\n        {\n            FileEx.MakeReadOnly(tempFile);\n\n            var attributes = File.GetAttributes(tempFile);\n            Assert.True((attributes & FileAttributes.ReadOnly) != 0);\n        }\n        finally\n        {\n            File.SetAttributes(tempFile, FileAttributes.Normal);\n            File.Delete(tempFile);\n        }\n    }\n\n    [Fact]\n    public void ClearReadOnly_RemovesReadOnlyAttribute()\n    {\n        var tempFile = Path.GetTempFileName();\n        try\n        {\n            File.SetAttributes(tempFile, File.GetAttributes(tempFile) | FileAttributes.ReadOnly);\n\n            FileEx.ClearReadOnly(tempFile);\n\n            var attributes = File.GetAttributes(tempFile);\n            Assert.False((attributes & FileAttributes.ReadOnly) != 0);\n        }\n        finally\n        {\n            File.SetAttributes(tempFile, FileAttributes.Normal);\n            File.Delete(tempFile);\n        }\n    }\n\n    [Fact]\n    public void ClearReadOnly_DoesNothingIfFileDoesNotExist()\n    {\n        var nonExistentFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());\n\n        FileEx.ClearReadOnly(nonExistentFile);\n\n        Assert.False(File.Exists(nonExistentFile));\n    }\n\n    [Fact]\n    public void MakeReadOnly_ThenClearReadOnly_RoundTrip()\n    {\n        var tempFile = Path.GetTempFileName();\n        try\n        {\n            FileEx.MakeReadOnly(tempFile);\n            Assert.True((File.GetAttributes(tempFile) & FileAttributes.ReadOnly) != 0);\n\n            FileEx.ClearReadOnly(tempFile);\n            Assert.False((File.GetAttributes(tempFile) & FileAttributes.ReadOnly) != 0);\n        }\n        finally\n        {\n            File.SetAttributes(tempFile, FileAttributes.Normal);\n            File.Delete(tempFile);\n        }\n    }\n\n    [Fact]\n    public void FixFileCapitalization_ReturnsActualCasing()\n    {\n        using var tempDir = new TempDirectory();\n        var actualPath = Path.Combine(tempDir, \"TestFile.txt\");\n        File.WriteAllText(actualPath, \"test\");\n\n        var inputPath = Path.Combine(tempDir, \"testfile.txt\");\n\n        var result = FileEx.FixFileCapitalization(inputPath);\n\n        Assert.Equal(actualPath, result);\n    }\n\n    [Fact]\n    public void FixFileCapitalization_WorksWhenCasingMatches()\n    {\n        using var tempFile = TempFile.Create();\n        var result = FileEx.FixFileCapitalization(tempFile);\n\n        Assert.Equal(tempFile, result);\n    }\n}\n"
  },
  {
    "path": "src/Tests/FileToUseAsSnippet.txt",
    "content": "The\nContent\n#region aaa\n  #region indented\nFrom\nFile"
  },
  {
    "path": "src/Tests/GirRepoDirectoryFinderTests.cs",
    "content": "﻿public class GirRepoDirectoryFinderTests\n{\n    [Fact]\n    public void CanFindGirRepoDir()\n    {\n        var path = GitRepoDirectoryFinder.FindForFilePath();\n        Assert.True(Directory.Exists(path));\n    }\n}"
  },
  {
    "path": "src/Tests/GitDirs/NoRef/HEAD",
    "content": "061e8aee1b5174df8ed7b201e0d8dd5a0d155418\n"
  },
  {
    "path": "src/Tests/GitDirs/WithRef/HEAD",
    "content": "ref: refs/heads/master\n"
  },
  {
    "path": "src/Tests/GitDirs/WithRef/refs/heads/master",
    "content": "ad4901fc6e8bf4a253ee053327eba32fe31010ed\n"
  },
  {
    "path": "src/Tests/GlobalUsings.cs",
    "content": "﻿global using Argon;\nglobal using MarkdownSnippets;\nglobal using Polyfills;\nglobal using VerifyTests.DiffPlex;"
  },
  {
    "path": "src/Tests/HeaderWriterTests.DefaultHeader.verified.txt",
    "content": "﻿GENERATED FILE - DO NOT EDIT\nThis file was generated by [MarkdownSnippets](https://github.com/SimonCropp/MarkdownSnippets).\nSource File: {relativePath}\nTo change this file edit the source file and then run MarkdownSnippets."
  },
  {
    "path": "src/Tests/HeaderWriterTests.WriteHeaderDefaultHeader.verified.txt",
    "content": "﻿<!--\nGENERATED FILE - DO NOT EDIT\nThis file was generated by [MarkdownSnippets](https://github.com/SimonCropp/MarkdownSnippets).\nSource File: thePath\nTo change this file edit the source file and then run MarkdownSnippets.\n-->\n"
  },
  {
    "path": "src/Tests/HeaderWriterTests.WriteHeaderHeaderCustom.verified.txt",
    "content": "﻿<!--\nline1\nline2\n-->\n"
  },
  {
    "path": "src/Tests/HeaderWriterTests.cs",
    "content": "﻿public class HeaderWriterTests\n{\n    [Fact]\n    public Task DefaultHeader() =>\n        Verify(HeaderWriter.DefaultHeader);\n\n    [Fact]\n    public Task WriteHeaderDefaultHeader() =>\n        Verify(HeaderWriter.WriteHeader(\"thePath\", null, \"\\r\\n\"));\n\n    [Fact]\n    public Task WriteHeaderHeaderCustom() =>\n        Verify(HeaderWriter.WriteHeader(\"thePath\", @\"line1\\nline2\", \"\\r\\n\"));\n}"
  },
  {
    "path": "src/Tests/IncludeFileFinder/Nested/nested/nested/file.include.md",
    "content": "﻿Nested"
  },
  {
    "path": "src/Tests/IncludeFileFinder/Nested/nested/nested/other.txt",
    "content": "﻿begin-snippet: nestedsnippet\nSome code\nend-snippet"
  },
  {
    "path": "src/Tests/IncludeFileFinder/Simple/file1.include.md",
    "content": "﻿Simple1"
  },
  {
    "path": "src/Tests/IncludeFileFinder/Simple/file2.include.md",
    "content": "﻿begin-snippet: nestedsnippet\nSome code\nend-snippet"
  },
  {
    "path": "src/Tests/IncludeFileFinder/Simple/other.txt",
    "content": "﻿Simple2"
  },
  {
    "path": "src/Tests/IncludeFileFinder/VerifyLambdasAreCalled/subpath/file.include.md",
    "content": "﻿VerifyLambdasAreCalled"
  },
  {
    "path": "src/Tests/IncludeFinder/file.include.md",
    "content": "﻿Simple1"
  },
  {
    "path": "src/Tests/IndexReaderTests.cs",
    "content": "﻿public class IndexReaderTests\n{\n    [Theory]\n    [InlineData(\"a\\r\", \"\\r\")]\n    [InlineData(\"a\\n\", \"\\n\")]\n    [InlineData(\"a\\r\\n\", \"\\r\\n\")]\n    [InlineData(\"\", null)]\n    [InlineData(\"a\", null)]\n    [InlineData(\"a\\rb\", \"\\r\")]\n    [InlineData(\"a\\nb\", \"\\n\")]\n    [InlineData(\"a\\r\\nb\", \"\\r\\n\")]\n    [InlineData(\"a\\r\\r\", \"\\r\")]\n    [InlineData(\"a\\r\\r\\nb\", \"\\r\")]\n    public void NewLineDetection(string input, string? expected)\n    {\n        var fileName = Path.GetTempFileName();\n        try\n        {\n            File.WriteAllText(fileName, input);\n            using var streamReader = File.OpenText(fileName);\n            streamReader.TryFindNewline(out var newline);\n            Assert.Equal(expected, newline);\n        }\n        finally\n        {\n            File.Delete(fileName);\n        }\n    }\n}"
  },
  {
    "path": "src/Tests/LoopState/LoopStateTests.ExcludeEmptyPaddingLines.verified.txt",
    "content": "﻿Line2"
  },
  {
    "path": "src/Tests/LoopState/LoopStateTests.TrimIndentation.verified.txt",
    "content": "﻿Line1\n Line2\nLine2"
  },
  {
    "path": "src/Tests/LoopState/LoopStateTests.TrimIndentation_no_initial_padding.verified.txt",
    "content": "﻿Line1\n    Line2\n   Line2"
  },
  {
    "path": "src/Tests/LoopState/LoopStateTests.TrimIndentation_with_mis_match.verified.txt",
    "content": "﻿Line2\n\nLine4"
  },
  {
    "path": "src/Tests/LoopState/LoopStateTests.cs",
    "content": "public class LoopStateTests\n{\n    [Fact]\n    public Task TrimIndentation()\n    {\n        var loopState = new LoopState(\"key\", _ => throw new(), 1, int.MaxValue, \"\\n\");\n        loopState.AppendLine(\"   Line1\");\n        loopState.AppendLine(\"    Line2\");\n        loopState.AppendLine(\"   Line2\");\n        return Verify(loopState.GetLines());\n    }\n\n    [Fact]\n    public Task ExcludeEmptyPaddingLines()\n    {\n        var loopState = new LoopState(\"key\", _ => throw new(), 1, int.MaxValue, \"\\n\");\n        loopState.AppendLine(\"   \");\n        loopState.AppendLine(\"    Line2\");\n        loopState.AppendLine(\"   \");\n        return Verify(loopState.GetLines());\n    }\n\n    [Fact]\n    public Task TrimIndentation_with_mis_match()\n    {\n        var loopState = new LoopState(\"key\", _ => throw new(), 1, int.MaxValue, \"\\n\");\n        loopState.AppendLine(\"      Line2\");\n        loopState.AppendLine(\"   \");\n        loopState.AppendLine(\"     Line4\");\n        return Verify(loopState.GetLines());\n    }\n\n    [Fact]\n    public void ExcludeEmptyPaddingLines_empty_list()\n    {\n        var loopState = new LoopState(\"key\", _ => throw new(), 1, int.MaxValue, \"\\n\");\n        Assert.Empty(loopState.GetLines());\n    }\n\n    [Fact]\n    public void ExcludeEmptyPaddingLines_whitespace_list()\n    {\n        var loopState = new LoopState(\"key\", _ => throw new(), 1, int.MaxValue, \"\\n\");\n        loopState.AppendLine(\"\");\n        loopState.AppendLine(\"  \");\n        Assert.Empty(loopState.GetLines());\n    }\n\n    [Fact]\n    public Task TrimIndentation_no_initial_padding()\n    {\n        var loopState = new LoopState(\"key\", _ => throw new(), 1, int.MaxValue, \"\\n\");\n        loopState.AppendLine(\"Line1\");\n        loopState.AppendLine(\"    Line2\");\n        loopState.AppendLine(\"   Line2\");\n        return Verify(loopState.GetLines());\n    }\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.Empty_snippet_key.verified.txt",
    "content": "﻿{\n  Type: SnippetException,\n  Message: Could not parse snippet from: snippet:. Path: . Line: 2\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.MissingInclude.verified.txt",
    "content": "﻿{\n  result:\nbefore\n\n** Could not find include 'theKey' ** <!-- singleLineInclude: theKey -->\n\nafter\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.Missing_endInclude.verified.txt",
    "content": "﻿{\n  Type: MarkdownProcessingException,\n  LineNumber: 2,\n  Message: Expected to find `<!-- endInclude -->`. File: . LineNumber: 2.\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.Missing_endToc.verified.txt",
    "content": "﻿{\n  Type: MarkdownProcessingException,\n  File: sourceFile,\n  LineNumber: 2,\n  Message: Expected to find `<!-- endToc -->`. File: sourceFile. LineNumber: 2.\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.MixedNewlinesInFile.verified.txt",
    "content": "﻿{\n  UsedSnippets: [\n    {\n      Key: FileWithMixedNewLines.txt,\n      Language: txt,\n      Value:\na\nb\nc\nd,\n      Error: ,\n      FileLocation: null,\n      IsInError: false\n    }\n  ],\n  result:\nsome other text\n\n<!-- snippet: FileWithMixedNewLines.txt -->\n```txt\na\nb\nc\nd\n```\n<!-- endSnippet -->\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.Simple.verified.txt",
    "content": "﻿{\n  UsedSnippets: [\n    {\n      Key: snippet1,\n      Language: cs,\n      Value: Snippet,\n      Error: ,\n      FileLocation: thePath(1-2),\n      IsInError: false\n    },\n    {\n      Key: snippet2,\n      Language: cs,\n      Value: Snippet,\n      Error: ,\n      FileLocation: thePath(1-2),\n      IsInError: false\n    },\n    {\n      Key: FileToUseAsSnippet.txt,\n      Language: txt,\n      Value:\nThe\nContent\nFrom\nFile,\n      Error: ,\n      FileLocation: {ProjectDirectory}FileToUseAsSnippet.txt(1-4),\n      IsInError: false\n    },\n    {\n      Key: /FileToUseAsSnippet.txt,\n      Language: txt,\n      Value:\nThe\nContent\nFrom\nFile,\n      Error: ,\n      FileLocation: {ProjectDirectory}FileToUseAsSnippet.txt(1-4),\n      IsInError: false\n    }\n  ],\n  result:\n<!-- snippet: snippet1 -->\n```cs\nSnippet\n```\n<!-- endSnippet -->\n\nsome text\n\n<!-- snippet: snippet2 -->\n```cs\nSnippet\n```\n<!-- endSnippet -->\n\nsome other text\n\n<!-- snippet: FileToUseAsSnippet.txt -->\n```txt\nThe\nContent\nFrom\nFile\n```\n<!-- endSnippet -->\n\nsome other text\n\n<!-- snippet: /FileToUseAsSnippet.txt -->\n```txt\nThe\nContent\nFrom\nFile\n```\n<!-- endSnippet -->\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.Simple_Overwrite.verified.txt",
    "content": "﻿{\n  UsedSnippets: [\n    {\n      Key: snippet1,\n      Language: cs,\n      Value: Snippet,\n      Error: ,\n      FileLocation: thePath(1-2),\n      IsInError: false\n    },\n    {\n      Key: snippet2,\n      Language: cs,\n      Value: Snippet,\n      Error: ,\n      FileLocation: thePath(1-2),\n      IsInError: false\n    },\n    {\n      Key: FileToUseAsSnippet.txt,\n      Language: txt,\n      Value:\nThe\nContent\nFrom\nFile,\n      Error: ,\n      FileLocation: {ProjectDirectory}FileToUseAsSnippet.txt(1-4),\n      IsInError: false\n    },\n    {\n      Key: /FileToUseAsSnippet.txt,\n      Language: txt,\n      Value:\nThe\nContent\nFrom\nFile,\n      Error: ,\n      FileLocation: {ProjectDirectory}FileToUseAsSnippet.txt(1-4),\n      IsInError: false\n    }\n  ],\n  result:\n<!-- snippet: snippet1 -->\n```cs\nSnippet\n```\n<!-- endSnippet -->\n\nsome text\n\n<!-- snippet: snippet2 -->\n```cs\nSnippet\n```\n<!-- endSnippet -->\n\nsome other text\n\n<!-- snippet: FileToUseAsSnippet.txt -->\n```txt\nThe\nContent\nFrom\nFile\n```\n<!-- endSnippet -->\n\nsome other text\n\n<!-- snippet: /FileToUseAsSnippet.txt -->\n```txt\nThe\nContent\nFrom\nFile\n```\n<!-- endSnippet -->\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.SkipHeadingBeforeToc.verified.txt",
    "content": "﻿{\n  result:\n## Heading 1\n\n<!-- toc -->\n## Contents\n\n  * [Heading 2](#heading-2)<!-- endToc -->\n\nText1\n\n## Heading 2\n\nText2\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.SnippetInInclude.verified.txt",
    "content": "﻿{\n  UsedSnippets: [\n    {\n      Key: snippet1,\n      Language: cs,\n      Value: Snippet,\n      Error: ,\n      FileLocation: thePath(1-2),\n      IsInError: false\n    }\n  ],\n  result:\nsome text\n\n<!-- include: theKey. path: thePath -->\n<!-- snippet: snippet1 -->\n```cs\nSnippet\n```\n<!-- endSnippet -->\n<!-- endInclude -->\n\nsome other text\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.SnippetInIncludeLast.verified.txt",
    "content": "﻿{\n  UsedSnippets: [\n    {\n      Key: snippet1,\n      Language: cs,\n      Value: Snippet,\n      Error: ,\n      FileLocation: thePath(1-2),\n      IsInError: false\n    }\n  ],\n  result:\nsome text\n\nline1<!-- include: theKey. path: thePath -->\n<!-- snippet: snippet1 -->\n```cs\nSnippet\n```\n<!-- endSnippet -->\n<!-- endInclude -->\n\nsome other text\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.TableInInclude.verified.txt",
    "content": "﻿{\n  result:\nsome text\n\n<!-- include: theKey. path: thePath -->\n| Number of Parameters | Variations per Parameter | Total Combinations | Pairwise Combinations |\n| -------------------- | ----------------------- | ------------------ | --------------------- |\n|2|5|25|25|\n<!-- endInclude -->\n\nsome other text\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.Toc.verified.txt",
    "content": "﻿{\n  result:\n# Title\n\n<!-- toc -->\n## Contents\n\n  * [Heading 1](#heading-1)\n  * [Heading 2](#heading-2)<!-- endToc -->\n\n## Heading 1\n\nText1\n\n## Heading 2\n\nText2\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.Toc1.verified.txt",
    "content": "﻿{\n  result:\n# Title\n\ntoc1\n\n## Heading 1\n\nText1\n\n## Heading 2\n\nText2\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.TocRetainedIfNoHeadingsInFile.verified.txt",
    "content": "﻿{\n  result:\n# Title\n\n<!-- toc -->\n<!-- endToc -->\n\nThis document has no headings.\n\nAn empty toc section should be generated, in case\nany headings are added in future.\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.Toc_Overwrite.verified.txt",
    "content": "﻿{\n  result:\n# Title\n\n<!-- toc -->\n## Contents\n\n  * [Heading 1](#heading-1)\n  * [Heading 2](#heading-2)<!-- endToc -->\n\n## Heading 1\n\nText1\n\n## Heading 2\n\nText2\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.Whitespace_snippet_key.verified.txt",
    "content": "﻿{\n  Type: SnippetException,\n  Message: Could not parse snippet from: snippet:. Path: . Line: 2\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.WithCommentWebSnippetUpdate.verified.txt",
    "content": "﻿{\n  UsedSnippets: [\n    {\n      Key: snipPet,\n      Language: txt,\n      Value: Some code,\n      Error: ,\n      FileLocation: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt(1-3),\n      IsInError: false\n    }\n  ],\n  result:\nbefore\n\n<!-- web-snippet: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet -->\n```txt\nSome code\n```\n<!-- endSnippet -->\n\nafter\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.WithCommentWebSnippetWithViewUrl.verified.txt",
    "content": "﻿{\n  UsedSnippets: [\n    {\n      Key: snipPet,\n      Language: txt,\n      Value: Some code,\n      Error: ,\n      FileLocation: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt(1-3),\n      IsInError: false\n    }\n  ],\n  result:\nbefore\n\n<!-- web-snippet: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet https://github.com/SimonCropp/MarkdownSnippets/blob/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt -->\n```txt\nSome code\n```\n<!-- endSnippet -->\n\nafter\n}\n"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.WithDoubleInclude.verified.txt",
    "content": "﻿{\n  result:\nbefore\n\ntheValue1<!-- include: theKey. path: {CurrentDirectory}thePath -->\ntheValue2<!-- endInclude -->\n\nafter\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.WithEmptyMultiLineInclude_Overwrite.verified.txt",
    "content": "﻿{\n  result:\nbefore\n\none<!-- include: theKey. path: {CurrentDirectory}thePath -->\ntwo<!-- endInclude -->\n\nafter\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.WithEmptyMultipleInclude.verified.txt",
    "content": "﻿{\n  result:\nbefore\n\n<!-- include: theKey. path: {CurrentDirectory}thePath -->\n\n<!-- endInclude -->\n\nafter\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.WithIndentedCommentSnippet.verified.txt",
    "content": "﻿{\n  UsedSnippets: [\n    {\n      Key: theKey,\n      Language: cs,\n      Value: Snippet,\n      Error: ,\n      FileLocation: thePath(1-2),\n      IsInError: false\n    }\n  ],\n  result:\nbefore\n\n    <!-- snippet: theKey -->\n    ```cs\n    Snippet\n    ```\n    <!-- endSnippet -->\n\nafter\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.WithIndentedMultiLineSnippet.verified.txt",
    "content": "﻿{\n  UsedSnippets: [\n    {\n      Key: theKey,\n      Language: cs,\n      Value:\nthe\nlong\nSnippet,\n      Error: ,\n      FileLocation: thePath(1-2),\n      IsInError: false\n    }\n  ],\n  result:\nbefore\n\n  <!-- snippet: theKey -->\n  ```cs\n  the\n  long\n  Snippet\n  ```\n  <!-- endSnippet -->\n\nafter\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.WithIndentedSnippet.verified.txt",
    "content": "﻿{\n  UsedSnippets: [\n    {\n      Key: theKey,\n      Language: cs,\n      Value: Snippet,\n      Error: ,\n      FileLocation: thePath(1-2),\n      IsInError: false\n    }\n  ],\n  result:\nbefore\n\n    <!-- snippet: theKey -->\n    ```cs\n    Snippet\n    ```\n    <!-- endSnippet -->\n\nafter\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.WithIndentedSnippetMultipleSpaces.verified.txt",
    "content": "﻿{\n  UsedSnippets: [\n    {\n      Key: theKey,\n      Language: cs,\n      Value: Snippet,\n      Error: ,\n      FileLocation: thePath(1-2),\n      IsInError: false\n    }\n  ],\n  result:\nbefore\n\n        <!-- snippet: theKey -->\n        ```cs\n        Snippet\n        ```\n        <!-- endSnippet -->\n\nafter\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.WithIndentedWebSnippet.verified.txt",
    "content": "﻿{\n  MissingSnippets: [\n    {\n      Key: http://example.com/file.cs#snippet1,\n      LineNumber: 4\n    }\n  ],\n  result:\nbefore\n\n    <!-- web-snippet: http://example.com/file.cs#snippet1 -->\n    ```\n    ** Could not fetch or parse web-snippet 'http://example.com/file.cs#snippet1' **\n    ```\n    <!-- endSnippet -->\n\nafter\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.WithInlineWebSnippetWithViewUrl.verified.txt",
    "content": "﻿{\n  UsedSnippets: [\n    {\n      Key: snipPet,\n      Language: txt,\n      Value: Some code,\n      Error: ,\n      FileLocation: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt(1-3),\n      IsInError: false\n    }\n  ],\n  result:\nbefore\n\n<!-- web-snippet: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet https://github.com/SimonCropp/MarkdownSnippets/blob/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt -->\n```txt\nSome code\n```\n<!-- endSnippet -->\n\nafter\n}\n"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.WithMixedCaseInclude.verified.txt",
    "content": "﻿{\n  result:\nbefore\n\ntheValue1<!-- singleLineInclude: theKey. path: {CurrentDirectory}thePath -->\n\ntheValue2<!-- singleLineInclude: TheKey. path: {CurrentDirectory}thePath -->\n\nafter\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.WithMixedCaseSnippet.verified.txt",
    "content": "﻿{\n  UsedSnippets: [\n    {\n      Key: theKey,\n      Language: cs,\n      Value: Snippet,\n      Error: ,\n      FileLocation: thePath(1-2),\n      IsInError: false\n    },\n    {\n      Key: TheKey,\n      Language: cs,\n      Value: Snippet,\n      Error: ,\n      FileLocation: thePath(1-2),\n      IsInError: false\n    }\n  ],\n  result:\nbefore\n\n<!-- snippet: theKey -->\n```cs\nSnippet\n```\n<!-- endSnippet -->\n\n<!-- snippet: TheKey -->\n```cs\nSnippet\n```\n<!-- endSnippet -->\n\nafter\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.WithMultiLineInclude_Overwrite.verified.txt",
    "content": "﻿{\n  result:\nbefore\n\ntheValue1<!-- include: theKey. path: {CurrentDirectory}thePath -->\ntheValue2<!-- endInclude -->\n\nafter\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.WithMultiLineSnippet.verified.txt",
    "content": "﻿{\n  UsedSnippets: [\n    {\n      Key: theKey,\n      Language: cs,\n      Value:\nthe\nlong\nSnippet,\n      Error: ,\n      FileLocation: thePath(1-2),\n      IsInError: false\n    }\n  ],\n  result:\nbefore\n\n<!-- snippet: theKey -->\n```cs\nthe\nlong\nSnippet\n```\n<!-- endSnippet -->\n\nafter\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.WithMultipleInclude.verified.txt",
    "content": "﻿{\n  result:\nbefore\n\ntheValue1<!-- include: theKey. path: {CurrentDirectory}thePath -->\ntheValue2\ntheValue3<!-- endInclude -->\n\nafter\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.WithSingleInclude.verified.txt",
    "content": "﻿{\n  result:\nbefore\n\ntheValue1<!-- singleLineInclude: theKey. path: {CurrentDirectory}thePath -->\n\nafter\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.WithSingleInclude_Overwrite.verified.txt",
    "content": "﻿{\n  result:\nbefore\n\ntheValue1<!-- singleLineInclude: theKey. path: {CurrentDirectory}thePath -->\n\nafter\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.WithSingleSnippet.verified.txt",
    "content": "﻿{\n  UsedSnippets: [\n    {\n      Key: theKey,\n      Language: cs,\n      Value: Snippet,\n      Error: ,\n      FileLocation: thePath(1-2),\n      IsInError: false\n    }\n  ],\n  result:\nbefore\n\n<!-- snippet: theKey -->\n```cs\nSnippet\n```\n<!-- endSnippet -->\n\nafter\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.WithTabIndentedSnippet.verified.txt",
    "content": "﻿{\n  UsedSnippets: [\n    {\n      Key: theKey,\n      Language: cs,\n      Value: Snippet,\n      Error: ,\n      FileLocation: thePath(1-2),\n      IsInError: false\n    }\n  ],\n  result:\nbefore\n\n\t<!-- snippet: theKey -->\n\t```cs\n\tSnippet\n\t```\n\t<!-- endSnippet -->\n\nafter\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.WithTwoLineSnippet.verified.txt",
    "content": "﻿{\n  UsedSnippets: [\n    {\n      Key: theKey,\n      Language: cs,\n      Value:\nthe\nSnippet,\n      Error: ,\n      FileLocation: thePath(1-2),\n      IsInError: false\n    }\n  ],\n  result:\nbefore\n\n<!-- snippet: theKey -->\n```cs\nthe\nSnippet\n```\n<!-- endSnippet -->\n\nafter\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.WrongNewlineInSnippet.verified.txt",
    "content": "﻿{\n  MissingSnippets: [\n    {\n      Key: 'snippet1',\n      LineNumber: 2\n    }\n  ],\n  content: \"\n<!-- snippet: snippet1 -->\n** Could not find snippet 'snippet1' **\n\nsome text\n\"\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/MarkdownProcessorTests.cs",
    "content": "public class MarkdownProcessorTests\n{\n    [Fact]\n    public Task Missing_endInclude()\n    {\n        var content = \"\"\"\n\n                      BAD<!-- include: theKey. path: /thePath -->\n\n                      \"\"\";\n        return SnippetVerifier.VerifyThrows(\n            DocumentConvention.InPlaceOverwrite,\n            content,\n            includes: [Include.Build(\"theKey\", [], Path.GetFullPath(\"thePath\"))]);\n    }\n\n    [Fact]\n    public Task WithEmptyMultiLineInclude_Overwrite()\n    {\n        var content = \"\"\"\n\n                      before\n\n                      <!-- include: theKey. path: /thePath -->\n\n                      <!-- endInclude -->\n\n                      after\n\n                      \"\"\";\n        var lines = new List<string>\n        {\n            \"one\",\n            \"two\"\n        };\n        return SnippetVerifier.Verify(\n            DocumentConvention.InPlaceOverwrite,\n            content,\n            includes: [Include.Build(\"theKey\", lines, Path.GetFullPath(\"thePath\"))]);\n    }\n\n    [Fact]\n    public Task WithMultiLineInclude_Overwrite()\n    {\n        var content = \"\"\"\n\n                      before\n\n                      BAD<!-- include: theKey. path: /thePath -->\n                      BAD\n                      BAD<!-- endInclude -->\n\n                      after\n\n                      \"\"\";\n        var lines = new List<string>\n        {\n            \"theValue1\",\n            \"theValue2\"\n        };\n        return SnippetVerifier.Verify(\n            DocumentConvention.InPlaceOverwrite,\n            content,\n            includes: [Include.Build(\"theKey\", lines, Path.GetFullPath(\"thePath\"))]);\n    }\n\n    [Fact]\n    public Task WithSingleInclude_Overwrite()\n    {\n        var content = \"\"\"\n\n                      before\n\n                      BAD<!-- singleLineInclude: theKey. path: /thePath -->\n\n                      after\n\n                      \"\"\";\n        var lines = new List<string>\n        {\n            \"theValue1\"\n        };\n        return SnippetVerifier.Verify(\n            DocumentConvention.InPlaceOverwrite,\n            content,\n            includes: [Include.Build(\"theKey\", lines, Path.GetFullPath(\"thePath\"))]);\n    }\n\n    [Fact]\n    public Task WithSingleInclude()\n    {\n        var content = \"\"\"\n\n                      before\n\n                      include: theKey\n\n                      after\n\n                      \"\"\";\n        var lines = new List<string>\n        {\n            \"theValue1\"\n        };\n        return SnippetVerifier.Verify(\n            DocumentConvention.SourceTransform,\n            content,\n            includes: [Include.Build(\"theKey\", lines, Path.GetFullPath(\"thePath\"))]);\n    }\n\n    [Fact]\n    public Task WithMixedCaseInclude()\n    {\n        var content = \"\"\"\n\n                      before\n\n                      include: theKey\n\n                      include: TheKey\n\n                      after\n\n                      \"\"\";\n        return SnippetVerifier.Verify(\n            DocumentConvention.SourceTransform,\n            content,\n            includes:\n            [\n                Include.Build(\"theKey\", [\"theValue1\"], Path.GetFullPath(\"thePath\")),\n                Include.Build(\"TheKey\", [\"theValue2\"], Path.GetFullPath(\"thePath\"))\n            ]);\n    }\n\n    [Fact]\n    public Task WithSingleSnippet()\n    {\n        var content = \"\"\"\n\n                      before\n\n                      snippet: theKey\n\n                      after\n\n                      \"\"\";\n\n        return SnippetVerifier.Verify(\n            DocumentConvention.SourceTransform,\n            content,\n            snippets: [SnippetBuild(\"cs\", \"theKey\")]);\n    }\n    [Fact]\n    public Task WithMixedCaseSnippet()\n    {\n        var content = \"\"\"\n\n                      before\n\n                      snippet: theKey\n\n                      snippet: TheKey\n\n                      after\n\n                      \"\"\";\n\n        return SnippetVerifier.Verify(\n            DocumentConvention.SourceTransform,\n            content,\n            snippets:\n            [\n                SnippetBuild(\"cs\", \"theKey\"),\n                SnippetBuild(\"cs\", \"TheKey\"),\n            ]);\n    }\n\n    [Fact]\n    public Task WithTwoLineSnippet()\n    {\n        var content = \"\"\"\n\n                      before\n\n                      snippet: theKey\n\n                      after\n\n                      \"\"\";\n\n        return SnippetVerifier.Verify(\n            DocumentConvention.SourceTransform,\n            content,\n            snippets:\n            [\n                Snippet.Build(\n                    language: \"cs\",\n                    startLine: 1,\n                    endLine: 2,\n                    value: \"\"\"\n                           the\n                           Snippet\n                           \"\"\",\n                    key: \"theKey\",\n                    path: \"thePath\",\n                    expressiveCode: null),\n            ]);\n    }\n\n    [Fact]\n    public Task WithMultiLineSnippet()\n    {\n        var content = \"\"\"\n\n                      before\n\n                      snippet: theKey\n\n                      after\n\n                      \"\"\";\n\n        return SnippetVerifier.Verify(\n            DocumentConvention.SourceTransform,\n            content,\n            snippets:\n            [\n                Snippet.Build(\n                    language: \"cs\",\n                    startLine: 1,\n                    endLine: 2,\n                    value: \"\"\"\n                           the\n                           long\n                           Snippet\n                           \"\"\",\n                    key: \"theKey\",\n                    path: \"thePath\",\n                    expressiveCode: null)\n            ]);\n    }\n\n    [Fact]\n    public Task WithDoubleInclude()\n    {\n        var content = \"\"\"\n\n                      before\n\n                      include: theKey\n\n                      after\n\n                      \"\"\";\n        var lines = new[]\n        {\n            \"theValue1\",\n            \"theValue2\"\n        };\n        return SnippetVerifier.Verify(\n            DocumentConvention.SourceTransform,\n            content,\n            includes:\n            [\n                Include.Build(\"theKey\", lines, Path.GetFullPath(\"thePath\"))\n            ]);\n    }\n\n    [Fact]\n    public Task WithEmptyMultipleInclude()\n    {\n        var content = \"\"\"\n\n                      before\n\n                      include: theKey\n\n                      after\n\n                      \"\"\";\n        var lines = new[]\n        {\n            \"\",\n            \"\",\n            \"\"\n        };\n        return SnippetVerifier.Verify(\n            DocumentConvention.SourceTransform,\n            content,\n            includes: [Include.Build(\"theKey\", lines, Path.GetFullPath(\"thePath\"))]);\n    }\n\n    [Fact]\n    public Task WithMultipleInclude()\n    {\n        var content = \"\"\"\n\n                      before\n\n                      include: theKey\n\n                      after\n\n                      \"\"\";\n        var lines = new[]\n        {\n            \"theValue1\",\n            \"theValue2\",\n            \"theValue3\"\n        };\n        return SnippetVerifier.Verify(\n            DocumentConvention.SourceTransform,\n            content,\n            includes: [Include.Build(\"theKey\", lines, Path.GetFullPath(\"thePath\"))]);\n    }\n\n    [Fact]\n    public Task MissingInclude()\n    {\n        var content = \"\"\"\n\n                      before\n\n                      include: theKey\n\n                      after\n\n                      \"\"\";\n        return SnippetVerifier.Verify(DocumentConvention.SourceTransform, content);\n    }\n\n    [Fact]\n    public Task SkipHeadingBeforeToc()\n    {\n        var content = \"\"\"\n\n                      ## Heading 1\n\n                      toc\n\n                      Text1\n\n                      ## Heading 2\n\n                      Text2\n\n                      \"\"\";\n        return SnippetVerifier.Verify(DocumentConvention.SourceTransform, content);\n    }\n\n    [Fact]\n    public Task Toc1()\n    {\n        var content = \"\"\"\n\n                      # Title\n\n                      toc1\n\n                      ## Heading 1\n\n                      Text1\n\n                      ## Heading 2\n\n                      Text2\n\n                      \"\"\";\n        return SnippetVerifier.Verify(DocumentConvention.SourceTransform, content);\n    }\n\n    [Fact]\n    public Task Toc()\n    {\n        var content = \"\"\"\n\n                      # Title\n\n                      toc\n\n                      ## Heading 1\n\n                      Text1\n\n                      ## Heading 2\n\n                      Text2\n\n                      \"\"\";\n        return SnippetVerifier.Verify(DocumentConvention.SourceTransform, content);\n    }\n\n    [Fact]\n    public Task TocRetainedIfNoHeadingsInFile()\n    {\n        var content = \"\"\"\n\n                      # Title\n\n                      toc\n\n                      This document has no headings.\n\n                      An empty toc section should be generated, in case\n                      any headings are added in future.\n\n                      \"\"\";\n        return SnippetVerifier.Verify(DocumentConvention.SourceTransform, content);\n    }\n\n    [Fact]\n    public Task Missing_endToc()\n    {\n        var content = \"\"\"\n\n                      <!-- toc -->\n                      Bad\n\n                      \"\"\";\n        return SnippetVerifier.VerifyThrows(DocumentConvention.InPlaceOverwrite, content);\n    }\n\n    [Fact]\n    public Task Empty_snippet_key()\n    {\n        var content = \"\"\"\n\n                      snippet:\n\n\n                      \"\"\";\n        return SnippetVerifier.VerifyThrows(DocumentConvention.InPlaceOverwrite, content);\n    }\n\n    [Fact]\n    public Task Whitespace_snippet_key()\n    {\n        var content = \"\"\"\n\n                      snippet:\n\n\n                      \"\"\";\n        return SnippetVerifier.VerifyThrows(DocumentConvention.InPlaceOverwrite, content);\n    }\n\n    [Fact]\n    public Task Toc_Overwrite()\n    {\n        var content = \"\"\"\n\n                      # Title\n\n                      <!-- toc -->\n                      Bad<!-- endToc -->\n\n                      ## Heading 1\n\n                      Text1\n\n                      ## Heading 2\n\n                      Text2\n\n                      \"\"\";\n        return SnippetVerifier.Verify(DocumentConvention.InPlaceOverwrite, content);\n    }\n\n    [Fact]\n    public Task Simple_Overwrite()\n    {\n        var availableSnippets = new List<Snippet>\n        {\n            SnippetBuild(\"cs\", \"snippet1\"),\n            SnippetBuild(\"cs\", \"snippet2\")\n        };\n        var content = \"\"\"\n\n                      <!-- snippet: snippet1 -->\n                      ```cs\n                      BAD\n                      ```\n                      <!-- endSnippet -->\n\n                      some text\n\n                      <!-- snippet: snippet2 -->\n                      ```cs\n                      BAD\n                      ```\n                      <!-- endSnippet -->\n\n                      some other text\n\n                      <!-- snippet: FileToUseAsSnippet.txt -->\n                      ```txt\n                      BAD\n                      ```\n                      <!-- endSnippet -->\n\n                      some other text\n\n                      <!-- snippet: /FileToUseAsSnippet.txt -->\n                      ```txt\n                      BAD\n                      ```\n                      <!-- endSnippet -->\n\n                      \"\"\";\n        return SnippetVerifier.Verify(\n            DocumentConvention.InPlaceOverwrite,\n            content,\n            availableSnippets,\n            new List<string>\n            {\n                Path.Combine(GitRepoDirectoryFinder.FindForFilePath(), \"src/Tests/FileToUseAsSnippet.txt\")\n            });\n    }\n\n    [Fact]\n    public async Task MixedNewlinesInFile()\n    {\n        var file = \"FileWithMixedNewLines.txt\";\n        File.Delete(file);\n        await File.WriteAllTextAsync(file, \"a\\rb\\nc\\r\\nd\");\n        var availableSnippets = new List<Snippet>();\n        var content = \"\"\"\n\n                      some other text\n\n                      snippet: FileWithMixedNewLines.txt\n\n                      \"\"\";\n        var result = await SnippetVerifier.Verify(\n            DocumentConvention.SourceTransform,\n            content,\n            availableSnippets,\n            new List<string>\n            {\n                file\n            });\n\n        Assert.DoesNotContain(\"\\r\\n\", result);\n        Assert.DoesNotContain(\"\\r\", result);\n    }\n\n    [Fact]\n    public Task Simple()\n    {\n        var availableSnippets = new List<Snippet>\n        {\n            SnippetBuild(\"cs\", \"snippet1\"),\n            SnippetBuild(\"cs\", \"snippet2\")\n        };\n        var content = \"\"\"\n\n                      snippet: snippet1\n\n                      some text\n\n                      snippet: snippet2\n\n                      some other text\n\n                      snippet: FileToUseAsSnippet.txt\n\n                      some other text\n\n                      snippet: /FileToUseAsSnippet.txt\n\n                      \"\"\";\n        return SnippetVerifier.Verify(\n            DocumentConvention.SourceTransform,\n            content,\n            availableSnippets,\n            new List<string>\n            {\n                Path.Combine(GitRepoDirectoryFinder.FindForFilePath(), \"src/Tests/FileToUseAsSnippet.txt\")\n            });\n    }\n\n    [Fact]\n    public Task SnippetInInclude()\n    {\n        var availableSnippets = new List<Snippet>\n        {\n            SnippetBuild(\"cs\", \"snippet1\")\n        };\n        var content = \"\"\"\n\n                      some text\n\n                      include: theKey\n\n                      some other text\n\n                      \"\"\";\n        var lines = new List<string>\n        {\n            \"snippet: snippet1\"\n        };\n        return SnippetVerifier.Verify(\n            DocumentConvention.SourceTransform,\n            content,\n            availableSnippets,\n            includes: [Include.Build(\"theKey\", lines, \"thePath\")]);\n    }\n\n    [Fact]\n    public Task TableInInclude()\n    {\n        var availableSnippets = new List<Snippet>();\n        var content = \"\"\"\n\n                      some text\n\n                      include: theKey\n\n                      some other text\n\n                      \"\"\";\n        var lines = new List<string>\n        {\n            \"\"\"\n            | Number of Parameters | Variations per Parameter | Total Combinations | Pairwise Combinations |\n            | -------------------- | ----------------------- | ------------------ | --------------------- |\n            |2|5|25|25|\n            \"\"\"\n        };\n        return SnippetVerifier.Verify(\n            DocumentConvention.SourceTransform,\n            content,\n            availableSnippets,\n            includes: [Include.Build(\"theKey\", lines, \"thePath\")]);\n    }\n\n    [Fact]\n    public Task SnippetInIncludeLast()\n    {\n        var availableSnippets = new List<Snippet>\n        {\n            SnippetBuild(\"cs\", \"snippet1\")\n        };\n        var content = \"\"\"\n\n                      some text\n\n                      include: theKey\n\n                      some other text\n\n                      \"\"\";\n        var lines = new List<string>\n        {\n            \"line1\",\n            \"snippet: snippet1\"\n        };\n        return SnippetVerifier.Verify(\n            DocumentConvention.SourceTransform,\n            content,\n            availableSnippets,\n            includes: [Include.Build(\"theKey\", lines, \"thePath\")]);\n    }\n\n    [Fact]\n    public Task WithIndentedSnippet()\n    {\n        var content = \"\"\"\n\n                      before\n\n                          snippet: theKey\n\n                      after\n\n                      \"\"\";\n\n        return SnippetVerifier.Verify(\n            DocumentConvention.SourceTransform,\n            content,\n            snippets: [SnippetBuild(\"cs\", \"theKey\")]);\n    }\n\n    [Fact]\n    public Task WithIndentedSnippetMultipleSpaces()\n    {\n        var content = \"\"\"\n\n                      before\n\n                              snippet: theKey\n\n                      after\n\n                      \"\"\";\n\n        return SnippetVerifier.Verify(\n            DocumentConvention.SourceTransform,\n            content,\n            snippets: [SnippetBuild(\"cs\", \"theKey\")]);\n    }\n\n    [Fact]\n    public Task WithIndentedCommentSnippet()\n    {\n        var content = \"\"\"\n\n                      before\n\n                          <!-- snippet: theKey -->\n                          bad content\n                          <!-- endSnippet -->\n\n                      after\n\n                      \"\"\";\n\n        return SnippetVerifier.Verify(\n            DocumentConvention.InPlaceOverwrite,\n            content,\n            snippets: [SnippetBuild(\"cs\", \"theKey\")]);\n    }\n\n    [Fact]\n    public Task WithTabIndentedSnippet()\n    {\n        var content = $\"\"\"\n\n                      before\n\n                      {\"\\t\"}snippet: theKey\n\n                      after\n\n                      \"\"\";\n\n        return SnippetVerifier.Verify(\n            DocumentConvention.SourceTransform,\n            content,\n            snippets: [SnippetBuild(\"cs\", \"theKey\")]);\n    }\n\n    [Fact]\n    public Task WithIndentedWebSnippet()\n    {\n        var content = \"\"\"\n\n                      before\n\n                          web-snippet: http://example.com/file.cs#snippet1\n\n                      after\n\n                      \"\"\";\n\n        return SnippetVerifier.Verify(\n            DocumentConvention.SourceTransform,\n            content,\n            snippets: [SnippetBuild(\"cs\", \"snippet1\")]);\n    }\n\n    [Fact]\n    public Task WithIndentedMultiLineSnippet()\n    {\n        var content = \"\"\"\n\n                      before\n\n                        snippet: theKey\n\n                      after\n\n                      \"\"\";\n\n        return SnippetVerifier.Verify(\n            DocumentConvention.SourceTransform,\n            content,\n            snippets:\n            [\n                Snippet.Build(\n                    language: \"cs\",\n                    startLine: 1,\n                    endLine: 2,\n                    value: \"\"\"\n                           the\n                           long\n                           Snippet\n                           \"\"\",\n                    key: \"theKey\",\n                    path: \"thePath\",\n                    expressiveCode: null)\n            ]);\n    }\n\n    [Fact]\n    public Task WithCommentWebSnippetUpdate()\n    {\n        var content = \"\"\"\n\n                      before\n\n                      <!-- web-snippet: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet -->\n                      OLD CONTENT\n                      THAT SHOULD BE\n                      REPLACED\n                      <!-- endSnippet -->\n\n                      after\n\n                      \"\"\";\n\n        return SnippetVerifier.Verify(\n            DocumentConvention.InPlaceOverwrite,\n            content);\n    }\n\n    [Fact]\n    public Task WithCommentWebSnippetWithViewUrl()\n    {\n        var content = \"\"\"\n\n                      before\n\n                      <!-- web-snippet: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet https://github.com/SimonCropp/MarkdownSnippets/blob/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt -->\n                      OLD CONTENT\n                      THAT SHOULD BE\n                      REPLACED\n                      <!-- endSnippet -->\n\n                      after\n\n                      \"\"\";\n\n        return SnippetVerifier.Verify(\n            DocumentConvention.InPlaceOverwrite,\n            content);\n    }\n\n    [Fact]\n    public Task WithInlineWebSnippetWithViewUrl()\n    {\n        var content = \"\"\"\n\n                      before\n\n                      web-snippet: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt#snipPet https://github.com/SimonCropp/MarkdownSnippets/blob/main/src/Tests/DirectorySnippetExtractor/Case/code1.txt\n\n                      after\n\n                      \"\"\";\n\n        return SnippetVerifier.Verify(\n            DocumentConvention.SourceTransform,\n            content);\n    }\n\n    static Snippet SnippetBuild(string language, string key) =>\n        Snippet.Build(\n            language: language,\n            startLine: 1,\n            endLine: 2,\n            value: \"Snippet\",\n            key: key,\n            path: \"thePath\",\n            expressiveCode: null);\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/SnippetKey_ExtractStartCommentSnippet.cs",
    "content": "﻿public class SnippetKey_ExtractStartCommentSnippet\n{\n    [Fact]\n    public void WithDashes()\n    {\n        Assert.True(SnippetKey.ExtractStartCommentSnippet(new(\"<!-- snippet: my-code-snippet -->\", \"path\", 1), out var key));\n        Assert.Equal(\"my-code-snippet\", key);\n    }\n\n    [Fact]\n    public void Simple()\n    {\n        Assert.True(SnippetKey.ExtractStartCommentSnippet(new(\"<!-- snippet: snippet -->\", \"path\", 1), out var key));\n        Assert.Equal(\"snippet\", key);\n    }\n\n    [Fact]\n    public void MissingClosingComment_Throws()\n    {\n        var line = new Line(\"<!-- snippet: my-snippet\", \"test.md\", 5);\n        var exception = Assert.Throws<SnippetException>(() => SnippetKey.ExtractStartCommentSnippet(line, out _));\n        Assert.Contains(\"-->\", exception.Message);\n        Assert.Contains(\"test.md\", exception.Message);\n    }\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/SnippetKey_ExtractStartCommentWebSnippet.cs",
    "content": "﻿public class SnippetKey_ExtractStartCommentWebSnippet\n{\n    [Fact]\n    public void Simple()\n    {\n        Assert.True(SnippetKey.ExtractStartCommentWebSnippet(new(\"<!-- web-snippet: https://example.com/file.cs#mysnippet -->\", \"path\", 1), out var url, out var key));\n        Assert.Equal(\"https://example.com/file.cs\", url);\n        Assert.Equal(\"mysnippet\", key);\n    }\n\n    [Fact]\n    public void MissingClosingComment_Throws()\n    {\n        var line = new Line(\"<!-- web-snippet: https://example.com/file.cs#mysnippet\", \"test.md\", 10);\n        var exception = Assert.Throws<SnippetException>(() => SnippetKey.ExtractStartCommentWebSnippet(line, out _, out _));\n        Assert.Contains(\"-->\", exception.Message);\n        Assert.Contains(\"test.md\", exception.Message);\n    }\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/SnippetKey_ExtractTransform.cs",
    "content": "﻿public class SnippetKey_ExtractTransform\n{\n    [Fact]\n    public void MissingSpaces()\n    {\n        Assert.True( SnippetKey.ExtractSnippet(new(\"snippet:snippet\", \"path\", 1), out var key));\n        Assert.Equal(\"snippet\", key);\n    }\n\n    [Fact]\n    public void WithDashes()\n    {\n        Assert.True(SnippetKey.ExtractSnippet(new(\"snippet: my-code-snippet\", \"path\", 1), out var key));\n        Assert.Equal(\"my-code-snippet\", key);\n    }\n\n    [Fact]\n    public void Simple()\n    {\n        Assert.True(SnippetKey.ExtractSnippet(new(\"snippet: snippet\", \"path\", 1), out var key));\n        Assert.Equal(\"snippet\", key);\n    }\n\n    [Fact]\n    public void ExtraSpace()\n    {\n        Assert.True(SnippetKey.ExtractSnippet(new(\"snippet:  snippet   \", \"path\", 1), out var key));\n        Assert.Equal(\"snippet\", key);\n    }\n}"
  },
  {
    "path": "src/Tests/MarkdownProcessor/TocBuilderTests.Deep.verified.txt",
    "content": "﻿<!-- toc -->\n## Contents\n\n  * [Heading1](#heading1)\n    * [Heading2](#heading2)\n      * [Heading3](#heading3)\n        * [Heading4](#heading4)<!-- endToc -->"
  },
  {
    "path": "src/Tests/MarkdownProcessor/TocBuilderTests.DuplicateNested.verified.txt",
    "content": "﻿<!-- toc -->\n## Contents\n\n  * [Heading](#heading)\n    * [Heading](#heading-1)\n      * [Heading](#heading-2)<!-- endToc -->"
  },
  {
    "path": "src/Tests/MarkdownProcessor/TocBuilderTests.Duplicates.verified.txt",
    "content": "﻿<!-- toc -->\n## Contents\n\n  * [A](#a)\n  * [A](#a-1)\n  * [a](#a-2)<!-- endToc -->"
  },
  {
    "path": "src/Tests/MarkdownProcessor/TocBuilderTests.EmptyHeading.verified.txt",
    "content": "﻿<!-- toc -->\n<!-- endToc -->"
  },
  {
    "path": "src/Tests/MarkdownProcessor/TocBuilderTests.Exclude.verified.txt",
    "content": "﻿<!-- toc -->\n## Contents\n\n  * [Heading1](#heading1)<!-- endToc -->"
  },
  {
    "path": "src/Tests/MarkdownProcessor/TocBuilderTests.IgnoreTop.verified.txt",
    "content": "﻿<!-- toc -->\n## Contents\n\n  * [Heading2](#heading2)<!-- endToc -->"
  },
  {
    "path": "src/Tests/MarkdownProcessor/TocBuilderTests.Nested.verified.txt",
    "content": "﻿<!-- toc -->\n## Contents\n\n  * [Heading1](#heading1)\n    * [Heading2](#heading2)\n  * [Heading3](#heading3)\n    * [Heading4](#heading4)<!-- endToc -->"
  },
  {
    "path": "src/Tests/MarkdownProcessor/TocBuilderTests.SanitizeLink.verified.txt",
    "content": "﻿a_-b"
  },
  {
    "path": "src/Tests/MarkdownProcessor/TocBuilderTests.Single.verified.txt",
    "content": "﻿<!-- toc -->\n## Contents\n\n  * [Heading](#heading)<!-- endToc -->"
  },
  {
    "path": "src/Tests/MarkdownProcessor/TocBuilderTests.StopAtLevel.verified.txt",
    "content": "﻿<!-- toc -->\n## Contents\n\n  * [Heading1](#heading1)\n    * [Heading2](#heading2)<!-- endToc -->"
  },
  {
    "path": "src/Tests/MarkdownProcessor/TocBuilderTests.StripMarkdown.verified.txt",
    "content": "﻿<!-- toc -->\n## Contents\n\n  * [bold italic Link](#bold-italic-link)<!-- endToc -->"
  },
  {
    "path": "src/Tests/MarkdownProcessor/TocBuilderTests.WithSpaces.verified.txt",
    "content": "﻿<!-- toc -->\n## Contents\n\n  * [A B](#a-b)<!-- endToc -->"
  },
  {
    "path": "src/Tests/MarkdownProcessor/TocBuilderTests.cs",
    "content": "﻿public class TocBuilderTests\n{\n    [Fact]\n    public Task EmptyHeading()\n    {\n        var lines = new List<Line>\n        {\n            new(\"##\", \"\", 0)\n        };\n\n        var buildToc = TocBuilder.BuildToc(lines, 1, [], \"\\r\");\n        Assert.DoesNotContain(\"\\r\\n\", buildToc);\n        return Verify(buildToc);\n    }\n\n    [Fact]\n    public Task IgnoreTop()\n    {\n        var lines = new List<Line>\n        {\n            new(\"# Heading1\", \"\", 0),\n            new(\"## Heading2\", \"\", 0)\n        };\n\n        return Verify(TocBuilder.BuildToc(lines, 1, [], Environment.NewLine));\n    }\n\n    [Fact]\n    public Task SanitizeLink()\n    {\n        var builder = new StringBuilder();\n        TocBuilder.SanitizeLink(builder, \"A!@#$%,^&*()_+-={};':\\\"<>?/b\");\n        return Verify(builder);\n    }\n\n    [Fact]\n    public Task StripMarkdown()\n    {\n        var lines = new List<Line>\n        {\n            new(\"## **bold** *italic* [Link](link)\", \"\", 0)\n        };\n\n        return Verify(TocBuilder.BuildToc(lines, 1, [], Environment.NewLine));\n    }\n\n    [Fact]\n    public Task Exclude()\n    {\n        var lines = new List<Line>\n        {\n            new(\"## Heading1\", \"\", 0),\n            new(\"### Heading2\", \"\", 0)\n        };\n\n        return Verify(TocBuilder.BuildToc(lines, 1, [\"Heading2\"], Environment.NewLine));\n    }\n\n    [Fact]\n    public Task Nested()\n    {\n        var lines = new List<Line>\n        {\n            new(\"## Heading1\", \"\", 0),\n            new(\"### Heading2\", \"\", 0),\n            new(\"## Heading3\", \"\", 0),\n            new(\"### Heading4\", \"\", 0)\n        };\n\n        return Verify(TocBuilder.BuildToc(lines, 2, [], Environment.NewLine));\n    }\n\n    [Fact]\n    public Task Deep()\n    {\n        var lines = new List<Line>\n        {\n            new(\"## Heading1\", \"\", 0),\n            new(\"### Heading2\", \"\", 0),\n            new(\"#### Heading3\", \"\", 0),\n            new(\"##### Heading4\", \"\", 0)\n        };\n\n        return Verify(TocBuilder.BuildToc(lines, 10, [], Environment.NewLine));\n    }\n\n    [Fact]\n    public Task StopAtLevel()\n    {\n        var lines = new List<Line>\n        {\n            new(\"## Heading1\", \"\", 0),\n            new(\"### Heading2\", \"\", 0),\n            new(\"#### Heading3\", \"\", 0)\n        };\n\n        return Verify(TocBuilder.BuildToc(lines, 2, [], Environment.NewLine));\n    }\n\n    [Fact]\n    public Task Single()\n    {\n        var lines = new List<Line>\n        {\n            new(\"## Heading\", \"\", 0)\n        };\n\n        return Verify(TocBuilder.BuildToc(lines, 1, [], Environment.NewLine));\n    }\n\n    [Fact]\n    public Task WithSpaces()\n    {\n        var lines = new List<Line>\n        {\n            new(\"##  A B \", \"\", 0)\n        };\n\n        return Verify(TocBuilder.BuildToc(lines, 1, [], Environment.NewLine));\n    }\n\n    [Fact]\n    public Task DuplicateNested()\n    {\n        var lines = new List<Line>\n        {\n            new(\"## Heading\", \"\", 0),\n            new(\"### Heading\", \"\", 0),\n            new(\"#### Heading\", \"\", 0)\n        };\n\n        return Verify(TocBuilder.BuildToc(lines,4, [], Environment.NewLine));\n    }\n\n    [Fact]\n    public Task Duplicates()\n    {\n        var lines = new List<Line>\n        {\n            new(\"## A\", \"\", 0),\n            new(\"## A\", \"\", 0),\n            new(\"## a\", \"\", 0)\n        };\n\n        return Verify(TocBuilder.BuildToc(lines, 1, [], Environment.NewLine));\n    }\n}\n"
  },
  {
    "path": "src/Tests/ModuleInitializer.cs",
    "content": "﻿public static class ModuleInitializer\n{\n    [ModuleInitializer]\n    public static void Initialize()\n    {\n        VerifyDiffPlex.Initialize(OutputType.Compact);\n        VerifierSettings.IgnoreStackTrace();\n        VerifierSettings.AddExtraSettings(serializer =>\n        {\n            var converters = serializer.Converters;\n            converters.Add(new ProcessResultConverter());\n            converters.Add(new SnippetConverter());\n        });\n        VerifierSettings.AddScrubber(_ => _.Replace('\\\\', '/'));\n    }\n}"
  },
  {
    "path": "src/Tests/MsBuildIntegrationTests.cs",
    "content": "// Integration tests for MSBuild task\n//\n// These tests verify that the MarkdownSnippets.MsBuild NuGet package works correctly\n// when consumed by a project using both .NET Core (dotnet build) and .NET Framework (msbuild.exe).\n//\n// How it works:\n// 1. Creates a temporary directory with a minimal test project\n// 2. Configures nuget.config to use the local nugets folder (C:\\Code\\MarkdownSnippets\\nugets)\n//    with a local packages cache to avoid global NuGet cache issues\n// 3. Creates a .csproj referencing MarkdownSnippets.MsBuild\n// 4. Creates a C# file with a code snippet and a markdown file referencing it\n// 5. Runs the build (dotnet or msbuild.exe) which triggers the MarkdownSnippets task\n// 6. Verifies the markdown was processed correctly\n//\n// The .NET Framework test (msbuild.exe) is particularly important because:\n// - MSBuild loads the netstandard2.0 version of the task DLL\n// - All dependencies (including System.Collections.Immutable with FrozenSet) are shaded\n//   via PackageShader to avoid version conflicts with MSBuild's own dependencies\n// - Static field data (FieldRVA entries) must be correctly patched when shading\n//\n// These tests only run in RELEASE configuration because they depend on the\n// MarkdownSnippets.MsBuild.nupkg being built in the nugets folder.\n#if RELEASE\npublic class MsBuildIntegrationTests\n{\n    static string GetNugetsDir()\n    {\n        var solutionDir = ProjectFiles.SolutionDirectory.Path.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);\n        // SolutionDirectory is C:\\Code\\MarkdownSnippets\\src, nugets is at C:\\Code\\MarkdownSnippets\\nugets\n        var repoDir = Directory.GetParent(solutionDir)?.FullName\n                      ?? throw new InvalidOperationException($\"Could not get parent of {solutionDir}\");\n        return Path.Combine(repoDir, \"nugets\");\n    }\n\n    [Fact]\n    public async Task DotnetBuild_UsesNetCoreTask()\n    {\n        using var tempDir = new TempDirectory();\n        await SetupTestProject(tempDir);\n\n        var result = await RunProcess(\"dotnet\", $\"build \\\"{tempDir}\\\" -c Release -nodeReuse:false\", tempDir);\n\n        Assert.True(result.ExitCode == 0, $\"dotnet build failed:\\n{result.Output}\\n{result.Error}\");\n\n        // Allow build processes to fully release file handles before cleanup\n        await Task.Delay(2000);\n\n        // Verify the markdown was processed (generated header indicates task ran)\n        var outputMd = Path.Combine(tempDir, \"docs\", \"readme.md\");\n        Assert.True(File.Exists(outputMd), $\"Output markdown should exist at {outputMd}\");\n        var content = await File.ReadAllTextAsync(outputMd);\n        Assert.Contains(\"GENERATED FILE\", content);\n    }\n\n    [Fact]\n    public async Task MsBuild_UsesNetFrameworkTask()\n    {\n        var msbuildPath = FindMsBuild();\n        if (msbuildPath == null)\n        {\n            // Skip if msbuild.exe not found\n            return;\n        }\n\n        using var tempDir = new TempDirectory();\n        await SetupTestProject(tempDir);\n\n        var result = await RunProcess(msbuildPath, $\"\\\"{tempDir}\\\" /p:Configuration=Release /restore /nodeReuse:false -verbosity:minimal\", tempDir);\n\n        Assert.True(result.ExitCode == 0, $\"msbuild failed:\\n{result.Output}\\n{result.Error}\");\n\n        // Allow MSBuild processes to fully release file handles before cleanup\n        await Task.Delay(2000);\n\n        // Verify the markdown was processed (generated header indicates task ran)\n        var outputMd = Path.Combine(tempDir, \"docs\", \"readme.md\");\n        Assert.True(File.Exists(outputMd), $\"Output markdown should exist at {outputMd}\");\n        var content = await File.ReadAllTextAsync(outputMd);\n        Assert.Contains(\"GENERATED FILE\", content);\n    }\n\n    static async Task SetupTestProject(TempDirectory tempDir)\n    {\n        // Find the latest nuget version\n        var nugetVersion = GetLatestNugetVersion();\n\n        // Create nuget.config pointing to local nugets folder\n        // Use a local packages folder to avoid global cache issues when testing local package changes\n        var localPackagesFolder = Path.Combine(tempDir, \"packages\");\n        var nugetConfig = $\"\"\"\n                           <?xml version=\"1.0\" encoding=\"utf-8\"?>\n                           <configuration>\n                             <config>\n                               <add key=\"globalPackagesFolder\" value=\"{localPackagesFolder}\" />\n                             </config>\n                             <packageSources>\n                               <clear />\n                               <add key=\"local\" value=\"{GetNugetsDir()}\" />\n                               <add key=\"nuget.org\" value=\"https://api.nuget.org/v3/index.json\" />\n                             </packageSources>\n                           </configuration>\n                           \"\"\";\n        await File.WriteAllTextAsync(Path.Combine(tempDir, \"nuget.config\"), nugetConfig);\n\n        // Create a minimal csproj\n        var csproj = $\"\"\"\n                      <Project Sdk=\"Microsoft.NET.Sdk\">\n                        <PropertyGroup>\n                          <TargetFramework>net8.0</TargetFramework>\n                          <OutputType>Library</OutputType>\n                        </PropertyGroup>\n                        <ItemGroup>\n                          <PackageReference Include=\"MarkdownSnippets.MsBuild\" Version=\"{nugetVersion}\" PrivateAssets=\"all\" />\n                        </ItemGroup>\n                      </Project>\n                      \"\"\";\n        await File.WriteAllTextAsync(Path.Combine(tempDir, \"TestProject.csproj\"), csproj);\n\n        // Create a simple C# file with a snippet\n        var csFile = \"\"\"\n                     using System;\n\n                     public class Sample\n                     {\n                         public void Method()\n                         {\n                             // begin-snippet: MySnippet\n                             var message = \"Hello from snippet\";\n                             Console.WriteLine(message);\n                             // end-snippet\n                         }\n                     }\n                     \"\"\";\n        await File.WriteAllTextAsync(Path.Combine(tempDir, \"Sample.cs\"), csFile);\n\n        // Initialize git repo (required by MarkdownSnippets)\n        await RunProcess(\"git\", \"init\", tempDir);\n        await RunProcess(\"git\", \"config user.email \\\"test@test.com\\\"\", tempDir);\n        await RunProcess(\"git\", \"config user.name \\\"Test\\\"\", tempDir);\n\n        // Create docs directory and source.md\n        var docsDir = Path.Combine(tempDir, \"docs\");\n        Directory.CreateDirectory(docsDir);\n\n        var sourceMd = \"\"\"\n                       Test Document\n\n                       <!-- snippet: MySnippet -->\n                       <!-- endSnippet -->\n                       \"\"\";\n        await File.WriteAllTextAsync(Path.Combine(docsDir, \"readme.source.md\"), sourceMd);\n    }\n\n    static string GetLatestNugetVersion()\n    {\n        var packages = Directory.GetFiles(GetNugetsDir(), \"MarkdownSnippets.MsBuild.*.nupkg\")\n            .Select(Path.GetFileNameWithoutExtension)\n            .Where(_ => _ != null)\n            .Select(_ => _!.Replace(\"MarkdownSnippets.MsBuild.\", \"\"))\n            .OrderByDescending(_ => _)\n            .FirstOrDefault();\n\n        return packages ?? throw new InvalidOperationException(\"No MarkdownSnippets.MsBuild nuget found. Run Release build first.\");\n    }\n\n    static string? FindMsBuild()\n    {\n        // Try to find msbuild.exe via vswhere\n        var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);\n        var vswherePath = Path.Combine(programFiles, \"Microsoft Visual Studio\", \"Installer\", \"vswhere.exe\");\n\n        if (!File.Exists(vswherePath))\n        {\n            return null;\n        }\n\n        var process = Process.Start(\n            new ProcessStartInfo\n            {\n                FileName = vswherePath,\n                Arguments = @\"-latest -requires Microsoft.Component.MSBuild -find MSBuild\\**\\Bin\\MSBuild.exe\",\n                RedirectStandardOutput = true,\n                UseShellExecute = false,\n                CreateNoWindow = true\n            })!;\n\n        process.WaitForExit();\n        var msbuildPath = process.StandardOutput.ReadToEnd().Trim().Split('\\n').FirstOrDefault()?.Trim();\n\n        if (string.IsNullOrEmpty(msbuildPath) || !File.Exists(msbuildPath))\n        {\n            return null;\n        }\n\n        return msbuildPath;\n    }\n\n    static async Task<ProcessResult> RunProcess(string fileName, string arguments, string workingDirectory)\n    {\n        var psi = new ProcessStartInfo\n        {\n            FileName = fileName,\n            Arguments = arguments,\n            WorkingDirectory = workingDirectory,\n            RedirectStandardOutput = true,\n            RedirectStandardError = true,\n            UseShellExecute = false,\n            CreateNoWindow = true\n        };\n\n        using var process = Process.Start(psi)!;\n        var output = await process.StandardOutput.ReadToEndAsync();\n        var error = await process.StandardError.ReadToEndAsync();\n        await process.WaitForExitAsync();\n\n        return new(process.ExitCode, output, error);\n    }\n\n    record ProcessResult(int ExitCode, string Output, string Error);\n\n    [Fact(Explicit = true)]\n    public async Task MsBuild_AllLocalProjects_UsingMarkdownSnippets()\n    {\n        var msbuildPath = FindMsBuild();\n        if (msbuildPath == null)\n        {\n            // Skip if msbuild.exe not found\n            return;\n        }\n\n        var codeRoots = new[] { @\"C:\\Code\", @\"D:\\Code\" }\n            .Where(Directory.Exists)\n            .ToList();\n\n        if (codeRoots.Count == 0)\n        {\n            // Skip if no code directories exist\n            return;\n        }\n\n        var projectsUsingMdSnippets = codeRoots\n            .SelectMany(FindProjectsUsingMarkdownSnippets)\n            .ToList();\n        if (projectsUsingMdSnippets.Count == 0)\n        {\n            return;\n        }\n\n        var failures = new List<(string Directory, string Error)>();\n\n        foreach (var projectDir in projectsUsingMdSnippets)\n        {\n            // Skip MarkdownSnippets itself\n            if (projectDir.Contains(\"MarkdownSnippets\", StringComparison.OrdinalIgnoreCase) &&\n                projectDir.Contains(Path.Combine(\"Code\", \"MarkdownSnippets\"), StringComparison.OrdinalIgnoreCase))\n            {\n                continue;\n            }\n\n            var result = await RunMsBuildOnProject(msbuildPath, projectDir);\n            if (result.ExitCode != 0)\n            {\n                // Extract just the error lines (lines containing \"error\")\n                var errorLines = ExtractErrorLines(result.Output + \"\\n\" + result.Error);\n                failures.Add((projectDir, errorLines));\n            }\n        }\n\n        // Generate markdown report\n        var reportPath = Path.Combine(GetNugetsDir(), \"msbuild-test-report.md\");\n        await GenerateMarkdownReport(reportPath, projectsUsingMdSnippets.Count, failures);\n\n        // Output report location\n        if (failures.Count > 0)\n        {\n            Assert.Fail($\"MSBuild failed for {failures.Count} projects. Report written to: {reportPath}\\n\\n\" +\n                        $\"Summary:\\n{string.Join(\"\\n\", failures.Select(f => $\"  - {f.Directory}\"))}\");\n        }\n    }\n\n    static List<string> FindProjectsUsingMarkdownSnippets(string rootDir)\n    {\n        var results = new List<string>();\n\n        try\n        {\n            // Find all Directory.Packages.props files\n            var propsFiles = Directory.GetFiles(rootDir, \"Directory.Packages.props\", SearchOption.AllDirectories);\n\n            foreach (var propsFile in propsFiles)\n            {\n                try\n                {\n                    var content = File.ReadAllText(propsFile);\n                    if (content.Contains(\"MarkdownSnippets.MsBuild\", StringComparison.OrdinalIgnoreCase))\n                    {\n                        // Get the directory containing this props file (the repo root or src folder)\n                        var dir = Path.GetDirectoryName(propsFile)!;\n                        results.Add(dir);\n                    }\n                }\n                catch\n                {\n                    // Ignore files we can't read\n                }\n            }\n        }\n        catch\n        {\n            // Ignore directories we can't access\n        }\n\n        return results;\n    }\n\n    static Task<ProcessResult> RunMsBuildOnProject(string msbuildPath, string projectDir)\n    {\n        // Find a solution file (.sln or .slnx)\n        var slnFiles = Directory.GetFiles(projectDir, \"*.sln\");\n        var slnxFiles = Directory.GetFiles(projectDir, \"*.slnx\");\n\n        string targetFile;\n        if (slnFiles.Length > 0)\n        {\n            targetFile = slnFiles[0];\n        }\n        else if (slnxFiles.Length > 0)\n        {\n            targetFile = slnxFiles[0];\n        }\n        else\n        {\n            // Try to find in subdirectories\n            slnFiles = Directory.GetFiles(projectDir, \"*.sln\", SearchOption.AllDirectories);\n            if (slnFiles.Length > 0)\n            {\n                targetFile = slnFiles[0];\n            }\n            else\n            {\n                slnxFiles = Directory.GetFiles(projectDir, \"*.slnx\", SearchOption.AllDirectories);\n                if (slnxFiles.Length > 0)\n                {\n                    targetFile = slnxFiles[0];\n                }\n                else\n                {\n                    throw new InvalidOperationException($\"No .sln or .slnx found in {projectDir}\");\n                }\n            }\n        }\n\n        var arguments = $\"\\\"{targetFile}\\\" /p:Configuration=Release /restore /nodeReuse:false /t:Build -verbosity:minimal -maxcpucount:1\";\n        return RunProcess(msbuildPath, arguments, projectDir);\n    }\n\n    static string ExtractErrorLines(string output)\n    {\n        var lines = output.Split('\\n');\n        var errorLines = lines\n            .Where(l =>\n                // Standard MSBuild error format: \"path(line,col): error CODE: message\"\n                l.Contains(\": error \", StringComparison.OrdinalIgnoreCase) ||\n                // Error prefix format\n                l.Contains(\"error MSB\", StringComparison.OrdinalIgnoreCase) ||\n                l.Contains(\"error CS\", StringComparison.OrdinalIgnoreCase) ||\n                l.Contains(\"error FS\", StringComparison.OrdinalIgnoreCase) ||\n                l.Contains(\"error NU\", StringComparison.OrdinalIgnoreCase) ||\n                // MarkdownSnippets specific errors\n                (l.Contains(\"MarkdownSnippets\", StringComparison.OrdinalIgnoreCase) &&\n                 l.Contains(\"error\", StringComparison.OrdinalIgnoreCase)) ||\n                // MSBUILD : error format (no file path)\n                l.TrimStart().StartsWith(\"MSBUILD : error\", StringComparison.OrdinalIgnoreCase) ||\n                // Build failed line\n                l.Contains(\"Build FAILED\", StringComparison.OrdinalIgnoreCase) ||\n                // Custom error messages\n                l.Contains(\"No .sln or .slnx found\", StringComparison.OrdinalIgnoreCase))\n            .Select(l => l.Trim())\n            .Where(l => !string.IsNullOrWhiteSpace(l))\n            .Take(10) // Limit to first 10 error lines\n            .ToList();\n\n        if (errorLines.Count > 0)\n        {\n            return string.Join(\"\\n\", errorLines);\n        }\n\n        // If no specific errors found, capture last 15 non-empty lines of output\n        var lastLines = lines\n            .Select(l => l.Trim())\n            .Where(l => !string.IsNullOrWhiteSpace(l))\n            .TakeLast(15)\n            .ToList();\n\n        return lastLines.Count > 0\n            ? $\"(Last {lastLines.Count} lines of output)\\n{string.Join(\"\\n\", lastLines)}\"\n            : \"Build failed (no output captured)\";\n    }\n\n    static Task GenerateMarkdownReport(string reportPath, int totalProjects, List<(string Directory, string Error)> failures)\n    {\n        var sb = new StringBuilder();\n        sb.AppendLine(\"# MSBuild Integration Test Report\");\n        sb.AppendLine();\n        sb.AppendLine($\"**Date:** {DateTime.Now:yyyy-MM-dd HH:mm:ss}\");\n        sb.AppendLine();\n        sb.AppendLine($\"**Total Projects Tested:** {totalProjects}\");\n        sb.AppendLine($\"**Passed:** {totalProjects - failures.Count}\");\n        sb.AppendLine($\"**Failed:** {failures.Count}\");\n        sb.AppendLine();\n\n        if (failures.Count == 0)\n        {\n            sb.AppendLine(\"## Result: All projects built successfully! ✓\");\n        }\n        else\n        {\n            sb.AppendLine(\"## Failures\");\n            sb.AppendLine();\n\n            foreach (var (directory, error) in failures)\n            {\n                sb.AppendLine($\"### {directory}\");\n                sb.AppendLine();\n                sb.AppendLine(\"```\");\n                sb.AppendLine(error);\n                sb.AppendLine(\"```\");\n                sb.AppendLine();\n            }\n        }\n\n        return File.WriteAllTextAsync(reportPath, sb.ToString());\n    }\n}\n#endif\n"
  },
  {
    "path": "src/Tests/NewLineConfigReaderTests.cs",
    "content": "public class NewLineConfigReaderTests\n{\n    [Fact]\n    public void GitAttributes_WildcardEolLf()\n    {\n        var directory = new TempDirectory();\n\n        File.WriteAllText(Path.Combine(directory, \".gitattributes\"), \"* text eol=lf\");\n        var result = NewLineConfigReader.ReadNewLine(directory, []);\n        Assert.Equal(\"\\n\", result);\n    }\n\n    [Fact]\n    public void GitAttributes_WildcardEolCrlf()\n    {\n        var directory = new TempDirectory();\n        File.WriteAllText(Path.Combine(directory, \".gitattributes\"), \"* text eol=crlf\");\n        var result = NewLineConfigReader.ReadNewLine(directory, []);\n        Assert.Equal(\"\\r\\n\", result);\n    }\n\n    [Fact]\n    public void GitAttributes_MdSpecificEolLf()\n    {\n        var directory = new TempDirectory();\n        File.WriteAllText(Path.Combine(directory, \".gitattributes\"), \"*.md text eol=lf\");\n        var result = NewLineConfigReader.ReadNewLine(directory, []);\n        Assert.Equal(\"\\n\", result);\n    }\n\n    [Fact]\n    public void GitAttributes_MdSpecificOverridesWildcard()\n    {\n        var directory = new TempDirectory();\n        File.WriteAllText(\n            Path.Combine(directory, \".gitattributes\"),\n            \"\"\"\n            * text eol=crlf\n            *.md text eol=lf\n            \"\"\");\n        var result = NewLineConfigReader.ReadNewLine(directory, []);\n        Assert.Equal(\"\\n\", result);\n    }\n\n    [Fact]\n    public void GitAttributes_NoEolSetting_FallsBackToEnvironmentNewLine()\n    {\n        var directory = new TempDirectory();\n        File.WriteAllText(Path.Combine(directory, \".gitattributes\"), \"* text\");\n        var result = NewLineConfigReader.ReadNewLine(directory, []);\n        Assert.Equal(Environment.NewLine, result);\n    }\n\n    [Fact]\n    public void GitAttributes_IgnoresComments()\n    {\n        var directory = new TempDirectory();\n        File.WriteAllText(\n            Path.Combine(directory, \".gitattributes\"),\n            \"\"\"\n            # comment eol=crlf\n            * text eol=lf\n            \"\"\");\n        var result = NewLineConfigReader.ReadNewLine(directory, []);\n        Assert.Equal(\"\\n\", result);\n    }\n\n    [Fact]\n    public void GitAttributes_InParentDirectory()\n    {\n        var directory = new TempDirectory();\n        var childDir = Path.Combine(directory, \"child\");\n        Directory.CreateDirectory(childDir);\n        File.WriteAllText(Path.Combine(directory, \".gitattributes\"), \"* text eol=lf\");\n        var result = NewLineConfigReader.ReadNewLine(childDir, []);\n        Assert.Equal(\"\\n\", result);\n    }\n\n    [Fact]\n    public void EditorConfig_WildcardEndOfLineLf()\n    {\n        var directory = new TempDirectory();\n        File.WriteAllText(\n            Path.Combine(directory, \".editorconfig\"),\n            \"\"\"\n            [*]\n            end_of_line = lf\n            \"\"\");\n        var result = NewLineConfigReader.ReadNewLine(directory, []);\n        Assert.Equal(\"\\n\", result);\n    }\n\n    [Fact]\n    public void EditorConfig_WildcardEndOfLineCrlf()\n    {\n        var directory = new TempDirectory();\n        File.WriteAllText(\n            Path.Combine(directory, \".editorconfig\"),\n            \"\"\"\n            [*]\n            end_of_line = crlf\n            \"\"\");\n        var result = NewLineConfigReader.ReadNewLine(directory, []);\n        Assert.Equal(\"\\r\\n\", result);\n    }\n\n    [Fact]\n    public void EditorConfig_MdSpecificEndOfLine()\n    {\n        var directory = new TempDirectory();\n        File.WriteAllText(\n            Path.Combine(directory, \".editorconfig\"),\n            \"\"\"\n            [*.md]\n            end_of_line = lf\n            \"\"\");\n        var result = NewLineConfigReader.ReadNewLine(directory, []);\n        Assert.Equal(\"\\n\", result);\n    }\n\n    [Fact]\n    public void EditorConfig_MdSpecificOverridesWildcard()\n    {\n        var directory = new TempDirectory();\n        File.WriteAllText(\n            Path.Combine(directory, \".editorconfig\"),\n            \"\"\"\n            [*]\n            end_of_line = crlf\n\n            [*.md]\n            end_of_line = lf\n            \"\"\");\n        var result = NewLineConfigReader.ReadNewLine(directory, []);\n        Assert.Equal(\"\\n\", result);\n    }\n\n    [Fact]\n    public void EditorConfig_BracePattern()\n    {\n        var directory = new TempDirectory();\n        File.WriteAllText(\n            Path.Combine(directory, \".editorconfig\"),\n            \"\"\"\n            [*.{md,txt}]\n            end_of_line = lf\n            \"\"\");\n        var result = NewLineConfigReader.ReadNewLine(directory, []);\n        Assert.Equal(\"\\n\", result);\n    }\n\n    [Fact]\n    public void EditorConfig_IgnoresComments()\n    {\n        var directory = new TempDirectory();\n        File.WriteAllText(\n            Path.Combine(directory, \".editorconfig\"),\n            \"\"\"\n            # comment\n            ; another comment\n            [*]\n            end_of_line = lf\n            \"\"\");\n        var result = NewLineConfigReader.ReadNewLine(directory, []);\n        Assert.Equal(\"\\n\", result);\n    }\n\n    [Fact]\n    public void GitAttributes_TakesPriorityOverEditorConfig()\n    {\n        var directory = new TempDirectory();\n        File.WriteAllText(Path.Combine(directory, \".gitattributes\"), \"* text eol=crlf\");\n        File.WriteAllText(\n            Path.Combine(directory, \".editorconfig\"),\n            \"\"\"\n            [*]\n            end_of_line = lf\n            \"\"\");\n        var result = NewLineConfigReader.ReadNewLine(directory, []);\n        Assert.Equal(\"\\r\\n\", result);\n    }\n\n    [Fact]\n    public void NoConfigFiles_FallsBackToEnvironmentNewLine()\n    {\n        var directory = new TempDirectory();\n        var result = NewLineConfigReader.ReadNewLine(directory, []);\n        Assert.Equal(Environment.NewLine, result);\n    }\n\n    [Fact]\n    public void EditorConfig_FallbackWhenGitAttributesHasNoEol()\n    {\n        var directory = new TempDirectory();\n        File.WriteAllText(Path.Combine(directory, \".gitattributes\"), \"* text\");\n        File.WriteAllText(\n            Path.Combine(directory, \".editorconfig\"),\n            \"\"\"\n            [*]\n            end_of_line = lf\n            \"\"\");\n        var result = NewLineConfigReader.ReadNewLine(directory, []);\n        Assert.Equal(\"\\n\", result);\n    }\n\n    [Fact]\n    public void DetectsNewLineFromMdFiles_Lf()\n    {\n        var directory = new TempDirectory();\n        var mdFile = Path.Combine(directory, \"test.md\");\n        File.WriteAllText(mdFile, \"line1\\nline2\\n\");\n        var result = NewLineConfigReader.ReadNewLine(directory, [mdFile]);\n        Assert.Equal(\"\\n\", result);\n    }\n\n    [Fact]\n    public void DetectsNewLineFromMdFiles_Crlf()\n    {\n        var directory = new TempDirectory();\n        var mdFile = Path.Combine(directory, \"test.md\");\n        File.WriteAllText(mdFile, \"line1\\r\\nline2\\r\\n\");\n        var result = NewLineConfigReader.ReadNewLine(directory, [mdFile]);\n        Assert.Equal(\"\\r\\n\", result);\n    }\n\n    [Fact]\n    public void ConfigTakesPriorityOverMdFileDetection()\n    {\n        var directory = new TempDirectory();\n        File.WriteAllText(Path.Combine(directory, \".gitattributes\"), \"* text eol=lf\");\n        var mdFile = Path.Combine(directory, \"test.md\");\n        File.WriteAllText(mdFile, \"line1\\r\\nline2\\r\\n\");\n        var result = NewLineConfigReader.ReadNewLine(directory, [mdFile]);\n        Assert.Equal(\"\\n\", result);\n    }\n\n    [Fact]\n    public void MdFileDetection_PicksShortestFileFirst()\n    {\n        var directory = new TempDirectory();\n        var shortFile = Path.Combine(directory, \"a.md\");\n        var longFile = Path.Combine(directory, \"longer-name.md\");\n        File.WriteAllText(shortFile, \"line1\\nline2\\n\");\n        File.WriteAllText(longFile, \"line1\\r\\nline2\\r\\n\");\n        var result = NewLineConfigReader.ReadNewLine(directory, [longFile, shortFile]);\n        Assert.Equal(\"\\n\", result);\n    }\n\n    [Fact]\n    public void MdFileDetection_SkipsFilesWithNoNewlines()\n    {\n        var directory = new TempDirectory();\n        var noNewlineFile = Path.Combine(directory, \"a.md\");\n        var withNewlineFile = Path.Combine(directory, \"bb.md\");\n        File.WriteAllText(noNewlineFile, \"no newlines here\");\n        File.WriteAllText(withNewlineFile, \"has\\nnewlines\");\n        var result = NewLineConfigReader.ReadNewLine(directory, [noNewlineFile, withNewlineFile]);\n        Assert.Equal(\"\\n\", result);\n    }\n}"
  },
  {
    "path": "src/Tests/PathsTests.cs",
    "content": "public class PathsTests\n{\n    [Theory]\n    [InlineData(\"file.md\", true)]\n    [InlineData(\"file.mdx\", true)]\n    [InlineData(\"file.MD\", true)]\n    [InlineData(\"file.MDX\", true)]\n    [InlineData(\"file.Md\", true)]\n    [InlineData(\"file.txt\", false)]\n    [InlineData(\"file.mdxx\", false)]\n    public void IsMdFile(string value, bool expected) =>\n        Assert.Equal(expected, value.IsMdFile());\n\n    [Theory]\n    [InlineData(\"file.source.md\", true)]\n    [InlineData(\"file.source.mdx\", true)]\n    [InlineData(\"file.SOURCE.MD\", true)]\n    [InlineData(\"file.Source.Mdx\", true)]\n    [InlineData(\"file.md\", false)]\n    [InlineData(\"file.mdx\", false)]\n    public void IsSourceMdFile(string value, bool expected) =>\n        Assert.Equal(expected, value.IsSourceMdFile());\n\n    [Theory]\n    [InlineData(\"file.include.md\", true)]\n    [InlineData(\"file.INCLUDE.MD\", true)]\n    [InlineData(\"file.Include.Md\", true)]\n    [InlineData(\"file.include.mdx\", false)]\n    [InlineData(\"file.md\", false)]\n    public void IsIncludeMdFile(string value, bool expected) =>\n        Assert.Equal(expected, value.IsIncludeMdFile());\n}\n"
  },
  {
    "path": "src/Tests/ProcessResultConverter.cs",
    "content": "class ProcessResultConverter :\n    JsonConverter\n{\n    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)\n    {\n        var processResult = (ProcessResult)value;\n        writer.WriteStartObject();\n        writer.WritePropertyName(\"missing\");\n        serializer.Serialize(writer, processResult.MissingSnippets);\n        writer.WritePropertyName(\"usedSnippets\");\n        serializer.Serialize(writer, processResult.UsedSnippets);\n        writer.WriteEndObject();\n    }\n\n    public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) =>\n        throw new NotImplementedException();\n\n    public override bool CanConvert(Type objectType) =>\n        objectType == typeof(ProcessResult);\n}"
  },
  {
    "path": "src/Tests/SimpleSnippetMarkdownHandlingTests.Append.verified.txt",
    "content": "﻿```thelanguage\ntheValue\n```\n"
  },
  {
    "path": "src/Tests/SimpleSnippetMarkdownHandlingTests.ExpressiveCode.verified.txt",
    "content": "﻿```cs title=\"HelloWorld.cs\" {1}\nConsole.WriteLine(\"Hello World\");\n```\n"
  },
  {
    "path": "src/Tests/SimpleSnippetMarkdownHandlingTests.cs",
    "content": "﻿public class SimpleSnippetMarkdownHandlingTests\n{\n    [Fact]\n    public Task Append()\n    {\n        var builder = new StringBuilder();\n        var snippets = new List<Snippet> {Snippet.Build(1, 2, \"theValue\", \"thekey\", \"thelanguage\", \"thePath\", null)};\n        using (var writer = new StringWriter(builder))\n        {\n            SimpleSnippetMarkdownHandling.Append(\"key1\", snippets, writer.WriteLine);\n        }\n\n        return Verify(builder.ToString());\n    }\n\n    [Fact]\n    public Task ExpressiveCode()\n    {\n        var builder = new StringBuilder();\n        var snippets = new List<Snippet> {Snippet.Build(1, 2, \"\"\"Console.WriteLine(\"Hello World\");\"\"\", \"thekey\", \"cs\", \"thePath\", \"\"\"title=\"HelloWorld.cs\" {1}\"\"\")};\n        using (var writer = new StringWriter(builder))\n        {\n            SimpleSnippetMarkdownHandling.Append(\"key1\", snippets, writer.WriteLine);\n        }\n\n        return Verify(builder.ToString());\n    }\n}"
  },
  {
    "path": "src/Tests/SnippetConverter.cs",
    "content": "class SnippetConverter :\n    JsonConverter\n{\n    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)\n    {\n        var snippet = (Snippet)value;\n        writer.WriteStartObject();\n        writer.WritePropertyName(\"Key\");\n        serializer.Serialize(writer, snippet.Key);\n        if (!snippet.IsInError)\n        {\n            writer.WritePropertyName(\"Language\");\n            serializer.Serialize(writer, snippet.Language);\n            writer.WritePropertyName(\"Value\");\n            serializer.Serialize(writer, snippet.Value);\n        }\n        writer.WritePropertyName(\"Error\");\n        serializer.Serialize(writer, snippet.Error);\n        writer.WritePropertyName(\"FileLocation\");\n        serializer.Serialize(writer, snippet.FileLocation);\n        writer.WritePropertyName(\"IsInError\");\n        serializer.Serialize(writer, snippet.IsInError);\n        writer.WriteEndObject();\n    }\n\n    public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) =>\n        throw new NotImplementedException();\n\n    public override bool CanConvert(Type objectType) =>\n        objectType == typeof(Snippet);\n}"
  },
  {
    "path": "src/Tests/SnippetExtensionsTests.ToDictionary.verified.txt",
    "content": "﻿{\n  snippet1: [\n    {\n      Key: snippet1,\n      Language: language,\n      Value: Snippet,\n      Error: ,\n      FileLocation: thePath(1-2),\n      IsInError: false\n    }\n  ],\n  snippet2: [\n    {\n      Key: snippet2,\n      Language: language,\n      Value: Snippet,\n      Error: ,\n      FileLocation: thePath(1-2),\n      IsInError: false\n    }\n  ]\n}"
  },
  {
    "path": "src/Tests/SnippetExtensionsTests.ToDictionary_SameKey.verified.txt",
    "content": "﻿{\n  snippet1: [\n    {\n      Key: snippet1,\n      Language: language,\n      Value: Snippet,\n      Error: ,\n      FileLocation: null,\n      IsInError: false\n    },\n    {\n      Key: snippet1,\n      Language: language,\n      Value: Snippet,\n      Error: ,\n      FileLocation: thePath1(1-2),\n      IsInError: false\n    },\n    {\n      Key: snippet1,\n      Language: language,\n      Value: Snippet,\n      Error: ,\n      FileLocation: thePath2(1-2),\n      IsInError: false\n    }\n  ]\n}"
  },
  {
    "path": "src/Tests/SnippetExtensionsTests.cs",
    "content": "﻿public class SnippetExtensionsTests\n{\n    [Fact]\n    public Task ToDictionary()\n    {\n        var snippets = new List<Snippet>\n        {\n            SnippetBuild(\"snippet1\", \"thePath\"),\n            SnippetBuild(\"snippet2\", \"thePath\")\n        };\n        return Verify(snippets.ToDictionary());\n    }\n\n    [Fact]\n    public Task ToDictionary_SameKey()\n    {\n        var snippets = new List<Snippet>\n        {\n            SnippetBuild(\"snippet1\", null),\n            SnippetBuild(\"snippet1\", \"thePath2\"),\n            SnippetBuild(\"snippet1\", \"thePath1\")\n        };\n        return Verify(snippets.ToDictionary());\n    }\n\n    static Snippet SnippetBuild(string key, string? path) =>\n        Snippet.Build(\n            language: \"language\",\n            startLine: 1,\n            endLine: 2,\n            value: \"Snippet\",\n            key: key,\n            path: path,\n            expressiveCode: null);\n}"
  },
  {
    "path": "src/Tests/SnippetExtractor/SnippetExtractorTests.AppendFileAsSnippet.verified.txt",
    "content": "﻿[\n  {\n    Key: File.tmp,\n    Language: tmp,\n    Value: Foo,\n    Error: ,\n    FileLocation: FilePath.txt(1-1),\n    IsInError: false\n  }\n]"
  },
  {
    "path": "src/Tests/SnippetExtractor/SnippetExtractorTests.AppendUrlAsSnippet.verified.txt",
    "content": "﻿[\n  {\n    Key: appveyor.yml,\n    Language: yml,\n    Value:\nimage:\n- Visual Studio 2022\n#- macOS\nenvironment:\n  DOTNET_NOLOGO: true\n  DOTNET_CLI_TELEMETRY_OPTOUT: true\n  DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true\nbuild_script:\n- pwsh: |\n    if ($isWindows) {\n      Invoke-WebRequest \"https://dot.net/v1/dotnet-install.ps1\" -OutFile \"./dotnet-install.ps1\"\n      ./dotnet-install.ps1 -JSonFile src/global.json -Architecture x64 -InstallDir 'C:/Program Files/dotnet'\n    }\n    else {\n      Invoke-WebRequest \"https://dot.net/v1/dotnet-install.sh\" -OutFile \"./dotnet-install.sh\"\n      sudo chmod u+x dotnet-install.sh\n      sudo ./dotnet-install.sh --jsonfile src/global.json --architecture x64 --install-dir '/usr/local/share/dotnet'\n      sudo ./dotnet-install.sh --version 9.0.306 --architecture x64 --install-dir '/usr/local/share/dotnet'\n    }\n- dotnet build src --configuration Release\n- dotnet test src --configuration Release --no-build --no-restore\ntest: off\non_failure:\n  - ps: Get-ChildItem *.received.* -recurse | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name }\nartifacts:\n- path: nugets/*.nupkg,\n    Error: ,\n    FileLocation: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/src/appveyor.yml(1-26),\n    IsInError: false\n  }\n]"
  },
  {
    "path": "src/Tests/SnippetExtractor/SnippetExtractorTests.AppendUrlAsSnippetInline.verified.txt",
    "content": "﻿[\n  {\n    Key: usage.cs,\n    Language: cs,\n    Value:\n// ReSharper disable UnusedVariable\n\nclass Usage\n{\n    static void ReadingFiles()\n    {\n\n        var files = Directory.EnumerateFiles(@\"C:/path\", \"*.cs\", SearchOption.AllDirectories);\n\n        var snippets = FileSnippetExtractor.Read(files);\n\n    }\n\n    static void DirectoryMarkdownProcessorRun()\n    {\n\n        var processor = new DirectoryMarkdownProcessor(\n            \"targetDirectory\",\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true);\n        processor.Run();\n\n    }\n\n    static void DirectoryMarkdownProcessorRunMaxWidth()\n    {\n\n        var processor = new DirectoryMarkdownProcessor(\n            \"targetDirectory\",\n            maxWidth: 80,\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true);\n        processor.Run();\n\n    }\n},\n    Error: ,\n    FileLocation: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/src/Tests/Snippets/Usage.cs(1-45),\n    IsInError: false\n  },\n  {\n    Key: ReadingFilesSimple,\n    Language: cs,\n    Value:\nvar files = Directory.EnumerateFiles(@\"C:/path\", \"*.cs\", SearchOption.AllDirectories);\n\nvar snippets = FileSnippetExtractor.Read(files);,\n    Error: ,\n    FileLocation: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/src/Tests/Snippets/Usage.cs(8-14),\n    IsInError: false\n  },\n  {\n    Key: DirectoryMarkdownProcessorRun,\n    Language: cs,\n    Value:\nvar processor = new DirectoryMarkdownProcessor(\n    \"targetDirectory\",\n    directoryIncludes: _ => true,\n    markdownDirectoryIncludes: _ => true,\n    snippetDirectoryIncludes: _ => true);\nprocessor.Run();,\n    Error: ,\n    FileLocation: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/src/Tests/Snippets/Usage.cs(19-28),\n    IsInError: false\n  },\n  {\n    Key: DirectoryMarkdownProcessorRunMaxWidth,\n    Language: cs,\n    Value:\nvar processor = new DirectoryMarkdownProcessor(\n    \"targetDirectory\",\n    maxWidth: 80,\n    directoryIncludes: _ => true,\n    markdownDirectoryIncludes: _ => true,\n    snippetDirectoryIncludes: _ => true);\nprocessor.Run();,\n    Error: ,\n    FileLocation: https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/src/Tests/Snippets/Usage.cs(33-43),\n    IsInError: false\n  }\n]"
  },
  {
    "path": "src/Tests/SnippetExtractor/SnippetExtractorTests.CanExtractFromRegion.verified.txt",
    "content": "﻿[\n  {\n    Key: CodeKey,\n    Language: cs,\n    Value: The Code,\n    Error: ,\n    FileLocation: path.cs(2-4),\n    IsInError: false\n  }\n]"
  },
  {
    "path": "src/Tests/SnippetExtractor/SnippetExtractorTests.CanExtractFromXml.verified.txt",
    "content": "﻿[\n  {\n    Key: CodeKey,\n    Language: cs,\n    Value: <configSections/>,\n    Error: ,\n    FileLocation: path.cs(1-3),\n    IsInError: false\n  }\n]"
  },
  {
    "path": "src/Tests/SnippetExtractor/SnippetExtractorTests.CanExtractWithExpressiveCode.verified.txt",
    "content": "﻿[\n  {\n    Key: CodeKey,\n    Language: cs,\n    Value: Console.WriteLine(\"Hello World\");,\n    Error: ,\n    FileLocation: path.cs(1-3),\n    IsInError: false\n  }\n]"
  },
  {
    "path": "src/Tests/SnippetExtractor/SnippetExtractorTests.CanExtractWithInnerWhiteSpace.verified.txt",
    "content": "﻿[\n  {\n    Key: CodeKey,\n    Language: cs,\n    Value:\nBeforeWhiteSpace\n\nAfterWhiteSpace,\n    Error: ,\n    FileLocation: path.cs(2-8),\n    IsInError: false\n  }\n]"
  },
  {
    "path": "src/Tests/SnippetExtractor/SnippetExtractorTests.CanExtractWithMissingSpaces.verified.txt",
    "content": "﻿[\n  {\n    Key: CodeKey,\n    Language: cs,\n    Value: <configSections/>,\n    Error: ,\n    FileLocation: path.cs(2-4),\n    IsInError: false\n  }\n]"
  },
  {
    "path": "src/Tests/SnippetExtractor/SnippetExtractorTests.CanExtractWithNoTrailingCharacters.verified.txt",
    "content": "﻿[\n  {\n    Key: CodeKey,\n    Language: cs,\n    Value: the code,\n    Error: ,\n    FileLocation: path.cs(2-4),\n    IsInError: false\n  }\n]"
  },
  {
    "path": "src/Tests/SnippetExtractor/SnippetExtractorTests.CanExtractWithTrailingWhitespace.verified.txt",
    "content": "﻿[\n  {\n    Key: CodeKey,\n    Language: cs,\n    Value: the code,\n    Error: ,\n    FileLocation: path.cs(2-4),\n    IsInError: false\n  }\n]"
  },
  {
    "path": "src/Tests/SnippetExtractor/SnippetExtractorTests.CanReadFileWhileLockedByAnotherProcess.verified.txt",
    "content": "[\n  {\n    Key: CodeKey,\n    Language: cs,\n    Value: The Code,\n    Error: ,\n    FileLocation: LockedFile.cs(1-3),\n    IsInError: false\n  }\n]"
  },
  {
    "path": "src/Tests/SnippetExtractor/SnippetExtractorTests.LanguageOverride.verified.txt",
    "content": "﻿[\n  {\n    Key: CodeKey,\n    Language: json,\n    Value: {\"a\": 1},\n    Error: ,\n    FileLocation: path.cs(1-3),\n    IsInError: false\n  }\n]"
  },
  {
    "path": "src/Tests/SnippetExtractor/SnippetExtractorTests.LanguageOverrideWithExpressiveCode.verified.txt",
    "content": "﻿[\n  {\n    Key: CodeKey,\n    Language: json,\n    Value: {\"a\": 1},\n    Error: ,\n    FileLocation: path.cs(1-3),\n    IsInError: false\n  }\n]"
  },
  {
    "path": "src/Tests/SnippetExtractor/SnippetExtractorTests.MixedNewLines.verified.txt",
    "content": "﻿{\n  Key: CodeKey,\n  Language: cs,\n  Value:\nA\nB\nC\nD,\n  Error: ,\n  FileLocation: path.cs(1-6),\n  IsInError: false\n}"
  },
  {
    "path": "src/Tests/SnippetExtractor/SnippetExtractorTests.NestedBroken.verified.txt",
    "content": "﻿[\n  {\n    Key: KeyChild,\n    Language: cs,\n    Value:\nb\nc,\n    Error: ,\n    FileLocation: path.cs(4-7),\n    IsInError: false\n  },\n  {\n    Key: KeyParent,\n    Error: Snippet was not closed,\n    FileLocation: path.cs(3-3),\n    IsInError: true\n  }\n]"
  },
  {
    "path": "src/Tests/SnippetExtractor/SnippetExtractorTests.NestedMixed1.verified.txt",
    "content": "﻿[\n  {\n    Key: KeyChild,\n    Language: cs,\n    Value: b,\n    Error: ,\n    FileLocation: path.cs(3-5),\n    IsInError: false\n  },\n  {\n    Key: KeyParent,\n    Language: cs,\n    Value:\na\nb\nc,\n    Error: ,\n    FileLocation: path.cs(1-7),\n    IsInError: false\n  }\n]"
  },
  {
    "path": "src/Tests/SnippetExtractor/SnippetExtractorTests.NestedMixed2.verified.txt",
    "content": "﻿[\n  {\n    Key: KeyChild,\n    Language: cs,\n    Value: b,\n    Error: ,\n    FileLocation: path.cs(3-5),\n    IsInError: false\n  },\n  {\n    Key: KeyParent,\n    Language: cs,\n    Value:\na\nb\nc,\n    Error: ,\n    FileLocation: path.cs(1-7),\n    IsInError: false\n  }\n]"
  },
  {
    "path": "src/Tests/SnippetExtractor/SnippetExtractorTests.NestedRegion.verified.txt",
    "content": "﻿[\n  {\n    Key: KeyChild,\n    Language: cs,\n    Value: b,\n    Error: ,\n    FileLocation: path.cs(4-6),\n    IsInError: false\n  },\n  {\n    Key: KeyParent,\n    Language: cs,\n    Value:\na\nb\nc,\n    Error: ,\n    FileLocation: path.cs(2-8),\n    IsInError: false\n  }\n]"
  },
  {
    "path": "src/Tests/SnippetExtractor/SnippetExtractorTests.NestedStartCode.verified.txt",
    "content": "﻿[\n  {\n    Key: KeyChild,\n    Language: cs,\n    Value: b,\n    Error: ,\n    FileLocation: path.cs(3-5),\n    IsInError: false\n  },\n  {\n    Key: KeyParent,\n    Language: cs,\n    Value:\na\nb\nc,\n    Error: ,\n    FileLocation: path.cs(1-7),\n    IsInError: false\n  }\n]"
  },
  {
    "path": "src/Tests/SnippetExtractor/SnippetExtractorTests.RemoveDuplicateNewlines.verified.txt",
    "content": "﻿[\n  {\n    Key: KeyChild,\n    Language: cs,\n    Value: b,\n    Error: ,\n    FileLocation: path.cs(8-14),\n    IsInError: false\n  },\n  {\n    Key: KeyParent,\n    Language: cs,\n    Value:\na\n\nb\n\nc,\n    Error: ,\n    FileLocation: path.cs(2-20),\n    IsInError: false\n  }\n]"
  },
  {
    "path": "src/Tests/SnippetExtractor/SnippetExtractorTests.TooWide.verified.txt",
    "content": "﻿[\n  {\n    Key: CodeKey,\n    Error: Line too long: caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab,\n    FileLocation: path.cs(3-3),\n    IsInError: true\n  }\n]"
  },
  {
    "path": "src/Tests/SnippetExtractor/SnippetExtractorTests.UnClosedRegion.verified.txt",
    "content": "﻿[\n  {\n    Key: CodeKey,\n    Error: Snippet was not closed,\n    FileLocation: path.cs(3-3),\n    IsInError: true\n  }\n]"
  },
  {
    "path": "src/Tests/SnippetExtractor/SnippetExtractorTests.UnClosedSnippet.verified.txt",
    "content": "﻿[\n  {\n    Key: CodeKey,\n    Error: Snippet was not closed,\n    FileLocation: path.cs(2-2),\n    IsInError: true\n  }\n]"
  },
  {
    "path": "src/Tests/SnippetExtractor/SnippetExtractorTests.cs",
    "content": "﻿public class SnippetExtractorTests\n{\n    [Fact]\n    public async Task AppendUrlAsSnippet()\n    {\n        var snippets = new List<Snippet>();\n        await snippets.AppendUrlAsSnippet(\"https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/src/appveyor.yml\");\n        await Verify(snippets);\n    }\n\n    [Fact]\n    public async Task AppendUrlAsSnippetInline()\n    {\n        var snippets = new List<Snippet>();\n        await snippets.AppendUrlAsSnippet(\"https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/src/Tests/Snippets/Usage.cs\");\n        await Verify(snippets).ScrubLinesContaining(\"#region\", \"#endregion\");\n    }\n\n    [Fact]\n    public async Task AppendFileAsSnippet()\n    {\n        var temp = Path.GetTempFileName().ToLowerInvariant();\n        try\n        {\n            await File.WriteAllTextAsync(temp, \"Foo\");\n            var snippets = new List<Snippet>();\n            snippets.AppendFileAsSnippet(temp);\n            await Verify(snippets)\n                .AddScrubber(_ =>\n                {\n                    var nameWithoutExtension = Path.GetFileNameWithoutExtension(temp);\n                    _.Replace(temp, \"FilePath.txt\");\n                    _.Replace(nameWithoutExtension, \"File\");\n                });\n        }\n        finally\n        {\n            File.Delete(temp);\n        }\n    }\n\n    [Fact]\n    public Task CanReadFileWhileLockedByAnotherProcess()\n    {\n        var temp = Path.Combine(Path.GetTempPath(), \"LockedSnippetFile.cs\");\n        try\n        {\n            File.WriteAllText(temp,\n                \"\"\"\n                #region CodeKey\n                The Code\n                #endregion\n                \"\"\");\n            using var lockingStream = new FileStream(temp, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);\n            var snippets = FileSnippetExtractor.Read(temp);\n            return Verify(snippets)\n                .AddScrubber(_ => _.Replace(temp, \"LockedFile.cs\"));\n        }\n        finally\n        {\n            File.Delete(temp);\n        }\n    }\n\n    [Fact]\n    public Task CanExtractWithInnerWhiteSpace()\n    {\n        var input = \"\"\"\n\n                      #region CodeKey\n\n                      BeforeWhiteSpace\n\n                      AfterWhiteSpace\n\n                      #endregion\n                    \"\"\";\n        var snippets = FromText(input);\n        return Verify(snippets);\n    }\n\n    [Fact]\n    public Task NestedBroken()\n    {\n        var input = \"\"\"\n\n                      #region KeyParent\n                      a\n                      #region KeyChild\n                      b\n                      c\n                      #endregion\n                    \"\"\";\n        var snippets = FromText(input);\n        return Verify(snippets);\n    }\n\n    [Fact]\n    public Task NestedRegion()\n    {\n        var input = \"\"\"\n\n                      #region KeyParent\n                      a\n                      #region KeyChild\n                      b\n                      #endregion\n                      c\n                      #endregion\n                    \"\"\";\n        var snippets = FromText(input);\n        return Verify(snippets);\n    }\n\n    [Fact]\n    public Task NestedMixed2()\n    {\n        var input = \"\"\"\n                    #region KeyParent\n                    a\n                    <!-- begin-snippet: KeyChild -->\n                    b\n                    <!-- end-snippet -->\n                    c\n                    #endregion\n                    \"\"\";\n        var snippets = FromText(input);\n        return Verify(snippets);\n    }\n\n    [Fact]\n    public Task RemoveDuplicateNewlines()\n    {\n        var input = \"\"\"\n\n                    <!-- begin-snippet: KeyParent -->\n\n\n                    a\n\n\n                    <!-- begin-snippet: KeyChild -->\n\n\n                    b\n\n\n                    <!-- end-snippet -->\n\n\n                    c\n\n\n                    <!-- end-snippet -->\n\n\n\n                    \"\"\";\n        var snippets = FromText(input);\n        return Verify(snippets);\n    }\n\n    [Fact]\n    public Task NestedStartCode()\n    {\n        var input = \"\"\"\n                    <!-- begin-snippet: KeyParent -->\n                    a\n                    <!-- begin-snippet: KeyChild -->\n                    b\n                    <!-- end-snippet -->\n                    c\n                    <!-- end-snippet -->\n                    \"\"\";\n        var snippets = FromText(input);\n        return Verify(snippets);\n    }\n\n    [Fact]\n    public Task NestedMixed1()\n    {\n        var input = \"\"\"\n                    <!-- begin-snippet: KeyParent -->\n                    a\n                    #region KeyChild\n                    b\n                    #endregion\n                    c\n                    <!-- end-snippet -->\n                    \"\"\";\n        var snippets = FromText(input);\n        return Verify(snippets);\n    }\n\n    [Fact]\n    public Task CanExtractFromXml()\n    {\n        var input = \"\"\"\n                    <!-- begin-snippet: CodeKey -->\n                    <configSections/>\n                    <!-- end-snippet -->\n                    \"\"\";\n        var snippets = FromText(input);\n        return Verify(snippets);\n    }\n\n    [Fact]\n    public Task LanguageOverride()\n    {\n        var input = \"\"\"\n                    <!-- begin-snippet: CodeKey (lang=json) -->\n                    {\"a\": 1}\n                    <!-- end-snippet -->\n                    \"\"\";\n        var snippets = FromText(input);\n        return Verify(snippets);\n    }\n\n    [Fact]\n    public Task LanguageOverrideWithExpressiveCode()\n    {\n        var input = \"\"\"\n                    <!-- begin-snippet: CodeKey (lang=json title=\"config.json\") -->\n                    {\"a\": 1}\n                    <!-- end-snippet -->\n                    \"\"\";\n        var snippets = FromText(input);\n        return Verify(snippets);\n    }\n\n    static List<Snippet> FromText(string contents)\n    {\n        using var reader = new StringReader(contents);\n        return FileSnippetExtractor.Read(reader, \"path.cs\", 80).ToList();\n    }\n\n    [Fact]\n    public Task UnClosedSnippet()\n    {\n        var input = \"\"\"\n                    <!-- begin-snippet: CodeKey -->\n                    <configSections/>\n                    \"\"\";\n        var snippets = FromText(input);\n        return Verify(snippets);\n    }\n\n    [Fact]\n    public Task UnClosedRegion()\n    {\n        var input = \"\"\"\n\n                      #region CodeKey\n                      <configSections/>\n                    \"\"\";\n        var snippets = FromText(input);\n        return Verify(snippets);\n    }\n\n    [Fact]\n    public Task TooWide()\n    {\n        var input = \"\"\"\n\n                      #region CodeKey\n                      caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab\n                      #endregion\n                    \"\"\";\n        var snippets = FromText(input);\n        return Verify(snippets);\n    }\n\n    [Fact]\n    public Task MixedNewLines()\n    {\n        var input = \"#region CodeKey\\r  A\\r\\n  B\\r  C\\n  D\\n  #endregion\";\n        var snippets = FromText(input);\n        var single = snippets.Single();\n        var value = single.Value;\n        Assert.DoesNotContain(\"\\r\\n\", value);\n        Assert.DoesNotContain(\"\\r\", value);\n        return Verify(single);\n    }\n\n    [Fact]\n    public Task CanExtractFromRegion()\n    {\n        var input = \"\"\"\n\n                      #region CodeKey\n                      The Code\n                      #endregion\n                    \"\"\";\n        var snippets = FromText(input);\n        return Verify(snippets);\n    }\n\n    [Fact]\n    public Task CanExtractWithNoTrailingCharacters()\n    {\n        var input = \"\"\"\n\n                      // begin-snippet: CodeKey\n                      the code\n                      // end-snippet\n                    \"\"\";\n        var snippets = FromText(input);\n        return Verify(snippets);\n    }\n\n    [Fact]\n    public Task CanExtractWithMissingSpaces()\n    {\n        var input = \"\"\"\n\n                      <!--begin-snippet: CodeKey-->\n                      <configSections/>\n                      <!--end-snippet-->\n                    \"\"\";\n        var snippets = FromText(input);\n        return Verify(snippets);\n    }\n\n    [Fact]\n    public Task CanExtractWithTrailingWhitespace()\n    {\n        var input = \"\"\"\n\n                      // begin-snippet: CodeKey\n                      the code\n                      // end-snippet\n                    \"\"\";\n        var snippets = FromText(input);\n        return Verify(snippets);\n    }\n\n    [Fact]\n    public Task CanExtractWithExpressiveCode()\n    {\n        var input = \"\"\"\n                      <!--begin-snippet: CodeKey(title=\"Program.cs\" {1-3})-->\n                      Console.WriteLine(\"Hello World\");\n                      <!--end-snippet-->\n                    \"\"\";\n        var snippets = FromText(input);\n        return Verify(snippets);\n    }\n}"
  },
  {
    "path": "src/Tests/SnippetFileFinder/Nested/nested/nested/code.txt",
    "content": "﻿begin-snippet: nestedsnippet\nSome code\nend-snippet"
  },
  {
    "path": "src/Tests/SnippetFileFinder/Simple/code1.txt",
    "content": "﻿begin-snippet: snippet2\nSome code\nend-snippet"
  },
  {
    "path": "src/Tests/SnippetFileFinder/Simple/code2.txt",
    "content": "﻿begin-snippet: snippet1\nSome code\nend-snippet"
  },
  {
    "path": "src/Tests/SnippetFileFinder/Simple/code3.txt",
    "content": "﻿begin-snippet: snippet2\nSome code\nend-snippet"
  },
  {
    "path": "src/Tests/SnippetFileFinder/Simple/code4.txt",
    "content": "﻿begin-snippet: snippet1\nSome code\nend-snippet"
  },
  {
    "path": "src/Tests/SnippetFileFinder/VerifyLambdasAreCalled/subpath/code4.txt",
    "content": "﻿begin-snippet: snippet1\nSome code\nend-snippet"
  },
  {
    "path": "src/Tests/SnippetFileFinderTests.ExcludeSnippetFiles.verified.txt",
    "content": "[\n  {ProjectDirectory}SnippetFileFinder/Simple/code1.txt,\n  {ProjectDirectory}SnippetFileFinder/Simple/code3.txt\n]"
  },
  {
    "path": "src/Tests/SnippetFileFinderTests.Nested.verified.txt",
    "content": "﻿[\n  {ProjectDirectory}SnippetFileFinder/Nested/nested/nested/code.txt\n]"
  },
  {
    "path": "src/Tests/SnippetFileFinderTests.Simple.verified.txt",
    "content": "﻿[\n  {ProjectDirectory}SnippetFileFinder/Simple/code1.txt,\n  {ProjectDirectory}SnippetFileFinder/Simple/code2.txt,\n  {ProjectDirectory}SnippetFileFinder/Simple/code3.txt,\n  {ProjectDirectory}SnippetFileFinder/Simple/code4.txt\n]"
  },
  {
    "path": "src/Tests/SnippetFileFinderTests.VerifyLambdasAreCalled.verified.txt",
    "content": "﻿{\n  directories: [\n    {ProjectDirectory}SnippetFileFinder/VerifyLambdasAreCalled/subpath\n  ],\n  markdownDirectories: [\n    {ProjectDirectory}SnippetFileFinder/VerifyLambdasAreCalled,\n    {ProjectDirectory}SnippetFileFinder/VerifyLambdasAreCalled/subpath\n  ],\n  snippetDirectories: [\n    {ProjectDirectory}SnippetFileFinder/VerifyLambdasAreCalled,\n    {ProjectDirectory}SnippetFileFinder/VerifyLambdasAreCalled/subpath\n  ]\n}"
  },
  {
    "path": "src/Tests/SnippetFileFinderTests.cs",
    "content": "﻿// ReSharper disable UnusedVariable\n\npublic class SnippetFileFinderTests\n{\n    [Fact]\n    public void Hidden()\n    {\n        var path = Path.Combine(Path.GetTempPath(), \"mdsnippetsHidden\");\n        var directory = new DirectoryInfo(path);\n        directory.Create();\n        directory.Attributes = FileAttributes.Directory | FileAttributes.Hidden;\n        Assert.True(DefaultDirectoryExclusions.ShouldExcludeDirectory(path));\n    }\n\n    [Fact]\n    public Task Nested()\n    {\n        var directory = Path.Combine(ProjectFiles.ProjectDirectory, \"SnippetFileFinder/Nested\");\n        var finder = new FileFinder(directory, DocumentConvention.SourceTransform, _ => true, _ => true, _ => true);\n        var files = finder.FindFiles();\n        return Verify(files.snippetFiles);\n    }\n\n    [Fact]\n    public Task Simple()\n    {\n        var directory = Path.Combine(ProjectFiles.ProjectDirectory, \"SnippetFileFinder/Simple\");\n        var finder = new FileFinder(directory, DocumentConvention.SourceTransform, _ => true, _ => true, _ => true);\n        var files = finder.FindFiles();\n        return Verify(files.snippetFiles);\n    }\n\n    [Fact]\n    public Task ExcludeSnippetFiles()\n    {\n        var directory = Path.Combine(ProjectFiles.ProjectDirectory, \"SnippetFileFinder/Simple\");\n        var finder = new FileFinder(\n            directory,\n            DocumentConvention.SourceTransform,\n            _ => true,\n            _ => true,\n            _ => true,\n            snippetFileIncludes: path =>\n            {\n                var name = Path.GetFileName(path);\n                return name != \"code2.txt\" && name != \"code4.txt\";\n            });\n        var files = finder.FindFiles();\n        return Verify(files.snippetFiles);\n    }\n\n    [Fact]\n    public Task VerifyLambdasAreCalled()\n    {\n        var directories = new ConcurrentBag<string>();\n        var snippetDirectories = new ConcurrentBag<string>();\n        var markdownDirectories = new ConcurrentBag<string>();\n        var directory = Path.Combine(ProjectFiles.ProjectDirectory, \"SnippetFileFinder/VerifyLambdasAreCalled\");\n        var finder = new FileFinder(\n            directory,\n            DocumentConvention.SourceTransform,\n            directoryIncludes: path =>\n            {\n                directories.Add(path);\n                return true;\n            },\n            markdownDirectoryIncludes: path =>\n            {\n                markdownDirectories.Add(path);\n                return true;\n            },\n            snippetDirectoryIncludes: path =>\n            {\n                snippetDirectories.Add(path);\n                return true;\n            });\n        var files = finder.FindFiles();\n        return Verify(new\n        {\n            directories = directories.OrderBy(_ => _),\n            markdownDirectories = markdownDirectories.OrderBy(_ => _),\n            snippetDirectories = snippetDirectories.OrderBy(_ => _),\n        });\n    }\n}"
  },
  {
    "path": "src/Tests/SnippetMarkdownHandlingTests.Append.verified.txt",
    "content": "﻿<a id='snippet-thekey'></a>\n```thelanguage\ntheValue\n```\n<sup><a href='#L1-L2' title='Snippet source file'>snippet source</a> | <a href='#snippet-thekey' title='Start of snippet'>anchor</a></sup>\n"
  },
  {
    "path": "src/Tests/SnippetMarkdownHandlingTests.AppendHashed.verified.txt",
    "content": "﻿<a id='snippet-thekey'></a>\n```thelanguage\ntheValue\n```\n<sup><a href='#L1-L2' title='Snippet source file'>snippet source</a> | <a href='#snippet-thekey' title='Start of snippet'>anchor</a></sup>\n"
  },
  {
    "path": "src/Tests/SnippetMarkdownHandlingTests.AppendOmitSnippetLinks.verified.txt",
    "content": "﻿```thelanguage\ntheValue\n```\n"
  },
  {
    "path": "src/Tests/SnippetMarkdownHandlingTests.AppendOmitSourceLink.verified.txt",
    "content": "﻿<a id='snippet-thekey'></a>\n```thelanguage\ntheValue\n```\n<sup><a href='#snippet-thekey' title='Start of snippet'>anchor</a></sup>\n"
  },
  {
    "path": "src/Tests/SnippetMarkdownHandlingTests.AppendPrefixed.verified.txt",
    "content": "﻿<a id='snippet-thekey'></a>\n```thelanguage\ntheValue\n```\n<sup><a href='prefix-#L1-L2' title='Snippet source file'>snippet source</a> | <a href='#snippet-thekey' title='Start of snippet'>anchor</a></sup>\n"
  },
  {
    "path": "src/Tests/SnippetMarkdownHandlingTests.AppendWebSnippet.verified.txt",
    "content": "﻿<a id='snippet-http://example.com/file.cs%23mysnippet'></a>\n```cs\ntheValue\n```\n<sup><a href='http://example.com/file.cs#mysnippet' title='Snippet source file'>anchor</a></sup>\n"
  },
  {
    "path": "src/Tests/SnippetMarkdownHandlingTests.AppendWebSnippetWithViewUrl.verified.txt",
    "content": "﻿<a id='snippet-http://example.com/raw/file.cs%23mysnippet'></a>\n```cs\ntheValue\n```\n<sup><a href='https://github.com/user/repo/blob/main/file.cs#L5-L10' title='Snippet source file'>anchor</a></sup>\n"
  },
  {
    "path": "src/Tests/SnippetMarkdownHandlingTests.cs",
    "content": "public class SnippetMarkdownHandlingTests\n{\n    [Fact]\n    public Task Append()\n    {\n        var builder = new StringBuilder();\n        var snippets = Snippets();\n        var markdownHandling = new SnippetMarkdownHandling(Environment.CurrentDirectory, LinkFormat.GitHub, false);\n        using (var writer = new StringWriter(builder))\n        {\n            markdownHandling.Append(\"key1\", snippets, writer.WriteLine);\n        }\n\n        return Verify(builder.ToString());\n    }\n\n    [Fact]\n    public Task AppendOmitSourceLink()\n    {\n        var builder = new StringBuilder();\n        var snippets = Snippets();\n        var markdownHandling = new SnippetMarkdownHandling(Environment.CurrentDirectory, LinkFormat.None, false);\n        using (var writer = new StringWriter(builder))\n        {\n            markdownHandling.Append(\"key1\", snippets, writer.WriteLine);\n        }\n\n        return Verify(builder.ToString());\n    }\n\n    [Fact]\n    public Task AppendOmitSnippetLinks()\n    {\n        var builder = new StringBuilder();\n        var snippets = Snippets();\n        var markdownHandling = new SnippetMarkdownHandling(Environment.CurrentDirectory, LinkFormat.GitHub, true);\n        using (var writer = new StringWriter(builder))\n        {\n            markdownHandling.Append(\"key1\", snippets, writer.WriteLine);\n        }\n\n        return Verify(builder.ToString());\n    }\n\n    [Fact]\n    public Task AppendPrefixed()\n    {\n        var builder = new StringBuilder();\n        var snippets = Snippets();\n        var markdownHandling = new SnippetMarkdownHandling(Environment.CurrentDirectory, LinkFormat.GitHub, false, \"prefix-\");\n        using (var writer = new StringWriter(builder))\n        {\n            markdownHandling.Append(\"key1\", snippets, writer.WriteLine);\n        }\n\n        return Verify(builder.ToString());\n    }\n\n    [Fact]\n    public Task AppendHashed()\n    {\n        var builder = new StringBuilder();\n        var snippets = Snippets();\n        var markdownHandling = new SnippetMarkdownHandling(Environment.CurrentDirectory, LinkFormat.GitHub, false);\n        using (var writer = new StringWriter(builder))\n        {\n            markdownHandling.Append(\"key1\", snippets, writer.WriteLine);\n        }\n\n        return Verify(builder.ToString());\n    }\n\n    [Fact]\n    public Task AppendWebSnippet()\n    {\n        var builder = new StringBuilder();\n        var webSnippet = Snippet.Build(\n            startLine: 1,\n            endLine: 2,\n            value: \"theValue\",\n            key: \"mysnippet\",\n            language: \"cs\",\n            path: \"http://example.com/file.cs\",\n            expressiveCode: null);\n        var markdownHandling = new SnippetMarkdownHandling(Environment.CurrentDirectory, LinkFormat.GitHub, false);\n        using (var writer = new StringWriter(builder))\n        {\n            markdownHandling.Append(\"key1\", new List<Snippet> { webSnippet }, writer.WriteLine);\n        }\n\n        return Verify(builder.ToString());\n    }\n\n    [Fact]\n    public Task AppendWebSnippetWithViewUrl()\n    {\n        var builder = new StringBuilder();\n        var webSnippet = Snippet.Build(\n            startLine: 5,\n            endLine: 10,\n            value: \"theValue\",\n            key: \"mysnippet\",\n            language: \"cs\",\n            path: \"http://example.com/raw/file.cs\",\n            expressiveCode: null,\n            viewUrl: \"https://github.com/user/repo/blob/main/file.cs\");\n        var markdownHandling = new SnippetMarkdownHandling(Environment.CurrentDirectory, LinkFormat.GitHub, false);\n        using (var writer = new StringWriter(builder))\n        {\n            markdownHandling.Append(\"key1\", new List<Snippet> { webSnippet }, writer.WriteLine);\n        }\n\n        return Verify(builder.ToString());\n    }\n\n    static List<Snippet> Snippets() =>\n        [Snippet.Build(1, 2, \"theValue\", \"thekey\", \"thelanguage\", Environment.CurrentDirectory, expressiveCode: null)];\n}"
  },
  {
    "path": "src/Tests/SnippetVerifier.cs",
    "content": "static class SnippetVerifier\n{\n    public static Task VerifyThrows(\n        DocumentConvention convention,\n        string markdownContent,\n        IReadOnlyList<Snippet>? snippets = null,\n        IReadOnlyList<string>? snippetSourceFiles = null,\n        IReadOnlyList<Include>? includes = null,\n        [CallerFilePath] string sourceFile = \"\")\n    {\n        var processor = BuildProcessor(convention, snippets, snippetSourceFiles, includes);\n        var builder = new StringBuilder();\n        using var reader = new StringReader(markdownContent);\n        using var writer = new StringWriter(builder);\n        return Throws(() => processor.Apply(reader, writer, \"sourceFile\"), null, sourceFile);\n    }\n\n    static MarkdownProcessor BuildProcessor(\n        DocumentConvention convention,\n        IReadOnlyList<Snippet>? snippets,\n        IReadOnlyList<string>? snippetSourceFiles,\n        IReadOnlyList<Include>? includes)\n    {\n        includes ??= [];\n        snippets ??= [];\n        snippetSourceFiles ??= [];\n        return new(\n            convention: convention,\n            snippets: snippets.ToDictionary(),\n            includes: includes,\n            appendSnippets: SimpleSnippetMarkdownHandling.Append,\n            snippetSourceFiles: snippetSourceFiles,\n            tocLevel: 2,\n            writeHeader: false,\n            targetDirectory: \"c:/root\",\n            validateContent: true,\n            allFiles: new List<string>());\n    }\n\n    public static async Task<string> Verify(\n        DocumentConvention convention,\n        string markdownContent,\n        List<Snippet>? snippets = null,\n        IReadOnlyList<string>? snippetSourceFiles = null,\n        IReadOnlyList<Include>? includes = null,\n        [CallerFilePath] string sourceFile = \"\")\n    {\n\n        var markdownProcessor = BuildProcessor(convention, snippets, snippetSourceFiles, includes);\n        var stringBuilder = new StringBuilder();\n        using var reader = new StringReader(markdownContent);\n        using var writer = new StringWriter(stringBuilder);\n        var processResult = markdownProcessor.Apply(reader, writer, \"sourceFile\");\n        var result = stringBuilder.ToString();\n        var output = new\n        {\n            processResult.MissingSnippets,\n            processResult.UsedSnippets,\n            result\n        };\n        await Verifier.Verify(output, null, sourceFile);\n        return result;\n    }\n}"
  },
  {
    "path": "src/Tests/Snippets/Usage.cs",
    "content": "﻿\n// ReSharper disable UnusedVariable\n\nclass Usage\n{\n    static void ReadingFiles()\n    {\n        #region ReadingFilesSimple\n\n        var files = Directory.EnumerateFiles(@\"C:\\path\", \"*.cs\", SearchOption.AllDirectories);\n\n        var snippets = FileSnippetExtractor.Read(files);\n\n        #endregion\n    }\n\n    static void DirectoryMarkdownProcessorRun()\n    {\n        #region DirectoryMarkdownProcessorRun\n\n        var processor = new DirectoryMarkdownProcessor(\n            \"targetDirectory\",\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true);\n        processor.Run();\n\n        #endregion\n    }\n\n    static void DirectoryMarkdownProcessorRunMaxWidth()\n    {\n        #region DirectoryMarkdownProcessorRunMaxWidth\n\n        var processor = new DirectoryMarkdownProcessor(\n            \"targetDirectory\",\n            maxWidth: 80,\n            directoryIncludes: _ => true,\n            markdownDirectoryIncludes: _ => true,\n            snippetDirectoryIncludes: _ => true);\n        processor.Run();\n\n        #endregion\n    }\n}"
  },
  {
    "path": "src/Tests/StartEndTester_IsBeginSnippetTests.ShouldThrowForEmptyLanguageValue.verified.txt",
    "content": "﻿{\n  Type: SnippetReadingException,\n  Message:\nlang= must have a value.\nKey: CodeKey\nPath: file\nLine: <!-- begin-snippet: CodeKey (lang=) -->\n}"
  },
  {
    "path": "src/Tests/StartEndTester_IsBeginSnippetTests.ShouldThrowForInvalidLanguageValue.verified.txt",
    "content": "﻿{\n  Type: SnippetReadingException,\n  Message:\nlang value must be lowercase alphanumeric.\nKey: CodeKey\nValue: C#\nPath: file\nLine: <!-- begin-snippet: CodeKey (lang=C#) -->\n}"
  },
  {
    "path": "src/Tests/StartEndTester_IsBeginSnippetTests.ShouldThrowForKeyEndingWithSymbol.verified.txt",
    "content": "﻿{\n  Type: SnippetReadingException,\n  Message:\nKey cannot contain whitespace or start/end with symbols.\nKey: key_\nPath: file\nLine: <!-- begin-snippet: key_ -->\n}"
  },
  {
    "path": "src/Tests/StartEndTester_IsBeginSnippetTests.ShouldThrowForKeyStartingWithSymbol.verified.txt",
    "content": "﻿{\n  Type: SnippetReadingException,\n  Message:\nKey cannot contain whitespace or start/end with symbols.\nKey: _key\nPath: file\nLine: <!-- begin-snippet: _key-->\n}"
  },
  {
    "path": "src/Tests/StartEndTester_IsBeginSnippetTests.ShouldThrowForNoKey.verified.txt",
    "content": "﻿{\n  Type: SnippetReadingException,\n  Message:\nNo Key could be derived.\nPath: file\nLine: '<!-- begin-snippet: -->'\n}"
  },
  {
    "path": "src/Tests/StartEndTester_IsBeginSnippetTests.cs",
    "content": "﻿public class StartEndTester_IsBeginSnippetTests\n{\n    [Fact]\n    public void CanExtractFromXml()\n    {\n        var isBeginSnippet = StartEndTester.IsBeginSnippet(\"<!-- begin-snippet: CodeKey -->\", \"file\", out var key, out _);\n        Assert.True(isBeginSnippet);\n        Assert.Equal(\"CodeKey\", key);\n    }\n\n    [Fact]\n    public Task ShouldThrowForNoKey() =>\n        Throws(() => StartEndTester.IsBeginSnippet(\"<!-- begin-snippet: -->\", \"file\", out _, out _));\n\n    [Fact]\n    public void ShouldNotThrowForNoKeyWithNoSpace() =>\n        StartEndTester.IsBeginSnippet(\"<!--begin-snippet:-->\", \"file\", out _, out _);\n\n    [Fact]\n    public void CanExtractFromXmlWithMissingSpaces()\n    {\n        var isBeginSnippet = StartEndTester.IsBeginSnippet(\"<!--begin-snippet: CodeKey-->\", \"file\", out var key, out _);\n        Assert.True(isBeginSnippet);\n        Assert.Equal(\"CodeKey\", key);\n    }\n\n    [Fact]\n    public void CanExtractFromXmlWithExtraSpaces()\n    {\n        var isBeginSnippet = StartEndTester.IsBeginSnippet(\"<!--  begin-snippet:  CodeKey  -->\", \"file\", out var key, out _);\n        Assert.True(isBeginSnippet);\n        Assert.Equal(\"CodeKey\", key);\n    }\n\n    [Fact]\n    public void CanExtractWithNoTrailingCharacters()\n    {\n        var isBeginSnippet = StartEndTester.IsBeginSnippet(\"<!-- begin-snippet: CodeKey\", \"file\", out var key, out _);\n        Assert.True(isBeginSnippet);\n        Assert.Equal(\"CodeKey\", key);\n    }\n\n    [Fact]\n    public void CanExtractWithUnderScores()\n    {\n        var isBeginSnippet = StartEndTester.IsBeginSnippet(\"<!-- begin-snippet: Code_Key -->\", \"file\", out var key, out _);\n        Assert.True(isBeginSnippet);\n        Assert.Equal(\"Code_Key\", key);\n    }\n\n    [Fact]\n    public void CanExtractWithDashes()\n    {\n        var isBeginSnippet = StartEndTester.IsBeginSnippet(\"<!-- begin-snippet: Code-Key -->\", \"file\", out var key, out _);\n        Assert.True(isBeginSnippet);\n        Assert.Equal(\"Code-Key\", key);\n    }\n\n    [Fact]\n    public Task ShouldThrowForKeyStartingWithSymbol() =>\n        Throws(() =>\n            StartEndTester.IsBeginSnippet(\"<!-- begin-snippet: _key-->\", \"file\", out _, out _));\n\n    [Fact]\n    public Task ShouldThrowForKeyEndingWithSymbol() =>\n        Throws(() =>\n            StartEndTester.IsBeginSnippet(\"<!-- begin-snippet: key_ -->\", \"file\", out _, out _));\n\n    [Fact]\n    public void CanExtractWithDifferentEndComments()\n    {\n        var isBeginSnippet = StartEndTester.IsBeginSnippet(\"/* begin-snippet: CodeKey */\", \"file\", out var key,out _);\n        Assert.True(isBeginSnippet);\n        Assert.Equal(\"CodeKey\", key);\n    }\n\n    [Fact]\n    public void CanExtractWithDifferentEndCommentsAndNoSpaces()\n    {\n        var isBeginSnippet = StartEndTester.IsBeginSnippet(\"/*begin-snippet: CodeKey */\", \"file\", out var key, out _);\n        Assert.True(isBeginSnippet);\n        Assert.Equal(\"CodeKey\", key);\n    }\n\n    [Fact]\n    public void CanExtractWithExpressiveCodeWithHtmlSnippet()\n    {\n        var isBeginSnippet = StartEndTester.IsBeginSnippet(\"\"\"<!--begin-snippet: CodeKey(title=\"Program.cs\" {1-3})-->\"\"\", \"file\", out var key, out var block);\n        Assert.True(isBeginSnippet);\n        Assert.Equal(\"CodeKey\", key);\n        Assert.Equal(\"\"\"title=\"Program.cs\" {1-3}\"\"\", block);\n    }\n\n    [Fact]\n    public void CanExtractWithExpressiveCodeWithCsharpComment()\n    {\n        var isBeginSnippet = StartEndTester.IsBeginSnippet(\"\"\"/*begin-snippet: CodeKey(title=\"Program.cs\" {1-3})*/\"\"\", \"file\", out var key, out var expressive);\n        Assert.True(isBeginSnippet);\n        Assert.Equal(\"CodeKey\", key);\n        Assert.Equal(\"\"\"title=\"Program.cs\" {1-3}\"\"\", expressive);\n    }\n    [Fact]\n    public void CanExtractWithExpressiveCodeWithHtmlSnippetTrailingWhitespace()\n    {\n        var isBeginSnippet = StartEndTester.IsBeginSnippet(\"\"\"<!--begin-snippet: CodeKey(title=\"Program.cs\" {1-3})  -->\"\"\", \"file\", out var key, out var block);\n        Assert.True(isBeginSnippet);\n        Assert.Equal(\"CodeKey\", key);\n        Assert.Equal(\"\"\"title=\"Program.cs\" {1-3}\"\"\", block);\n    }\n\n    [Fact]\n    public void CanExtractWithExpressiveCodeWithCsharpCommentTrailingWhitespace()\n    {\n        var isBeginSnippet = StartEndTester.IsBeginSnippet(\"\"\"/*begin-snippet: CodeKey(title=\"Program.cs\" {1-3})  */\"\"\", \"file\", out var key, out var expressive);\n        Assert.True(isBeginSnippet);\n        Assert.Equal(\"CodeKey\", key);\n        Assert.Equal(\"\"\"title=\"Program.cs\" {1-3}\"\"\", expressive);\n    }\n\n    [Fact]\n    public void CanExtractLanguageOverride()\n    {\n        var isBeginSnippet = StartEndTester.IsBeginSnippet(\"<!-- begin-snippet: CodeKey (lang=json) -->\", \"file\", out var key, out var expressive, out var language);\n        Assert.True(isBeginSnippet);\n        Assert.Equal(\"CodeKey\", key);\n        Assert.Equal(\"json\", language.ToString());\n        Assert.Equal(0, expressive.Length);\n    }\n\n    [Fact]\n    public void CanExtractLanguageOverrideWithExpressiveCode()\n    {\n        var isBeginSnippet = StartEndTester.IsBeginSnippet(\"\"\"<!-- begin-snippet: CodeKey (lang=json title=\"a.json\") -->\"\"\", \"file\", out var key, out var expressive, out var language);\n        Assert.True(isBeginSnippet);\n        Assert.Equal(\"CodeKey\", key);\n        Assert.Equal(\"json\", language.ToString());\n        Assert.Equal(\"\"\"title=\"a.json\" \"\"\".TrimEnd(), expressive.ToString());\n    }\n\n    [Fact]\n    public Task ShouldThrowForInvalidLanguageValue() =>\n        Throws(() =>\n            StartEndTester.IsBeginSnippet(\"<!-- begin-snippet: CodeKey (lang=C#) -->\", \"file\", out _, out _, out _));\n\n    [Fact]\n    public Task ShouldThrowForEmptyLanguageValue() =>\n        Throws(() =>\n            StartEndTester.IsBeginSnippet(\"<!-- begin-snippet: CodeKey (lang=) -->\", \"file\", out _, out _, out _));\n}"
  },
  {
    "path": "src/Tests/StartEndTester_IsStartRegionTests.cs",
    "content": "﻿public class StartEndTester_IsStartRegionTests\n{\n    [Fact]\n    public void CanExtractFromXml()\n    {\n        StartEndTester.IsStartRegion(\"#region CodeKey\", out var key);\n        Assert.Equal(\"CodeKey\", key);\n    }\n\n    [Fact]\n    public void ShouldThrowForKeyStartingWithSymbol() =>\n        Assert.False(StartEndTester.IsStartRegion(\"#region _key\", out _));\n\n    [Fact]\n    public void WithSpaces() =>\n        Assert.False(StartEndTester.IsStartRegion(\"#region the text\", out _));\n\n    [Fact]\n    public void ShouldThrowForKeyEndingWithSymbol() =>\n        Assert.False(StartEndTester.IsStartRegion(\"#region key_ \", out _));\n\n    [Fact]\n    public void ShouldIgnoreForNoKey() =>\n        Assert.False(StartEndTester.IsStartRegion(\"#region \", out _));\n\n    [Fact]\n    public void CanExtractFromXmlWithExtraSpaces()\n    {\n        StartEndTester.IsStartRegion(\"#region  CodeKey   \", out var key);\n        Assert.Equal(\"CodeKey\", key);\n    }\n\n    [Fact]\n    public void CanExtractWithNoTrailingCharacters()\n    {\n        StartEndTester.IsStartRegion(\"#region CodeKey\", out var key);\n        Assert.Equal(\"CodeKey\", key);\n    }\n\n    [Fact]\n    public void CanExtractWithUnderScores()\n    {\n        StartEndTester.IsStartRegion(\"#region Code_Key\", out var key);\n        Assert.Equal(\"Code_Key\", key);\n    }\n\n    [Fact]\n    public void CanExtractWithDashes()\n    {\n        StartEndTester.IsStartRegion(\"#region Code-Key\", out var key);\n        Assert.Equal(\"Code-Key\", key);\n    }\n}"
  },
  {
    "path": "src/Tests/Tests.csproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <TargetFrameworks Condition=\" '$(OS)' == 'Windows_NT' \">net48</TargetFrameworks>\n    <TargetFrameworks>net10.0;$(TargetFrameworks)</TargetFrameworks>\n    <OutputType>Exe</OutputType>\n    <NoWarn>$(NoWarn);xUnit1051</NoWarn>\n    <RootNamespace>testing</RootNamespace>\n  </PropertyGroup>\n  <ItemGroup>\n    <Folder Include=\"DirectorySnippetExtractor\\VerifyLambdasAreCalled\\subpath\\\" />\n    <Folder Include=\"GitDirs\\\" />\n    <PackageReference Include=\"Argon\" />\n    <PackageReference Include=\"ProjectFiles\" />\n    <PackageReference Include=\"Verify.DiffPlex\" />\n    <PackageReference Include=\"Verify.XunitV3\" />\n    <PackageReference Include=\"DiffEngine\" />\n    <PackageReference Include=\"EmptyFiles\" />\n    <PackageReference Include=\"SimpleInfoName\" />\n    <PackageReference Include=\"Verify\" />\n    <PackageReference Include=\"xunit.runner.visualstudio\" PrivateAssets=\"all\" />\n    <PackageReference Include=\"xunit.v3\" />\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" />\n    <ProjectReference Include=\"..\\MarkdownSnippets\\MarkdownSnippets.csproj\" />\n    <PackageReference Include=\"ProjectDefaults\" PrivateAssets=\"all\" />\n    <None Include=\"badsnippets\\code.txt\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n    </None>\n    <None Update=\"DirectorySnippetExtractor\\**\\*.*\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n      <Link>%(RecursiveDir)%(Filename)%(Extension)</Link>\n    </None>\n    <None Update=\"DirectoryMarkdownProcessor\\**\\*.md\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n      <Link>%(RecursiveDir)%(Filename)%(Extension)</Link>\n    </None>\n    <None Update=\"DirectoryMarkdownProcessor\\**\\*.mdx\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n      <Link>%(RecursiveDir)%(Filename)%(Extension)</Link>\n    </None>\n    <None Update=\"DirectoryMarkdownProcessor\\**\\*.dot\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n      <Link>%(RecursiveDir)%(Filename)%(Extension)</Link>\n    </None>\n    <None Update=\"DirectoryMarkdownProcessor\\**\\*.txt\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n      <Link>%(RecursiveDir)%(Filename)%(Extension)</Link>\n    </None>\n    <None Update=\"IncludeFileFinder\\**\\*.*\">\n      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>\n      <Link>%(RecursiveDir)%(Filename)%(Extension)</Link>\n    </None>\n    <None Update=\"IncludeFinder\\file.include.md\">\n      <CopyToOutputDirectory>Always</CopyToOutputDirectory>\n    </None>\n  </ItemGroup>\n</Project>\n"
  },
  {
    "path": "src/Tests/WebSnippetTests.cs",
    "content": "public class WebSnippetTests\n{\n    [Fact]\n    public void ExtractWebSnippet_ParsesCorrectly()\n    {\n        var line = new Line(\"web-snippet:https://example.com/file.cs#mysnippet\", \"\", 1);\n        Assert.True(SnippetKey.ExtractWebSnippet(line, out var url, out var key));\n        Assert.Equal(\"https://example.com/file.cs\", url);\n        Assert.Equal(\"mysnippet\", key);\n    }\n\n    [Fact]\n    public void ExtractWebSnippet_FailsWithoutHash()\n    {\n        var line = new Line(\"web-snippet:https://example.com/file.cs\", \"\", 1);\n        Assert.False(SnippetKey.ExtractWebSnippet(line, out var _, out var _));\n    }\n}\n"
  },
  {
    "path": "src/Tests/badsnippets/code.txt",
    "content": "﻿\n\nbegin-snippet: snippet1\nthis is some text to import\nend-snippet\nbegin-snippet: snippet1\nthis is some text to import\nend-snippet"
  },
  {
    "path": "src/appveyor.yml",
    "content": "image:\n- Visual Studio 2022\n#- macOS\nenvironment:\n  DOTNET_NOLOGO: true\n  DOTNET_CLI_TELEMETRY_OPTOUT: true\n  DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true\nbuild_script:\n- pwsh: |\n    if ($isWindows) {\n      Invoke-WebRequest \"https://dot.net/v1/dotnet-install.ps1\" -OutFile \"./dotnet-install.ps1\"\n      ./dotnet-install.ps1 -JSonFile src/global.json -Architecture x64 -InstallDir 'C:\\Program Files\\dotnet'\n    }\n    else {\n      Invoke-WebRequest \"https://dot.net/v1/dotnet-install.sh\" -OutFile \"./dotnet-install.sh\"\n      sudo chmod u+x dotnet-install.sh\n      sudo ./dotnet-install.sh --jsonfile src/global.json --architecture x64 --install-dir '/usr/local/share/dotnet'\n      sudo ./dotnet-install.sh --version 9.0.306 --architecture x64 --install-dir '/usr/local/share/dotnet'\n    }\n- dotnet build src --configuration Release\n- dotnet test src --configuration Release --no-build --no-restore\ntest: off\non_failure:\n  - ps: |\n      $root = (Get-Location).Path\n      Get-ChildItem *.received.* -Recurse | % {\n        $rel = $_.FullName.Substring($root.Length + 1)\n        Push-AppveyorArtifact $_.FullName -FileName $rel\n      }\nartifacts:\n- path: nugets\\*.nupkg"
  },
  {
    "path": "src/context-menu.reg",
    "content": "Windows Registry Editor Version 5.00\n[HKEY_CLASSES_ROOT\\Directory\\Shell]\n@=\"none\"\n[HKEY_CLASSES_ROOT\\Directory\\shell\\mdsnippets]\n\"MUIVerb\"=\"run mdsnippets\"\n\"Position\"=\"bottom\"\n[HKEY_CLASSES_ROOT\\Directory\\Background\\shell\\mdsnippets]\n\"MUIVerb\"=\"run mdsnippets\"\n\"Position\"=\"bottom\"\n[HKEY_CLASSES_ROOT\\Directory\\shell\\mdsnippets\\command]\n@=\"cmd.exe /c mdsnippets \\\"%V\\\"\"\n[HKEY_CLASSES_ROOT\\Directory\\Background\\shell\\mdsnippets\\command]\n@=\"cmd.exe /c mdsnippets \\\"%V\\\"\""
  },
  {
    "path": "src/global.json",
    "content": "{\n  \"msbuild-sdks\": {\n    \"MSBuild.Sdk.Extras\": \"3.0.44\"\n  },\n  \"sdk\": {\n    \"version\": \"10.0.203\",\n    \"allowPrerelease\": true,\n    \"rollForward\": \"latestFeature\"\n  }\n}"
  },
  {
    "path": "src/mdsnippets.json",
    "content": "﻿{\n  \"$schema\": \"https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/schema.json\",\n  \"TocLevel\": 1,\n  \"TocExcludes\": [ \"Credits\", \"Release Notes\", \"Icon\" ],\n  \"MaxWidth\": 80,\n  \"ValidateContent\": true\n}"
  },
  {
    "path": "src/nuget-readme.md",
    "content": "# MarkdownSnippets\n\nA [dotnet tool](https://docs.microsoft.com/en-us/dotnet/core/tools/global-tools) or [MsBuild Task](https://github.com/SimonCropp/MarkdownSnippets/blob/main/docs/msbuild.md) that extract snippets from code files and merges them into markdown documents.\n\nSee https://github.com/SimonCropp/MarkdownSnippets for full documentation.\n\n## Behavior\n\n * Recursively scan the target directory for code files containing snippets.\n * Recursively scan the target directory for markdown (`.md` or `mdx`) files.\n * Merge the snippets into those markdown files.\n\n## Installation\n\nEnsure [dotnet CLI is installed](https://docs.microsoft.com/en-us/dotnet/core/tools/).\n\nInstall [MarkdownSnippets.Tool](https://nuget.org/packages/MarkdownSnippets.Tool/)\n\n```\ndotnet tool install -g MarkdownSnippets.Tool\n```\n\n## Usage\n\n```\nmdsnippets C:\\Code\\TargetDirectory\n```\n\nIf no directory is passed the current directory will be used, but only if it exists with a git repository directory tree. If not an error is returned.\n\n## Defining Snippets\n\nAny code wrapped in a convention based comment will be picked up. The comment needs to start with `begin-snippet:` which is followed by the key. The snippet is then terminated by `end-snippet`.\n\n```\n// begin-snippet: MySnippetName\nMy Snippet Code\n// end-snippet\n```\n\nNamed [C# regions](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-region) will also be picked up, with the name of the region used as the key.\n\n```\n#region MySnippetName\nMy Snippet Code\n#endregion\n```\n\n## Using Snippets in Markdown\n\nThe raw snippet key can be used in any markdown document by subsequent surrounding it with `snippet:`:\n\n```\n<!-- snippet: MySnippetName -->\n```\n"
  },
  {
    "path": "src/nuget.config",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<configuration>\n  <config>\n    <add key=\"signatureValidationMode\" value=\"require\" />\n  </config>\n  <packageSources>\n    <clear />\n    <add key=\"nuget.org\" value=\"https://api.nuget.org/v3/index.json\" protocolVersion=\"3\" />\n  </packageSources>\n  <trustedSigners>\n    <repository name=\"nuget.org\" serviceIndex=\"https://api.nuget.org/v3/index.json\">\n      <!-- Current: expires May 15, 2024 -->\n      <certificate fingerprint=\"5A2901D6ADA3D18260B9C6DFE2133C95D74B9EEF6AE0E5DC334C8454D1477DF4\"\n                   hashAlgorithm=\"SHA256\"\n                   allowUntrustedRoot=\"false\" />\n      <!-- Next -->\n      <certificate fingerprint=\"1F4B311D9ACC115C8DC8018B5A49E00FCE6DA8E2855F9F014CA6F34570BC482D\"\n                   hashAlgorithm=\"SHA256\"\n                   allowUntrustedRoot=\"false\" />\n      <!-- For some very old MS packages -->\n      <certificate fingerprint=\"0E5F38F57DC1BCC806D8494F4F90FBCEDD988B46760709CBEEC6F4219AA6157D\"\n                   hashAlgorithm=\"SHA256\"\n                   allowUntrustedRoot=\"false\" />\n    </repository>\n  </trustedSigners>\n</configuration>"
  }
]