├── .editorconfig ├── .github └── workflows │ └── dotnet.yml ├── .gitignore ├── Directory.Build.props ├── LICENSE ├── Markdown.AIRender.sln ├── README.md ├── samples └── SamplesMarkdown │ ├── App.axaml │ ├── App.axaml.cs │ ├── Assets │ └── avalonia-logo.ico │ ├── Program.cs │ ├── SamplesMarkdown.csproj │ ├── ViewLocator.cs │ ├── ViewModels │ ├── MainWindowViewModel.cs │ └── ViewModelBase.cs │ ├── Views │ ├── MainWindow.axaml │ └── MainWindow.axaml.cs │ ├── app.manifest │ ├── i18n │ ├── Language.cs │ ├── Language.tt │ ├── SamplesMarkdown.en-US.xml │ ├── SamplesMarkdown.ja-JP.xml │ ├── SamplesMarkdown.zh-CN.xml │ └── SamplesMarkdown.zh-Hant.xml │ ├── logo.png │ └── markdowns │ ├── Full.md │ ├── MDSample.md │ └── OnlyTitles.md └── src └── MarkdownAIRender ├── AssemblyInfo.cs ├── CodeRender └── CodeRender.cs ├── CodeToolRenderEventHandler.cs ├── Controls ├── Images │ ├── AnimateInfo.cs │ ├── ImagesRender.cs │ └── VectorImage.cs └── MarkdownRender │ ├── MarkdownClass.cs │ └── MarkdownRender.cs ├── Helper └── UrlHelper.cs ├── Index.axaml ├── MarkdownAIRender.csproj ├── Themes ├── Controls │ ├── MarkdownRender.axaml │ └── _index.axaml ├── MarkdownThemes │ ├── Dark.axaml │ └── Light.axaml ├── Shared │ └── _index.axaml └── Styles │ ├── ColorfulPurple.axaml │ ├── Default.axaml │ ├── Inkiness.axaml │ ├── OrangeHeart.axaml │ ├── TechnologyBlue.axaml │ └── _index.axaml ├── i18n ├── Language.cs ├── Language.tt ├── MarkdownAIRender.en-US.xml ├── MarkdownAIRender.ja-JP.xml ├── MarkdownAIRender.zh-CN.xml └── MarkdownAIRender.zh-Hant.xml └── logo.png /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # All files 4 | [*] 5 | indent_style = space 6 | 7 | # Xml files 8 | [*.xml] 9 | indent_size = 2 10 | 11 | # C# files 12 | [*.cs] 13 | 14 | #### Core EditorConfig Options #### 15 | 16 | # Indentation and spacing 17 | indent_size = 4 18 | tab_width = 4 19 | 20 | # New line preferences 21 | insert_final_newline = false 22 | 23 | #### .NET Coding Conventions #### 24 | [*.{cs,vb}] 25 | 26 | # Organize usings 27 | dotnet_separate_import_directive_groups = true 28 | dotnet_sort_system_directives_first = true 29 | file_header_template = unset 30 | 31 | # this. and Me. preferences 32 | dotnet_style_qualification_for_event = false:silent 33 | dotnet_style_qualification_for_field = false:silent 34 | dotnet_style_qualification_for_method = false:silent 35 | dotnet_style_qualification_for_property = false:silent 36 | 37 | # Language keywords vs BCL types preferences 38 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent 39 | dotnet_style_predefined_type_for_member_access = true:silent 40 | 41 | # Parentheses preferences 42 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent 43 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent 44 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 45 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent 46 | 47 | # Modifier preferences 48 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent 49 | 50 | # Expression-level preferences 51 | dotnet_style_coalesce_expression = true:suggestion 52 | dotnet_style_collection_initializer = true:suggestion 53 | dotnet_style_explicit_tuple_names = true:suggestion 54 | dotnet_style_namespace_match_folder = true:suggestion 55 | dotnet_style_null_propagation = true:suggestion 56 | dotnet_style_object_initializer = true:suggestion 57 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 58 | dotnet_style_prefer_auto_properties = true:suggestion 59 | dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion 60 | dotnet_style_prefer_compound_assignment = true:suggestion 61 | dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion 62 | dotnet_style_prefer_conditional_expression_over_return = true:suggestion 63 | dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed:suggestion 64 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 65 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 66 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 67 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion 68 | dotnet_style_prefer_simplified_interpolation = true:suggestion 69 | 70 | # Field preferences 71 | dotnet_style_readonly_field = true:warning 72 | 73 | # Parameter preferences 74 | dotnet_code_quality_unused_parameters = all:suggestion 75 | 76 | # Suppression preferences 77 | dotnet_remove_unnecessary_suppression_exclusions = none 78 | 79 | #### C# Coding Conventions #### 80 | [*.cs] 81 | 82 | # var preferences 83 | csharp_style_var_elsewhere = false:silent 84 | csharp_style_var_for_built_in_types = false:silent 85 | csharp_style_var_when_type_is_apparent = false:silent 86 | 87 | # Expression-bodied members 88 | csharp_style_expression_bodied_accessors = true:silent 89 | csharp_style_expression_bodied_constructors = false:silent 90 | csharp_style_expression_bodied_indexers = true:silent 91 | csharp_style_expression_bodied_lambdas = true:suggestion 92 | csharp_style_expression_bodied_local_functions = false:silent 93 | csharp_style_expression_bodied_methods = false:silent 94 | csharp_style_expression_bodied_operators = false:silent 95 | csharp_style_expression_bodied_properties = true:silent 96 | 97 | # Pattern matching preferences 98 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 99 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 100 | csharp_style_prefer_extended_property_pattern = true:suggestion 101 | csharp_style_prefer_not_pattern = true:suggestion 102 | csharp_style_prefer_pattern_matching = true:silent 103 | csharp_style_prefer_switch_expression = true:suggestion 104 | 105 | # Null-checking preferences 106 | csharp_style_conditional_delegate_call = true:suggestion 107 | 108 | # Modifier preferences 109 | csharp_prefer_static_anonymous_function = true:suggestion 110 | csharp_prefer_static_local_function = true:warning 111 | csharp_preferred_modifier_order = public,private,protected,internal,file,const,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:suggestion 112 | csharp_style_prefer_readonly_struct = true:suggestion 113 | csharp_style_prefer_readonly_struct_member = true:suggestion 114 | 115 | # Code-block preferences 116 | csharp_prefer_braces = true:silent 117 | csharp_prefer_simple_using_statement = true:suggestion 118 | csharp_style_namespace_declarations = file_scoped:suggestion 119 | csharp_style_prefer_method_group_conversion = true:silent 120 | csharp_style_prefer_primary_constructors = true:suggestion 121 | csharp_style_prefer_top_level_statements = true:silent 122 | 123 | # Expression-level preferences 124 | csharp_prefer_simple_default_expression = true:suggestion 125 | csharp_style_deconstructed_variable_declaration = true:suggestion 126 | csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion 127 | csharp_style_inlined_variable_declaration = true:suggestion 128 | csharp_style_prefer_index_operator = true:suggestion 129 | csharp_style_prefer_local_over_anonymous_function = true:suggestion 130 | csharp_style_prefer_null_check_over_type_check = true:suggestion 131 | csharp_style_prefer_range_operator = true:suggestion 132 | csharp_style_prefer_tuple_swap = true:suggestion 133 | csharp_style_prefer_utf8_string_literals = true:suggestion 134 | csharp_style_throw_expression = true:suggestion 135 | csharp_style_unused_value_assignment_preference = discard_variable:suggestion 136 | csharp_style_unused_value_expression_statement_preference = discard_variable:silent 137 | 138 | # 'using' directive preferences 139 | csharp_using_directive_placement = outside_namespace:silent 140 | 141 | #### C# Formatting Rules #### 142 | 143 | # New line preferences 144 | csharp_new_line_before_catch = true 145 | csharp_new_line_before_else = true 146 | csharp_new_line_before_finally = true 147 | csharp_new_line_before_members_in_anonymous_types = true 148 | csharp_new_line_before_members_in_object_initializers = true 149 | csharp_new_line_before_open_brace = all 150 | csharp_new_line_between_query_expression_clauses = true 151 | 152 | # Indentation preferences 153 | csharp_indent_block_contents = true 154 | csharp_indent_braces = false 155 | csharp_indent_case_contents = true 156 | csharp_indent_case_contents_when_block = true 157 | csharp_indent_labels = one_less_than_current 158 | csharp_indent_switch_labels = true 159 | 160 | # Space preferences 161 | csharp_space_after_cast = false 162 | csharp_space_after_colon_in_inheritance_clause = true 163 | csharp_space_after_comma = true 164 | csharp_space_after_dot = false 165 | csharp_space_after_keywords_in_control_flow_statements = true 166 | csharp_space_after_semicolon_in_for_statement = true 167 | csharp_space_around_binary_operators = before_and_after 168 | csharp_space_around_declaration_statements = false 169 | csharp_space_before_colon_in_inheritance_clause = true 170 | csharp_space_before_comma = false 171 | csharp_space_before_dot = false 172 | csharp_space_before_open_square_brackets = false 173 | csharp_space_before_semicolon_in_for_statement = false 174 | csharp_space_between_empty_square_brackets = false 175 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 176 | csharp_space_between_method_call_name_and_opening_parenthesis = false 177 | csharp_space_between_method_call_parameter_list_parentheses = false 178 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 179 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 180 | csharp_space_between_method_declaration_parameter_list_parentheses = false 181 | csharp_space_between_parentheses = false 182 | csharp_space_between_square_brackets = false 183 | 184 | # Wrapping preferences 185 | csharp_preserve_single_line_blocks = true 186 | csharp_preserve_single_line_statements = true 187 | 188 | #### Naming styles #### 189 | [*.{cs,vb}] 190 | 191 | # Naming rules 192 | 193 | dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion 194 | dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces 195 | dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase 196 | 197 | dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion 198 | dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces 199 | dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase 200 | 201 | dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion 202 | dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters 203 | dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase 204 | 205 | dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion 206 | dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods 207 | dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase 208 | 209 | dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion 210 | dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties 211 | dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase 212 | 213 | dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion 214 | dotnet_naming_rule.events_should_be_pascalcase.symbols = events 215 | dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase 216 | 217 | dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion 218 | dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables 219 | dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase 220 | 221 | dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion 222 | dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants 223 | dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase 224 | 225 | dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion 226 | dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters 227 | dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase 228 | 229 | dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion 230 | dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields 231 | dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase 232 | 233 | dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion 234 | dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields 235 | dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase 236 | 237 | dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion 238 | dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields 239 | dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase 240 | 241 | dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion 242 | dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields 243 | dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase 244 | 245 | dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion 246 | dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields 247 | dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase 248 | 249 | dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion 250 | dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields 251 | dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase 252 | 253 | dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion 254 | dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields 255 | dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase 256 | 257 | dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion 258 | dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums 259 | dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase 260 | 261 | dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion 262 | dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions 263 | dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase 264 | 265 | dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion 266 | dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members 267 | dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase 268 | 269 | # Symbol specifications 270 | 271 | dotnet_naming_symbols.interfaces.applicable_kinds = interface 272 | dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 273 | dotnet_naming_symbols.interfaces.required_modifiers = 274 | 275 | dotnet_naming_symbols.enums.applicable_kinds = enum 276 | dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 277 | dotnet_naming_symbols.enums.required_modifiers = 278 | 279 | dotnet_naming_symbols.events.applicable_kinds = event 280 | dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 281 | dotnet_naming_symbols.events.required_modifiers = 282 | 283 | dotnet_naming_symbols.methods.applicable_kinds = method 284 | dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 285 | dotnet_naming_symbols.methods.required_modifiers = 286 | 287 | dotnet_naming_symbols.properties.applicable_kinds = property 288 | dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 289 | dotnet_naming_symbols.properties.required_modifiers = 290 | 291 | dotnet_naming_symbols.public_fields.applicable_kinds = field 292 | dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal 293 | dotnet_naming_symbols.public_fields.required_modifiers = 294 | 295 | dotnet_naming_symbols.private_fields.applicable_kinds = field 296 | dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected 297 | dotnet_naming_symbols.private_fields.required_modifiers = 298 | 299 | dotnet_naming_symbols.private_static_fields.applicable_kinds = field 300 | dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected 301 | dotnet_naming_symbols.private_static_fields.required_modifiers = static 302 | 303 | dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum 304 | dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 305 | dotnet_naming_symbols.types_and_namespaces.required_modifiers = 306 | 307 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 308 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 309 | dotnet_naming_symbols.non_field_members.required_modifiers = 310 | 311 | dotnet_naming_symbols.type_parameters.applicable_kinds = namespace 312 | dotnet_naming_symbols.type_parameters.applicable_accessibilities = * 313 | dotnet_naming_symbols.type_parameters.required_modifiers = 314 | 315 | dotnet_naming_symbols.private_constant_fields.applicable_kinds = field 316 | dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected 317 | dotnet_naming_symbols.private_constant_fields.required_modifiers = const 318 | 319 | dotnet_naming_symbols.local_variables.applicable_kinds = local 320 | dotnet_naming_symbols.local_variables.applicable_accessibilities = local 321 | dotnet_naming_symbols.local_variables.required_modifiers = 322 | 323 | dotnet_naming_symbols.local_constants.applicable_kinds = local 324 | dotnet_naming_symbols.local_constants.applicable_accessibilities = local 325 | dotnet_naming_symbols.local_constants.required_modifiers = const 326 | 327 | dotnet_naming_symbols.parameters.applicable_kinds = parameter 328 | dotnet_naming_symbols.parameters.applicable_accessibilities = * 329 | dotnet_naming_symbols.parameters.required_modifiers = 330 | 331 | dotnet_naming_symbols.public_constant_fields.applicable_kinds = field 332 | dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal 333 | dotnet_naming_symbols.public_constant_fields.required_modifiers = const 334 | 335 | dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field 336 | dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal 337 | dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static 338 | 339 | dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field 340 | dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected 341 | dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static 342 | 343 | dotnet_naming_symbols.local_functions.applicable_kinds = local_function 344 | dotnet_naming_symbols.local_functions.applicable_accessibilities = * 345 | dotnet_naming_symbols.local_functions.required_modifiers = 346 | 347 | # Naming styles 348 | 349 | dotnet_naming_style.pascalcase.required_prefix = 350 | dotnet_naming_style.pascalcase.required_suffix = 351 | dotnet_naming_style.pascalcase.word_separator = 352 | dotnet_naming_style.pascalcase.capitalization = pascal_case 353 | 354 | dotnet_naming_style.ipascalcase.required_prefix = I 355 | dotnet_naming_style.ipascalcase.required_suffix = 356 | dotnet_naming_style.ipascalcase.word_separator = 357 | dotnet_naming_style.ipascalcase.capitalization = pascal_case 358 | 359 | dotnet_naming_style.tpascalcase.required_prefix = T 360 | dotnet_naming_style.tpascalcase.required_suffix = 361 | dotnet_naming_style.tpascalcase.word_separator = 362 | dotnet_naming_style.tpascalcase.capitalization = pascal_case 363 | 364 | dotnet_naming_style._camelcase.required_prefix = _ 365 | dotnet_naming_style._camelcase.required_suffix = 366 | dotnet_naming_style._camelcase.word_separator = 367 | dotnet_naming_style._camelcase.capitalization = camel_case 368 | 369 | dotnet_naming_style.camelcase.required_prefix = 370 | dotnet_naming_style.camelcase.required_suffix = 371 | dotnet_naming_style.camelcase.word_separator = 372 | dotnet_naming_style.camelcase.capitalization = camel_case 373 | 374 | dotnet_naming_style.s_camelcase.required_prefix = s_ 375 | dotnet_naming_style.s_camelcase.required_suffix = 376 | dotnet_naming_style.s_camelcase.word_separator = 377 | dotnet_naming_style.s_camelcase.capitalization = camel_case 378 | 379 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: master 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - 'Directory.Build.props' 8 | 9 | jobs: 10 | deploy-nuget: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Build 15 | run: dotnet build --configuration Release 16 | - name: Pack 17 | run: dotnet pack --no-build --configuration Release 18 | - name: Push NuGet package 19 | run: | 20 | dotnet nuget push **/*.nupkg --skip-duplicate --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET }} 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | /packages/ 4 | riderModule.iml 5 | /_ReSharper.Caches/ 6 | .vs/ 7 | /.idea 8 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | net6.0;net7.0;net8.0;net9.0 4 | latest 5 | enable 6 | logo.png 7 | 11.2.3 8 | 0.1.16 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 AIDotNet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Markdown.AIRender.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5E59CE55-BA09-4097-B810-CDF06F29FF8B}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MarkdownAIRender", "src\MarkdownAIRender\MarkdownAIRender.csproj", "{6A156B99-4503-4A06-A8BE-BFD9967E0127}" 6 | EndProject 7 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5FF59DC0-3C48-4F8A-BE24-6A948F44375E}" 8 | ProjectSection(SolutionItems) = preProject 9 | .editorconfig = .editorconfig 10 | .gitignore = .gitignore 11 | Directory.Build.props = Directory.Build.props 12 | EndProjectSection 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{53BEF8D8-76B9-4527-81A6-2502371FE43F}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SamplesMarkdown", "samples\SamplesMarkdown\SamplesMarkdown.csproj", "{A21659C3-934A-4D2E-98D8-EFA564E328DC}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(NestedProjects) = preSolution 24 | {6A156B99-4503-4A06-A8BE-BFD9967E0127} = {5E59CE55-BA09-4097-B810-CDF06F29FF8B} 25 | {A21659C3-934A-4D2E-98D8-EFA564E328DC} = {53BEF8D8-76B9-4527-81A6-2502371FE43F} 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {6A156B99-4503-4A06-A8BE-BFD9967E0127}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {6A156B99-4503-4A06-A8BE-BFD9967E0127}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {6A156B99-4503-4A06-A8BE-BFD9967E0127}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {6A156B99-4503-4A06-A8BE-BFD9967E0127}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {A21659C3-934A-4D2E-98D8-EFA564E328DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {A21659C3-934A-4D2E-98D8-EFA564E328DC}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {A21659C3-934A-4D2E-98D8-EFA564E328DC}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {A21659C3-934A-4D2E-98D8-EFA564E328DC}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Markdown.AIRender 2 | 3 | 实现基础markdown的组件库功能,并且提供代码高亮,打造avalonia最棒的markdown组件库 4 | 5 | - [x] 支持标题 6 | - [x] 支持代码高亮 7 | - [x] 支持图片 8 | - [x] 支持SVG 9 | - [x] 支持svg动画 10 | 11 | 12 | -------------------------------------------------------------------------------- /samples/SamplesMarkdown/App.axaml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /samples/SamplesMarkdown/App.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls.ApplicationLifetimes; 3 | using Avalonia.Data.Core; 4 | using Avalonia.Data.Core.Plugins; 5 | using Avalonia.Markup.Xaml; 6 | 7 | using SamplesMarkdown.ViewModels; 8 | using SamplesMarkdown.Views; 9 | 10 | namespace SamplesMarkdown; 11 | 12 | public partial class App : Application 13 | { 14 | public override void Initialize() 15 | { 16 | AvaloniaXamlLoader.Load(this); 17 | } 18 | 19 | public override void OnFrameworkInitializationCompleted() 20 | { 21 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) 22 | { 23 | // Line below is needed to remove Avalonia data validation. 24 | // Without this line you will get duplicate validations from both Avalonia and CT 25 | BindingPlugins.DataValidators.RemoveAt(0); 26 | desktop.MainWindow = new MainWindow { DataContext = new MainWindowViewModel(), }; 27 | } 28 | 29 | base.OnFrameworkInitializationCompleted(); 30 | } 31 | } -------------------------------------------------------------------------------- /samples/SamplesMarkdown/Assets/avalonia-logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AIDotNet/Markdown.AIRender/723ec5792afa588b1777cce9596e0163f66866da/samples/SamplesMarkdown/Assets/avalonia-logo.ico -------------------------------------------------------------------------------- /samples/SamplesMarkdown/Program.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | 3 | using System; 4 | #if DEBUG 5 | using Nlnet.Avalonia.DevTools; 6 | #endif 7 | 8 | namespace SamplesMarkdown; 9 | 10 | sealed class Program 11 | { 12 | // Initialization code. Don't use any Avalonia, third-party APIs or any 13 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized 14 | // yet and stuff might break. 15 | [STAThread] 16 | public static void Main(string[] args) => BuildAvaloniaApp() 17 | .StartWithClassicDesktopLifetime(args); 18 | 19 | // Avalonia configuration, don't remove; also used by visual designer. 20 | public static AppBuilder BuildAvaloniaApp() 21 | => AppBuilder.Configure() 22 | .UsePlatformDetect() 23 | .WithInterFont() 24 | #if DEBUG 25 | .UseDevToolsForAvalonia() 26 | #endif 27 | .LogToTrace(); 28 | } -------------------------------------------------------------------------------- /samples/SamplesMarkdown/SamplesMarkdown.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | WinExe 4 | net8.0;net9.0; 5 | enable 6 | true 7 | app.manifest 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | True 27 | True 28 | Language.tt 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | TextTemplatingFileGenerator 50 | Language.cs 51 | 52 | 53 | PreserveNewest 54 | 55 | 56 | PreserveNewest 57 | 58 | 59 | PreserveNewest 60 | 61 | 62 | PreserveNewest 63 | 64 | 65 | true 66 | 67 | PreserveNewest 68 | 69 | 70 | PreserveNewest 71 | 72 | 73 | PreserveNewest 74 | 75 | 76 | PreserveNewest 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /samples/SamplesMarkdown/ViewLocator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Avalonia.Controls; 4 | using Avalonia.Controls.Templates; 5 | 6 | using SamplesMarkdown.ViewModels; 7 | 8 | namespace SamplesMarkdown; 9 | 10 | public class ViewLocator : IDataTemplate 11 | { 12 | public Control? Build(object? data) 13 | { 14 | if (data is null) 15 | return null; 16 | 17 | var name = data.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); 18 | var type = Type.GetType(name); 19 | 20 | if (type != null) 21 | { 22 | var control = (Control)Activator.CreateInstance(type)!; 23 | control.DataContext = data; 24 | return control; 25 | } 26 | 27 | return new TextBlock { Text = "Not Found: " + name }; 28 | } 29 | 30 | public bool Match(object? data) 31 | { 32 | return data is ViewModelBase; 33 | } 34 | } -------------------------------------------------------------------------------- /samples/SamplesMarkdown/ViewModels/MainWindowViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Globalization; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | using Avalonia.Styling; 11 | 12 | using AvaloniaXmlTranslator; 13 | using AvaloniaXmlTranslator.Models; 14 | 15 | using MarkdownAIRender.Controls.MarkdownRender; 16 | 17 | namespace SamplesMarkdown.ViewModels; 18 | 19 | public partial class MainWindowViewModel : ViewModelBase 20 | { 21 | public MainWindowViewModel() 22 | { 23 | InitSampleFiles(); 24 | InitLanguage(); 25 | InitMarkdownThemes(); 26 | } 27 | 28 | #region Properties 29 | 30 | public List MarkdownFiles { get; private set; } 31 | 32 | private string? _selectedFile; 33 | 34 | public string? SelectedFile 35 | { 36 | get => _selectedFile; 37 | set 38 | { 39 | SetProperty(ref _selectedFile, value); 40 | ReadMarkdown(); 41 | } 42 | } 43 | 44 | private string _markdown; 45 | 46 | public string Markdown 47 | { 48 | get => _markdown; 49 | set => this.SetProperty(ref _markdown, value); 50 | } 51 | 52 | public ObservableCollection MarkdownThemes { get; private set; } 53 | 54 | private MarkdownTheme? _selectedMarkdownTheme; 55 | 56 | public MarkdownTheme? SelectedMarkdownTheme 57 | { 58 | get => _selectedMarkdownTheme; 59 | set 60 | { 61 | this.SetProperty(ref _selectedMarkdownTheme, value); 62 | MarkdownClass.ChangeTheme(_selectedMarkdownTheme.Key); 63 | } 64 | } 65 | 66 | 67 | public ObservableCollection Languages { get; private set; } 68 | 69 | private LocalizationLanguage? _selectedLanguage; 70 | 71 | public LocalizationLanguage? SelectedLanguage 72 | { 73 | get => _selectedLanguage; 74 | set 75 | { 76 | this.SetProperty(ref _selectedLanguage, value); 77 | SetLanguage(); 78 | } 79 | } 80 | 81 | #endregion 82 | 83 | #region Command handlers 84 | 85 | public async Task RaiseChangeThemeHandler() 86 | { 87 | App.Current.RequestedThemeVariant = App.Current.RequestedThemeVariant == ThemeVariant.Dark 88 | ? ThemeVariant.Light 89 | : ThemeVariant.Dark; 90 | } 91 | 92 | #endregion 93 | 94 | #region private methods 95 | 96 | private void InitSampleFiles() 97 | { 98 | MarkdownFiles = Directory.GetFiles("markdowns", "*.md").Select(f=>new FileInfo(f).Name).ToList(); 99 | SelectedFile = MarkdownFiles.FirstOrDefault(); 100 | } 101 | 102 | private void InitLanguage() 103 | { 104 | var languages = I18nManager.Instance.GetLanguages(); 105 | Languages = new ObservableCollection(languages); 106 | 107 | var language = Thread.CurrentThread.CurrentCulture.Name; 108 | SelectedLanguage = Languages.FirstOrDefault(l => l.CultureName == language); 109 | } 110 | 111 | private void InitMarkdownThemes() 112 | { 113 | MarkdownThemes = new ObservableCollection(MarkdownClass.Themes); 114 | SelectedMarkdownTheme = MarkdownClass.Themes.FirstOrDefault(item => item.Key == MarkdownClass.CurrentThemeKey); 115 | } 116 | 117 | private void ReadMarkdown() 118 | { 119 | if (string.IsNullOrWhiteSpace(SelectedFile)) 120 | { 121 | Markdown = "No markdown contents"; 122 | } 123 | else 124 | { 125 | var filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "markdowns", SelectedFile); 126 | Markdown = File.ReadAllText(filePath); 127 | } 128 | } 129 | 130 | private void SetLanguage() 131 | { 132 | var culture = new CultureInfo(SelectedLanguage?.CultureName); 133 | I18nManager.Instance.Culture = culture; 134 | } 135 | 136 | #endregion 137 | } -------------------------------------------------------------------------------- /samples/SamplesMarkdown/ViewModels/ViewModelBase.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | 3 | namespace SamplesMarkdown.ViewModels; 4 | 5 | public class ViewModelBase : ObservableObject 6 | { 7 | } -------------------------------------------------------------------------------- /samples/SamplesMarkdown/Views/MainWindow.axaml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 27 | 28 | 29 | 34 | 35 | 36 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /samples/SamplesMarkdown/Views/MainWindow.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | 3 | namespace SamplesMarkdown.Views; 4 | 5 | public partial class MainWindow : Window 6 | { 7 | public MainWindow() 8 | { 9 | InitializeComponent(); 10 | } 11 | } -------------------------------------------------------------------------------- /samples/SamplesMarkdown/app.manifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /samples/SamplesMarkdown/i18n/Language.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Changes to this file may cause incorrect behavior and will be lost if 5 | // the code is regenerated. 6 | // 7 | //------------------------------------------------------------------------------ 8 | namespace Localization 9 | { 10 | public static class MainWindow 11 | { 12 | public static readonly string ApplicationThemeLabel = "Localization.MainWindow.ApplicationThemeLabel"; 13 | public static readonly string ThemeDark = "Localization.MainWindow.ThemeDark"; 14 | public static readonly string ThemeLight = "Localization.MainWindow.ThemeLight"; 15 | public static readonly string MarkdownThemeColorLabel = "Localization.MainWindow.MarkdownThemeColorLabel"; 16 | public static readonly string MarkdownThemeSelectPlaceholder = "Localization.MainWindow.MarkdownThemeSelectPlaceholder"; 17 | public static readonly string ApplicationLanguageLabel = "Localization.MainWindow.ApplicationLanguageLabel"; 18 | public static readonly string ApplicationLanguageSelectPlaceholder = "Localization.MainWindow.ApplicationLanguageSelectPlaceholder"; 19 | public static readonly string SampleMarkdownFileLabel = "Localization.MainWindow.SampleMarkdownFileLabel"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /samples/SamplesMarkdown/i18n/Language.tt: -------------------------------------------------------------------------------- 1 | <#@ template debug="false" hostspecific="true" language="C#" #> 2 | <#@ assembly name="System.Core" #> 3 | <#@ assembly name="System.Xml" #> 4 | <#@ assembly name="System.Xml.Linq" #> 5 | <#@ import namespace="System.Linq" #> 6 | <#@ import namespace="System.Text" #> 7 | <#@ import namespace="System.Collections.Generic" #> 8 | <#@ import namespace="System.Xml.Linq" #> 9 | <#@ import namespace="System.IO" #> 10 | <#@ output extension=".cs" #> 11 | //------------------------------------------------------------------------------ 12 | // 13 | // This code was generated by a tool. 14 | // Changes to this file may cause incorrect behavior and will be lost if 15 | // the code is regenerated. 16 | // 17 | //------------------------------------------------------------------------------ 18 | <# 19 | string templateDirectory = Path.GetDirectoryName(Host.TemplateFile); 20 | string xmlFilePath = Directory.GetFiles(templateDirectory, "*.xml").FirstOrDefault(); 21 | if (xmlFilePath!= null) 22 | { 23 | XDocument xdoc = XDocument.Load(xmlFilePath); 24 | var classNodes = xdoc.Nodes().OfType().DescendantsAndSelf().Where(e => e.Descendants().Count() == 0).Select(e => e.Parent).Distinct().ToList(); 25 | foreach (var classNode in classNodes) 26 | { 27 | var namespaceSegments = classNode.Ancestors().Reverse().Select(node => node.Name.LocalName); 28 | string namespaceName = string.Join(".", namespaceSegments); 29 | GenerateClasses(classNode, namespaceName); 30 | } 31 | } 32 | else 33 | { 34 | Write("XML file not found, please ensure that there is an XML file in the current directory"); 35 | } 36 | 37 | void GenerateClasses(XElement element, string namespaceName) 38 | { 39 | string className = element.Name.LocalName; 40 | StringBuilder classBuilder = new StringBuilder(); 41 | classBuilder.AppendLine($"namespace {namespaceName}"); 42 | classBuilder.AppendLine("{"); 43 | classBuilder.AppendLine($" public static class {className}"); 44 | classBuilder.AppendLine(" {"); 45 | var fieldNodes = element.Elements(); 46 | foreach (var fieldNode in fieldNodes) 47 | { 48 | var propertyName = fieldNode.Name.LocalName; 49 | var languageKey = $"{namespaceName}.{className}.{propertyName}"; 50 | classBuilder.AppendLine($" public static readonly string {propertyName} = \"{languageKey}\";"); 51 | } 52 | classBuilder.AppendLine(" }"); 53 | classBuilder.AppendLine("}"); 54 | Write(classBuilder.ToString()); 55 | } 56 | #> 57 | -------------------------------------------------------------------------------- /samples/SamplesMarkdown/i18n/SamplesMarkdown.en-US.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | System Theme: 5 | Dark Mode 6 | Light Mode 7 | Markdown Theme Color 8 | Please select a theme color. 9 | System Language: 10 | Please select a language. 11 | Example Markdown: 12 | 13 | -------------------------------------------------------------------------------- /samples/SamplesMarkdown/i18n/SamplesMarkdown.ja-JP.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | システムテーマ: 5 | ダークモード 6 | ライトモード 7 | Markdownのテーマ色 8 | テーマ色を選択してください。 9 | システム言語: 10 | 言語を選択してください。 11 | サンプルマークダウン: 12 | 13 | -------------------------------------------------------------------------------- /samples/SamplesMarkdown/i18n/SamplesMarkdown.zh-CN.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 系统主题: 6 | 黑夜模式 7 | 白天模式 8 | Markdown主题色 9 | 请选择一个主题色 10 | 系统语言: 11 | 请选择语言 12 | 示例Markdown: 13 | 14 | -------------------------------------------------------------------------------- /samples/SamplesMarkdown/i18n/SamplesMarkdown.zh-Hant.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 系統主題: 5 | 黑夜模式 6 | 白天模式 7 | Markdown主題色 8 | 請選擇一個主題色 9 | 系統語言: 10 | 請選擇語言 11 | 示例Markdown: 12 | 13 | -------------------------------------------------------------------------------- /samples/SamplesMarkdown/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AIDotNet/Markdown.AIRender/723ec5792afa588b1777cce9596e0163f66866da/samples/SamplesMarkdown/logo.png -------------------------------------------------------------------------------- /samples/SamplesMarkdown/markdowns/Full.md: -------------------------------------------------------------------------------- 1 | [TOOL_REQUEST 对不起,这是我的错误信息。请确保您使用了有效的命令来调用我们的工具。 -------------------------------------------------------------------------------- /samples/SamplesMarkdown/markdowns/MDSample.md: -------------------------------------------------------------------------------- 1 | @ -1,263 +1 @@ 2 | >微信公众号排版工具。问题或建议,请公众号留言。**[程序员翻身](#jump_8)** 3 | 4 | ![Markdown](https://i-blog.csdnimg.cn/img_convert/a22f3596997f92d972839215b03a4750.png) 5 | 6 | 建议使用 **Chrome** 浏览器,体验最佳效果。 7 | 8 | 使用微信公众号编辑器有一个十分头疼的问题:粘贴出来的代码,格式错乱,空间小还带行号,而且特别丑。Markdown.com.cn编辑器能够解决这个问题。 9 | 10 | Markdown是一种轻量级的「标记语言」。 11 | 12 | 请阅读下方文本熟悉工具使用方法,本文可直接拷贝到微信中预览。 13 | 14 | ## 1 Markdown.com.cn 简介 15 | 16 | - 支持自定义样式的 Markdown 编辑器 17 | - 支持微信公众号、知乎和稀土掘金 18 | - 点击右上方对应图标,一键复制到各平台 19 | 20 | ## 2 Markdown语法教程 21 | 22 | ### 2.1 标题 23 | 24 | 不同数量的`#`可以完成不同的标题,如下: 25 | 26 | # 一级标题 27 | 28 | ## 二级标题 29 | 30 | ### 三级标题 31 | 32 | ### 2.2 字体 33 | 34 | 粗体、斜体、粗体和斜体,删除线,需要在文字前后加不同的标记符号。如下: 35 | 36 | **这个是粗体** 37 | 38 | *这个是斜体* 39 | 40 | ***这个是粗体加斜体*** 41 | 42 | ~~这里想用删除线~~ 43 | 44 | 注:如果想给字体换颜色、字体或者居中显示,需要使用内嵌HTML来实现。 45 | 46 | ### 2.3 无序列表 47 | 48 | 无序列表的使用,在符号`-`后加空格使用。如下: 49 | 50 | - 无序列表 1 51 | - 无序列表 2 52 | - 无序列表 3 53 | 54 | 如果要控制列表的层级,则需要在符号`-`前使用空格。如下: 55 | 56 | - 无序列表 1 57 | - 无序列表 2 58 | - 无序列表 2.1 59 | - 无序列表 2.2 60 | 61 | **由于微信原因,最多支持到二级列表**。 62 | 63 | ### 2.4 有序列表 64 | 65 | 有序列表的使用,在数字及符号`.`后加空格后输入内容,如下: 66 | 67 | 1. 有序列表 1 68 | 2. 有序列表 2 69 | 3. 有序列表 3 70 | 71 | ### 2.5 引用 72 | 73 | 引用的格式是在符号`>`后面书写文字。如下: 74 | 75 | > 读一本好书,就是在和高尚的人谈话。 ——歌德 76 | 77 | > 雇用制度对工人不利,但工人根本无力摆脱这个制度。 ——阮一峰 78 | 79 | ### 2.7 链接 80 | 81 | 微信公众号仅支持公众号文章链接,即域名为`https://mp.weixin.qq.com/`的合法链接。使用方法如下所示: 82 | 83 | 对于该论述,欢迎读者查阅之前发过的文章,[你是《未来世界的幸存者》么?](https://mp.weixin.qq.com/s/s5IhxV2ooX3JN_X416nidA) 84 | 85 | ### 2.8 图片 86 | 87 | 插入图片,格式如下: 88 | 89 | ![这里写图片描述](https://fastly.picsum.photos/id/22/200/300.jpg?hmac=dffxO8G-Bxyn7S8ye3hMo7eRQgL9iW5yJJ_hOV6-CwI) 90 | 91 | 支持 jpg、png、gif、svg 等图片格式,**其中 svg 文件仅可在微信公众平台中使用**,svg 文件示例如下: 92 | 93 | ![](https://markdown.com.cn/images/i-am-svg.svg) 94 | 95 | 支持图片**拖拽和截图粘贴**到编辑器中。 96 | 97 | 注:支持图片 ***拖拽和截图粘贴*** 到编辑器中,仅支持 https 的图片,图片粘贴到微信时会自动上传微信服务器。 98 | 99 | ### 2.9 分割线 100 | 101 | 可以在一行中用三个以上的减号来建立一个分隔线,同时需要在分隔线的上面空一行。如下: 102 | 103 | --- 104 | 105 | ### 2.10 表格 106 | 107 | 可以使用冒号来定义表格的对齐方式,如下: 108 | 109 | | 姓名 | 年龄 | 工作 | 110 | | :----- | :--: | -------: | 111 | | 小可爱 | 18 | 吃可爱多 | 112 | | 小小勇敢 | 20 | 爬棵勇敢树 | 113 | | 小小小机智 | 22 | 看一本机智书 | 114 | 115 | 116 | 117 | ## 3. 特殊语法 118 | 119 | ### 3.1 脚注 120 | 121 | > 支持平台:微信公众号、知乎。 122 | 123 | 脚注与链接的区别如下所示: 124 | 125 | ```markdown 126 | 链接:[文字](链接) 127 | 脚注:[文字](脚注解释 "脚注名字") 128 | ``` 129 | 130 | 有人认为在[大前端时代](https://en.wikipedia.org/wiki/Front-end_web_development "Front-end web development")的背景下,移动端开发(Android、IOS)将逐步退出历史舞台。 131 | 132 | [全栈工程师](是指掌握多种技能,并能利用多种技能独立完成产品的人。 "什么是全栈工程师")在业务开发流程中起到了至关重要的作用。 133 | 134 | 脚注内容请拉到最下面观看。 135 | 136 | ### 3.2 代码块 137 | 138 | > 支持平台:微信代码主题仅支持微信公众号!其他主题无限制。 139 | 140 | 如果在一个行内需要引用代码,只要用反引号引起来就好,如下: 141 | 142 | Use the `printf()` function. 143 | 144 | 在需要高亮的代码块的前一行及后一行使用三个反引号,同时**第一行反引号后面表示代码块所使用的语言**,如下: 145 | 146 | ```java 147 | // FileName: HelloWorld.java 148 | public class HelloWorld { 149 | // Java 入口程序,程序从此入口 150 | public static void main(String[] args) { 151 | System.out.println("Hello,World!"); // 向控制台打印一条语句 152 | } 153 | } 154 | ``` 155 | 156 | 支持以下语言种类: 157 | 158 | ``` 159 | bash 160 | clojure,cpp,cs,css 161 | dart,dockerfile, diff 162 | erlang 163 | go,gradle,groovy 164 | haskell 165 | java,javascript,json,julia 166 | kotlin 167 | lisp,lua 168 | makefile,markdown,matlab 169 | objectivec 170 | perl,php,python 171 | r,ruby,rust 172 | scala,shell,sql,swift 173 | tex,typescript 174 | verilog,vhdl 175 | xml 176 | yaml 177 | ``` 178 | 179 | 如果想要更换代码高亮样式,可在上方**代码主题**中挑选。 180 | 181 | 其中**微信代码主题与微信官方一致**,有以下注意事项: 182 | 183 | - 带行号且不换行,代码大小与官方一致 184 | - 需要在代码块处标志语言,否则无法高亮 185 | - 粘贴到公众号后,用鼠标点代码块内外一次,完成高亮 186 | 187 | diff 不能同时和其他语言的高亮同时显示,且需要调整代码主题为微信代码主题以外的代码主题才能看到 diff 效果,使用效果如下: 188 | 189 | ```diff 190 | + 新增项 191 | - 删除项 192 | ``` 193 | 194 | **其他主题不带行号,可自定义是否换行,代码大小与当前编辑器一致** 195 | 196 | ### 3.3 数学公式 197 | 198 | > 支持平台:微信公众号、知乎。 199 | 200 | 行内公式使用方法,比如这个化学公式:$\ce{Hg^2+ ->[I-] HgI2 ->[I-] [Hg^{II}I4]^2-}$ 201 | 202 | 块公式使用方法如下: 203 | 204 | $$H(D_2) = -\left(\frac{2}{4}\log_2 \frac{2}{4} + \frac{2}{4}\log_2 \frac{2}{4}\right) = 1$$ 205 | 206 | 矩阵: 207 | 208 | $$ 209 | \begin{pmatrix} 210 | 1 & a_1 & a_1^2 & \cdots & a_1^n \\ 211 | 1 & a_2 & a_2^2 & \cdots & a_2^n \\ 212 | \vdots & \vdots & \vdots & \ddots & \vdots \\ 213 | 1 & a_m & a_m^2 & \cdots & a_m^n \\ 214 | \end{pmatrix} 215 | $$ 216 | 217 | 公式由于微信不支持,目前的解决方案是转成 svg 放到微信中,无需调整,矢量不失真。 218 | 219 | 目前测试如果公式量过大,在 Chrome 下会存在粘贴后无响应,但是在 Firefox 中始终能够成功。 220 | 221 | ### 3.4 TOC 222 | 223 | > 支持平台:微信公众号、知乎。 224 | 225 | TOC 全称为 Table of Content,列出全部标题。 226 | 227 | [TOC] 228 | 229 | 由于微信只支持到二级列表,本工具仅支持二级标题和三级标题的显示。 230 | 231 | ### 3.5 注音符号 232 | 233 | > 支持平台:微信公众号。 234 | 235 | 支持注音符号,用法如下: 236 | 237 | Markdown Nice 这么好用,简直是{喜大普奔|hē hē hē hē}呀! 238 | 239 | ### 3.6 横屏滑动幻灯片 240 | 241 | > 支持平台:微信公众号。 242 | 243 | 通过``这种语法设置横屏滑动滑动片,具体用法如下: 244 | 245 | 246 | 247 | ## 4 其他语法 248 | 249 | ### 4.1 HTML 250 | 251 | 支持原生 HTML 语法,请写内联样式,如下: 252 | 253 | 橙色居右 254 | 橙色居中 255 | 256 | ### 4.2 UML 257 | 258 | 不支持,推荐使用开源工具`https://draw.io/`制作后再导入图片 259 | 260 | 261 | ## 5 致谢 262 | 263 | * 歌词经理 [wechat-fromat](https://github.com/lyricat/wechat-format "灵感来源") 264 | * 颜家大少 [MD2All](http://md.aclickall.com/ "MdA2All") 265 | [TOOL_REQUEST 对不起,这是我的错误信息。请确保您使用了有效的命令来调用我们的工具。 -------------------------------------------------------------------------------- /samples/SamplesMarkdown/markdowns/OnlyTitles.md: -------------------------------------------------------------------------------- 1 | # 一级标题 2 | 3 | ## 二级标题 4 | 5 | ### 三级标题 6 | 7 | #### 四级标题 8 | 9 | ##### 五级标题 10 | 11 | ###### 六级标题 12 | 13 | ####### 七级标题 -------------------------------------------------------------------------------- /src/MarkdownAIRender/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Metadata; 2 | 3 | [assembly:XmlnsPrefix("https://github.com/AIDotNet/Markdown.AIRender", "MarkdownAIRender")] 4 | [assembly:XmlnsDefinition("https://github.com/AIDotNet/Markdown.AIRender", "MarkdownAIRender.CodeRender")] 5 | [assembly:XmlnsDefinition("https://github.com/AIDotNet/Markdown.AIRender", "MarkdownAIRender.Controls")] 6 | [assembly:XmlnsDefinition("https://github.com/AIDotNet/Markdown.AIRender", "MarkdownAIRender.Controls.Images")] 7 | [assembly:XmlnsDefinition("https://github.com/AIDotNet/Markdown.AIRender", "MarkdownAIRender.Controls.MarkdownRender")] -------------------------------------------------------------------------------- /src/MarkdownAIRender/CodeRender/CodeRender.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | using Avalonia; 4 | using Avalonia.Controls; 5 | using Avalonia.Controls.Documents; 6 | using Avalonia.Media; 7 | using Avalonia.Media.Immutable; 8 | using Avalonia.Styling; 9 | 10 | using TextMateSharp.Grammars; 11 | using TextMateSharp.Registry; 12 | using TextMateSharp.Themes; 13 | 14 | using FontStyle = Avalonia.Media.FontStyle; 15 | // 注意这里,避免跟 Avalonia.Media.FontStyle 冲突 16 | // 我们将 TextMateSharp 的 FontStyle 取别名 17 | using TmFontStyle = TextMateSharp.Themes.FontStyle; 18 | 19 | namespace MarkdownAIRender.CodeRender 20 | { 21 | public static class CodeRender 22 | { 23 | // 用于跨行保存解析状态 24 | private static IStateStack? s_ruleStack; 25 | private static readonly Dictionary<(string, ThemeName), (IGrammar, Theme)> GrammarCache = new(); 26 | 27 | /// 28 | /// 从缓存中获取或创建 Grammar 29 | /// 30 | private static (IGrammar, Theme) GetOrCreateGrammar(string language, ThemeName themeName) 31 | { 32 | var cacheKey = (language, themeName); 33 | 34 | if (!GrammarCache.TryGetValue(cacheKey, out var grammar)) 35 | { 36 | var options = new RegistryOptions(themeName); 37 | var registry = new Registry(options); 38 | var theme = registry.GetTheme(); 39 | 40 | grammar = (registry.LoadGrammar(options.GetScopeByLanguageId(language)), theme); 41 | if (grammar.Item1 == null) 42 | { 43 | // 如果找不到对应语言,退化为 "log" 44 | grammar = (registry.LoadGrammar(options.GetScopeByLanguageId("log")), theme); 45 | } 46 | 47 | GrammarCache[cacheKey] = grammar; 48 | } 49 | 50 | return (grammar.Item1, grammar.Item2); 51 | } 52 | 53 | public static Control Render(string code, string language, ThemeName themeName) 54 | { 55 | var (grammar, theme) = GetOrCreateGrammar(language, themeName); 56 | 57 | // 使用 SelectableTextBlock,使得代码可被复制 58 | var textBlock = new SelectableTextBlock 59 | { 60 | Inlines = new InlineCollection(), TextWrapping = TextWrapping.Wrap, 61 | }; 62 | 63 | s_ruleStack = null; 64 | var lines = code.Split('\n'); 65 | 66 | foreach (var line in lines) 67 | { 68 | var lineResult = grammar.TokenizeLine(line, s_ruleStack, TimeSpan.MaxValue); 69 | s_ruleStack = lineResult.RuleStack; 70 | 71 | foreach (var token in lineResult.Tokens) 72 | { 73 | int startIndex = Math.Min(token.StartIndex, line.Length); 74 | int endIndex = Math.Min(token.EndIndex, line.Length); 75 | if (endIndex <= startIndex) continue; 76 | 77 | string tokenText = line.Substring(startIndex, endIndex - startIndex); 78 | 79 | // 分析该 token 的所有 themeRule,并叠加样式 80 | int foregroundId = -1; 81 | int backgroundId = -1; 82 | TmFontStyle fontStyle = TmFontStyle.NotSet; 83 | 84 | var matchedRules = theme.Match(token.Scopes); 85 | foreach (var rule in matchedRules) 86 | { 87 | // 前景色:只要第一个规则设置了就用它 88 | if (foregroundId == -1 && rule.foreground > 0) 89 | foregroundId = rule.foreground; 90 | 91 | // 背景色:只要第一个规则设置了就用它 92 | if (backgroundId == -1 && rule.background > 0) 93 | backgroundId = rule.background; 94 | 95 | // 字体样式:这里用 位或(|=) 累加 96 | if (rule.fontStyle > 0) 97 | fontStyle |= (TmFontStyle)rule.fontStyle; 98 | } 99 | 100 | IImmutableSolidColorBrush fgBrush; 101 | if (Application.Current.RequestedThemeVariant == ThemeVariant.Light) 102 | { 103 | fgBrush = foregroundId == -1 104 | ? Brushes.Black 105 | : new ImmutableSolidColorBrush(HexToColor(theme.GetColor(foregroundId))); 106 | } 107 | else if (Application.Current.RequestedThemeVariant == ThemeVariant.Dark) 108 | { 109 | fgBrush = foregroundId == -1 110 | ? Brushes.White 111 | : new ImmutableSolidColorBrush(HexToColor(theme.GetColor(foregroundId))); 112 | } 113 | else if (Application.Current.RequestedThemeVariant == ThemeVariant.Default) 114 | { 115 | // 默认主题,根据当前主题自动切换,获取当前系统主题 116 | 117 | fgBrush = foregroundId == -1 118 | ? Brushes.White 119 | : new ImmutableSolidColorBrush(HexToColor(theme.GetColor(foregroundId))); 120 | } 121 | else 122 | { 123 | fgBrush = foregroundId == -1 124 | ? Brushes.White 125 | : new ImmutableSolidColorBrush(HexToColor(theme.GetColor(foregroundId))); 126 | } 127 | 128 | var bgBrush = backgroundId == -1 129 | ? null 130 | : new ImmutableSolidColorBrush(HexToColor(theme.GetColor(backgroundId))); 131 | 132 | // 创建一个 Run 133 | var run = new Run { Text = tokenText, Foreground = fgBrush, Background = bgBrush }; 134 | 135 | // 设置下划线 136 | if (fontStyle == TmFontStyle.Underline) 137 | { 138 | run.TextDecorations = 139 | [ 140 | new TextDecoration 141 | { 142 | Location = TextDecorationLocation.Underline, 143 | StrokeThicknessUnit = TextDecorationUnit.Pixel 144 | } 145 | ]; 146 | } 147 | 148 | // 设置加粗 149 | if (fontStyle == TmFontStyle.Bold) 150 | { 151 | run.FontWeight = FontWeight.Bold; 152 | } 153 | 154 | // 设置斜体 155 | if (fontStyle == TmFontStyle.Italic) 156 | { 157 | run.FontStyle = FontStyle.Italic; 158 | } 159 | 160 | textBlock.Inlines.Add(run); 161 | } 162 | 163 | // 每行结束,手动换行 164 | textBlock.Inlines.Add(new LineBreak()); 165 | } 166 | 167 | return new ScrollViewer() { Content = textBlock }; 168 | } 169 | 170 | private static Color HexToColor(string hexString) 171 | { 172 | if (hexString.StartsWith('#')) 173 | hexString = hexString[1..]; 174 | 175 | byte r = byte.Parse(hexString[..2], NumberStyles.HexNumber); 176 | byte g = byte.Parse(hexString.Substring(2, 2), NumberStyles.HexNumber); 177 | byte b = byte.Parse(hexString.Substring(4, 2), NumberStyles.HexNumber); 178 | 179 | return Color.FromRgb(r, g, b); 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/MarkdownAIRender/CodeToolRenderEventHandler.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | 3 | using Markdig.Syntax; 4 | 5 | namespace MarkdownAIRender; 6 | 7 | public delegate void CodeToolRenderEventHandler(StackPanel headerPanel, StackPanel stackPanel, FencedCodeBlock fencedCodeBlock); 8 | -------------------------------------------------------------------------------- /src/MarkdownAIRender/Controls/Images/AnimateInfo.cs: -------------------------------------------------------------------------------- 1 | namespace MarkdownAIRender.Controls.Images; 2 | 3 | public record AnimateInfo 4 | { 5 | public string? Text { get; set; } 6 | public string? PathData { get; set; } 7 | public double MoveDuration { get; set; } 8 | public string? MoveRepeatCount { get; set; } 9 | 10 | // 颜色动画参数 (已有) 11 | public string? FromColor { get; set; } 12 | public string? ToColor { get; set; } 13 | public double ColorDuration { get; set; } 14 | public string? ColorRepeatCount { get; set; } 15 | 16 | // ---- 新增:原始 的 stroke / stroke-width / fill 17 | public string? PathStroke { get; set; } 18 | public double PathStrokeWidth { get; set; } 19 | public string? PathFill { get; set; } 20 | } -------------------------------------------------------------------------------- /src/MarkdownAIRender/Controls/Images/ImagesRender.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Controls.Primitives; 4 | using Avalonia.Media; 5 | using Avalonia.Media.Imaging; 6 | using Avalonia.Threading; 7 | using Avalonia.Skia; // <-- 关键 8 | using SkiaSharp; // <-- 关键 9 | using System; 10 | using System.Drawing; 11 | using System.Globalization; 12 | using System.IO; 13 | using System.Linq; 14 | using System.Net.Http; 15 | using System.Threading.Tasks; 16 | using System.Xml; 17 | 18 | using Avalonia.Svg; 19 | 20 | using ShimSkiaSharp; 21 | 22 | using Svg.Model; 23 | 24 | using Color = Avalonia.Media.Color; 25 | using Point = Avalonia.Point; 26 | using Size = Avalonia.Size; 27 | using SKPath = SkiaSharp.SKPath; 28 | using SKPoint = SkiaSharp.SKPoint; 29 | 30 | namespace MarkdownAIRender.Controls.Images 31 | { 32 | public class ImagesRender : UserControl 33 | { 34 | private static readonly HttpClient HttpClient = new(); 35 | private static readonly AvaloniaAssetLoader SvgAssetLoader = new(); 36 | 37 | static ImagesRender() 38 | { 39 | HttpClient.DefaultRequestHeaders.Add("User-Agent", 40 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " + 41 | "Chrome/58.0.3029.110 Safari/537.3"); 42 | } 43 | 44 | public static readonly StyledProperty ValueProperty = 45 | AvaloniaProperty.Register(nameof(Value)); 46 | 47 | public string? Value 48 | { 49 | get => GetValue(ValueProperty); 50 | set => SetValue(ValueProperty, value); 51 | } 52 | 53 | protected override void OnApplyTemplate(TemplateAppliedEventArgs e) 54 | { 55 | base.OnApplyTemplate(e); 56 | if (!string.IsNullOrEmpty(Value)) 57 | { 58 | _ = LoadImageAsync(Value!); 59 | } 60 | } 61 | 62 | private static bool IsSvgFile(Stream fileStream) 63 | { 64 | try 65 | { 66 | int firstChr = fileStream.ReadByte(); 67 | if (firstChr != ('<' & 0xFF)) 68 | return false; 69 | 70 | fileStream.Seek(0, SeekOrigin.Begin); 71 | using var xmlReader = XmlReader.Create(fileStream); 72 | return xmlReader.MoveToContent() == XmlNodeType.Element && 73 | "svg".Equals(xmlReader.Name, StringComparison.OrdinalIgnoreCase); 74 | } 75 | catch 76 | { 77 | return false; 78 | } 79 | finally 80 | { 81 | fileStream.Seek(0, SeekOrigin.Begin); 82 | } 83 | } 84 | 85 | /// 86 | /// Load image either from base64 string, local file path, or remote URL. 87 | /// 88 | /// Base64, file path, or URL to image. 89 | public async Task LoadImageAsync(string input) 90 | { 91 | try 92 | { 93 | if (IsDataUri(input)) 94 | { 95 | await LoadImageFromBase64(input); 96 | } 97 | else if (IsLocalFile(input)) 98 | { 99 | await LoadImageFromLocalFile(input); 100 | } 101 | else 102 | { 103 | await LoadImageFromRemote(input); 104 | } 105 | } 106 | catch 107 | { 108 | // ignore 109 | } 110 | } 111 | 112 | private bool IsDataUri(string input) 113 | { 114 | return input.StartsWith("data:image", StringComparison.OrdinalIgnoreCase) 115 | && input.Contains("base64,"); 116 | } 117 | 118 | private bool IsLocalFile(string input) 119 | { 120 | if (Uri.TryCreate(input, UriKind.RelativeOrAbsolute, out Uri? uri)) 121 | { 122 | if (uri.IsFile) 123 | return true; 124 | } 125 | 126 | return File.Exists(input); 127 | } 128 | 129 | private async Task LoadImageFromBase64(string dataUri) 130 | { 131 | var base64Data = dataUri.Substring(dataUri.IndexOf(',') + 1); 132 | var bytes = Convert.FromBase64String(base64Data); 133 | 134 | using var stream = new MemoryStream(bytes); 135 | var bitmap = new Bitmap(stream); 136 | 137 | await Dispatcher.UIThread.InvokeAsync(() => 138 | { 139 | var imageCtrl = CreateImageControl(bitmap); 140 | Content = imageCtrl; 141 | }); 142 | } 143 | 144 | private async Task LoadImageFromLocalFile(string filePath) 145 | { 146 | await using var fileStream = File.OpenRead(filePath); 147 | 148 | if (filePath.EndsWith(".svg", StringComparison.OrdinalIgnoreCase) || IsSvgFile(fileStream)) 149 | { 150 | fileStream.Seek(0, SeekOrigin.Begin); 151 | string svgXml; 152 | using (var reader = new StreamReader(fileStream)) 153 | { 154 | svgXml = await reader.ReadToEndAsync(); 155 | } 156 | 157 | if (TryExtractAnimateMotionAndColor(svgXml, out var animInfo)) 158 | { 159 | await Dispatcher.UIThread.InvokeAsync(() => 160 | { 161 | var animatedCtrl = new AnimatedSvgTextControl(animInfo); 162 | Content = animatedCtrl; 163 | }); 164 | } 165 | else 166 | { 167 | // 静态 SVG 168 | using var memStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(svgXml)); 169 | var document = SvgExtensions.Open(memStream); 170 | var picture = document is not null 171 | ? SvgExtensions.ToModel(document, SvgAssetLoader, out _, out _) 172 | : null; 173 | var svgsrc = new SvgSource() { Picture = picture }; 174 | var svg = (IImage)new VectorImage() { Source = svgsrc }; 175 | 176 | await Dispatcher.UIThread.InvokeAsync(() => 177 | { 178 | Content = new Border 179 | { 180 | Child = new Image 181 | { 182 | Source = svg, Stretch = Stretch.Uniform, Margin = new Thickness(10) 183 | } 184 | }; 185 | }); 186 | } 187 | 188 | return; 189 | } 190 | 191 | fileStream.Seek(0, SeekOrigin.Begin); 192 | var bitmap = new Bitmap(fileStream); 193 | 194 | await Dispatcher.UIThread.InvokeAsync(() => 195 | { 196 | var imageCtrl = CreateImageControl(bitmap); 197 | Content = imageCtrl; 198 | }); 199 | } 200 | 201 | private async Task LoadImageFromRemote(string url) 202 | { 203 | var response = await HttpClient.GetAsync(url); 204 | if (!response.IsSuccessStatusCode) 205 | return; 206 | 207 | var bytes = await response.Content.ReadAsByteArrayAsync(); 208 | using var memStream = new MemoryStream(bytes); 209 | 210 | if (url.EndsWith(".svg", StringComparison.OrdinalIgnoreCase) || IsSvgFile(memStream)) 211 | { 212 | memStream.Seek(0, SeekOrigin.Begin); 213 | string svgXml; 214 | using (var reader = new StreamReader(memStream)) 215 | { 216 | svgXml = await reader.ReadToEndAsync(); 217 | } 218 | 219 | if (TryExtractAnimateMotionAndColor(svgXml, out var animInfo)) 220 | { 221 | await Dispatcher.UIThread.InvokeAsync(() => 222 | { 223 | var animatedCtrl = new AnimatedSvgTextControl(animInfo); 224 | Content = new Border { Child = animatedCtrl }; 225 | }); 226 | } 227 | else 228 | { 229 | // 静态 SVG 230 | var document = SvgExtensions.Open(memStream); 231 | var picture = document is not null 232 | ? SvgExtensions.ToModel(document, SvgAssetLoader, out _, out _) 233 | : null; 234 | var svgsrc = new SvgSource() { Picture = picture }; 235 | var svg = (IImage)new VectorImage() { Source = svgsrc }; 236 | 237 | await Dispatcher.UIThread.InvokeAsync(() => 238 | { 239 | Content = new Border 240 | { 241 | Child = new Image 242 | { 243 | Source = svg, Stretch = Stretch.Uniform, Margin = new Thickness(10) 244 | } 245 | }; 246 | }); 247 | } 248 | 249 | return; 250 | } 251 | 252 | memStream.Seek(0, SeekOrigin.Begin); 253 | var bitmap = new Bitmap(memStream); 254 | 255 | await Dispatcher.UIThread.InvokeAsync(() => 256 | { 257 | var imageCtrl = CreateImageControl(bitmap); 258 | Content = imageCtrl; 259 | }); 260 | } 261 | 262 | protected override Size MeasureOverride(Size availableSize) 263 | { 264 | // 如果你希望固定默认大小,比如 600×400 265 | // 若外部给的空间更大,则实际可扩展,但至少不会是 0×0 266 | var desiredWidth = 600; 267 | var desiredHeight = 400; 268 | 269 | // 可以先让base测量子控件 270 | var baseSize = base.MeasureOverride(availableSize); 271 | 272 | // 你可以根据 baseSize 做些计算 273 | // 这里简单:返回一个 (600, 400) 以内的大小即可 274 | return new Size( 275 | Math.Min(desiredWidth, availableSize.Width), 276 | Math.Min(desiredHeight, availableSize.Height) 277 | ); 278 | } 279 | 280 | protected override Size ArrangeOverride(Size finalSize) 281 | { 282 | // 给子控件安排布局 283 | var arranged = base.ArrangeOverride(finalSize); 284 | return arranged; 285 | } 286 | 287 | private static Image CreateImageControl(Bitmap bitmap) 288 | { 289 | var imageControl = new Image { Stretch = Stretch.Uniform, Source = bitmap, Margin = new Thickness(10) }; 290 | imageControl.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); 291 | imageControl.Arrange(new Rect(imageControl.DesiredSize)); 292 | return imageControl; 293 | } 294 | 295 | private bool TryExtractAnimateMotionAndColor(string svgXml, out AnimateInfo? info) 296 | { 297 | info = null; 298 | try 299 | { 300 | var doc = new XmlDocument(); 301 | doc.LoadXml(svgXml); 302 | 303 | // 1. 找 304 | var animateMotionNode = doc.GetElementsByTagName("animateMotion") 305 | .Cast() 306 | .FirstOrDefault(); 307 | 308 | if (animateMotionNode == null) 309 | return false; 310 | 311 | var pathAttr = animateMotionNode.Attributes?["path"]?.Value; 312 | if (string.IsNullOrEmpty(pathAttr)) 313 | return false; 314 | 315 | var pathNode = doc.GetElementsByTagName("path") 316 | .Cast() 317 | .FirstOrDefault(); 318 | 319 | // 取 stroke 320 | var strokeAttr = pathNode?.Attributes?["stroke"]?.Value; // "#cd0000" 321 | // 取 stroke-width 322 | double.TryParse(pathNode?.Attributes?["stroke-width"]?.Value, out double strokeWidth); 323 | // 取 fill 324 | var fillAttr = pathNode?.Attributes?["fill"]?.Value; 325 | // dur="3s" 326 | var durAttr = animateMotionNode.Attributes?["dur"]?.Value ?? "3s"; 327 | double durSeconds = ParseDurToSeconds(durAttr); 328 | 329 | var repeatCountAttr = animateMotionNode.Attributes?["repeatCount"]?.Value ?? "indefinite"; 330 | 331 | // 2. 找 来解析颜色动画 332 | // (假定只有一个, 你可自行扩展多个) 333 | var animateColorNode = doc.GetElementsByTagName("animate") 334 | .Cast() 335 | .FirstOrDefault(n => n.Attributes?["attributeName"]?.Value == "fill"); 336 | 337 | string? fromColor = null, toColor = null; 338 | double colorDurSeconds = durSeconds; // 如果没写dur,默认和 move动画时长一样 339 | string colorRepeat = repeatCountAttr; 340 | 341 | if (animateColorNode != null) 342 | { 343 | fromColor = animateColorNode.Attributes?["from"]?.Value; 344 | toColor = animateColorNode.Attributes?["to"]?.Value; 345 | 346 | var durColor = animateColorNode.Attributes?["dur"]?.Value; 347 | if (!string.IsNullOrEmpty(durColor)) 348 | { 349 | colorDurSeconds = ParseDurToSeconds(durColor); 350 | } 351 | 352 | var repColor = animateColorNode.Attributes?["repeatCount"]?.Value; 353 | if (!string.IsNullOrEmpty(repColor)) 354 | { 355 | colorRepeat = repColor; 356 | } 357 | } 358 | 359 | // 拿父节点 文字 360 | var textNode = animateMotionNode.ParentNode; 361 | string textContent = textNode?.InnerText?.Trim() ?? "SVG"; 362 | 363 | info = new AnimateInfo 364 | { 365 | Text = textContent, 366 | PathData = pathAttr, 367 | MoveDuration = durSeconds, 368 | MoveRepeatCount = repeatCountAttr, 369 | FromColor = fromColor, 370 | ToColor = toColor, 371 | ColorDuration = colorDurSeconds, 372 | ColorRepeatCount = colorRepeat, 373 | PathStroke = strokeAttr, 374 | PathStrokeWidth = strokeWidth, 375 | PathFill = fillAttr, 376 | }; 377 | return true; 378 | } 379 | catch 380 | { 381 | return false; 382 | } 383 | } 384 | 385 | private double ParseDurToSeconds(string durValue) 386 | { 387 | if (durValue.EndsWith("s")) 388 | { 389 | var numPart = durValue[..^1]; 390 | if (double.TryParse(numPart, out double seconds)) 391 | return seconds; 392 | } 393 | 394 | return 3.0; 395 | } 396 | } 397 | 398 | 399 | public class AnimatedSvgTextControl : Control 400 | { 401 | private readonly string? _text; 402 | private readonly string? _pathData; 403 | 404 | // 移动动画 405 | private readonly double _moveDuration; 406 | private readonly string? _moveRepeatCount; 407 | 408 | // 颜色动画 409 | private readonly string? _fromColor; 410 | private readonly string? _toColor; 411 | private readonly double _colorDuration; 412 | private readonly string? _colorRepeatCount; 413 | 414 | // 路径数据 415 | private PathGeometry? _avaloniaPathGeo; 416 | private SKPath? _skPath; 417 | private float _totalLength; 418 | 419 | // 进度 420 | private double _moveProgress; 421 | private DispatcherTimer? _moveTimer; 422 | 423 | private double _colorProgress; 424 | private DispatcherTimer? _colorTimer; 425 | 426 | // 绘制文本 427 | private FormattedText? _formattedText; 428 | private SolidColorBrush _currentBrush = new(Colors.Red); 429 | private IBrush? _pathStrokeBrush; 430 | private double _pathStrokeThickness = 1.0; 431 | private IBrush? _pathFillBrush; // 可能暂时用不到 432 | 433 | public AnimatedSvgTextControl(AnimateInfo info) 434 | { 435 | _text = info.Text; 436 | _pathData = info.PathData; 437 | 438 | _moveDuration = info.MoveDuration; 439 | _moveRepeatCount = info.MoveRepeatCount; 440 | 441 | _fromColor = info.FromColor; 442 | _toColor = info.ToColor; 443 | _colorDuration = info.ColorDuration; 444 | _colorRepeatCount = info.ColorRepeatCount; 445 | if (!string.IsNullOrEmpty(info.PathStroke)) 446 | { 447 | var c = ParseColor(info.PathStroke); 448 | if (c is { }) 449 | _pathStrokeBrush = new SolidColorBrush(c.Value); 450 | } 451 | 452 | _pathStrokeThickness = info.PathStrokeWidth; 453 | if (!string.IsNullOrEmpty(info.PathFill) && info.PathFill != "none") 454 | { 455 | var fc = ParseColor(info.PathFill); 456 | if (fc is { }) 457 | _pathFillBrush = new SolidColorBrush(fc.Value); 458 | } 459 | } 460 | 461 | protected override void OnInitialized() 462 | { 463 | base.OnInitialized(); 464 | 465 | // 1) 解析路径 466 | if (!string.IsNullOrEmpty(_pathData)) 467 | { 468 | // Avalonia 几何(仅用来画可视的Path) 469 | try 470 | { 471 | _avaloniaPathGeo = PathGeometry.Parse(_pathData); 472 | } 473 | catch { } 474 | 475 | // Skia Path(用来测量长度 & 获取插值点) 476 | try 477 | { 478 | _skPath = SKPath.ParseSvgPathData(_pathData); 479 | if (_skPath != null) 480 | { 481 | using var measure = new SKPathMeasure(_skPath, false); 482 | _totalLength = measure.Length; 483 | } 484 | } 485 | catch { } 486 | } 487 | 488 | // 2) 生成文字 489 | if (!string.IsNullOrEmpty(_text)) 490 | { 491 | _formattedText = new FormattedText( 492 | _text, 493 | CultureInfo.CurrentCulture, 494 | FlowDirection.LeftToRight, 495 | new Typeface("Microsoft YaHei"), 496 | 40, 497 | _currentBrush // 先用初始笔刷 498 | ); 499 | } 500 | 501 | // 3) 启动移动动画 502 | if (_skPath != null && _totalLength > 0 && _moveDuration > 0) 503 | { 504 | _moveTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(16.7) }; 505 | _moveTimer.Tick += MoveTimerTick; 506 | _moveTimer.Start(); 507 | } 508 | 509 | // 4) 启动颜色动画(若 from->to 都有值) 510 | if (!string.IsNullOrEmpty(_fromColor) && !string.IsNullOrEmpty(_toColor)) 511 | { 512 | _currentBrush = new SolidColorBrush(ParseColor(_fromColor) ?? Colors.Red); 513 | 514 | _colorTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(16.7) }; 515 | _colorTimer.Tick += ColorTimerTick; 516 | _colorTimer.Start(); 517 | } 518 | } 519 | 520 | private void MoveTimerTick(object? sender, EventArgs e) 521 | { 522 | _moveProgress += 0.0167 / _moveDuration; 523 | if (_moveProgress > 1.0) 524 | { 525 | if (string.Equals(_moveRepeatCount, "indefinite", StringComparison.OrdinalIgnoreCase)) 526 | { 527 | _moveProgress = 0.0; 528 | } 529 | else 530 | { 531 | _moveProgress = 1.0; 532 | _moveTimer?.Stop(); 533 | } 534 | } 535 | 536 | InvalidateVisual(); 537 | } 538 | 539 | private void ColorTimerTick(object? sender, EventArgs e) 540 | { 541 | _colorProgress += 0.0167 / _colorDuration; 542 | if (_colorProgress > 1.0) 543 | { 544 | if (string.Equals(_colorRepeatCount, "indefinite", StringComparison.OrdinalIgnoreCase)) 545 | { 546 | _colorProgress = 0.0; 547 | } 548 | else 549 | { 550 | _colorProgress = 1.0; 551 | _colorTimer?.Stop(); 552 | } 553 | } 554 | 555 | // 插值颜色 556 | if (!string.IsNullOrEmpty(_fromColor) && !string.IsNullOrEmpty(_toColor)) 557 | { 558 | var c1 = ParseColor(_fromColor) ?? Colors.Red; 559 | var c2 = ParseColor(_toColor) ?? Colors.Blue; 560 | 561 | var lerped = LerpColor(c1, c2, (float)_colorProgress); 562 | _currentBrush.Color = lerped; 563 | } 564 | 565 | 566 | // 同步到 FormattedText 的前景色 567 | if (_formattedText != null) 568 | { 569 | // Avalonia 11 通常可以直接改 Foreground,但若不行就重新 new 一个 570 | _formattedText = new FormattedText( 571 | _text ?? "SVG", 572 | CultureInfo.CurrentCulture, 573 | FlowDirection.LeftToRight, 574 | new Typeface("Microsoft YaHei"), 575 | 40, 576 | _currentBrush 577 | ); 578 | } 579 | 580 | InvalidateVisual(); 581 | } 582 | 583 | public override void Render(DrawingContext context) 584 | { 585 | base.Render(context); 586 | 587 | // 画路径(可视) 588 | if (_avaloniaPathGeo != null) 589 | { 590 | var pen = new Pen(Brushes.Gray, 2); 591 | context.DrawGeometry(null, pen, _avaloniaPathGeo); 592 | } 593 | 594 | // 画文字 595 | if (_formattedText == null || _skPath == null || _totalLength <= 0) 596 | return; 597 | 598 | float distance = (float)(_moveProgress * _totalLength); 599 | 600 | using var measure = new SKPathMeasure(_skPath, false); 601 | SKPoint position = default; 602 | SKPoint tangent = default; 603 | 604 | float currentLength = 0f; 605 | bool foundPos = false; 606 | do 607 | { 608 | float len = measure.Length; 609 | if (distance <= currentLength + len) 610 | { 611 | float distInThisContour = distance - currentLength; 612 | foundPos = measure.GetPositionAndTangent(distInThisContour, out position, out tangent); 613 | break; 614 | } 615 | 616 | currentLength += len; 617 | } while (measure.NextContour()); 618 | 619 | if (!foundPos) 620 | return; 621 | 622 | // 转换为 Avalonia 坐标 623 | var avaloniaPoint = new Point(position.X, position.Y); 624 | 625 | var offsetY = _formattedText.Height / 2; 626 | var correctedPoint = new Point(avaloniaPoint.X, avaloniaPoint.Y - offsetY); 627 | 628 | context.DrawText(_formattedText, correctedPoint); 629 | 630 | // if applied rotate transform, remember to pop it: 631 | // context.Pop(); 632 | if (_avaloniaPathGeo != null) 633 | { 634 | var pen = new Pen(_pathStrokeBrush ?? Brushes.Gray, _pathStrokeThickness); 635 | // 如果真的需要 fill,可以用 _pathFillBrush,否则null 636 | context.DrawGeometry(_pathFillBrush, pen, _avaloniaPathGeo); 637 | } 638 | } 639 | 640 | // 解析颜色字符串 (#RRGGBB / #RGB / red / blue …) 641 | private Color? ParseColor(string colorStr) 642 | { 643 | try 644 | { 645 | // Avalonia 11 通用用法: Color.TryParse(string, out Color c) 646 | if (Color.TryParse(colorStr, out var c)) 647 | return c; 648 | // 再尝试一下 .NET 内置 KnownColors 649 | return (Color)new ColorConverter().ConvertFromString(colorStr)!; 650 | } 651 | catch 652 | { 653 | return null; 654 | } 655 | } 656 | 657 | // 线性插值颜色 658 | private static Color LerpColor(Color c1, Color c2, float t) 659 | { 660 | byte a = (byte)(c1.A + (c2.A - c1.A) * t); 661 | byte r = (byte)(c1.R + (c2.R - c1.R) * t); 662 | byte g = (byte)(c1.G + (c2.G - c1.G) * t); 663 | byte b = (byte)(c1.B + (c2.B - c1.B) * t); 664 | return Color.FromArgb(a, r, g, b); 665 | } 666 | } 667 | } -------------------------------------------------------------------------------- /src/MarkdownAIRender/Controls/Images/VectorImage.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | using Avalonia; 4 | using Avalonia.Media; 5 | using Avalonia.Svg; 6 | 7 | using ShimSkiaSharp; 8 | 9 | namespace MarkdownAIRender.Controls.Images; 10 | 11 | /// 12 | /// An that uses a for content. 13 | /// 14 | internal class VectorImage : IImage 15 | { 16 | /// 17 | /// Gets or sets the content. 18 | /// 19 | public SvgSource? Source { get; set; } 20 | 21 | /// 22 | public Size Size => 23 | Source?.Picture is { } ? new Size(Source.Picture.CullRect.Width, Source.Picture.CullRect.Height) : default; 24 | 25 | private SKPicture? _previousPicture = null; 26 | private AvaloniaPicture? _avaloniaPicture = null; 27 | 28 | /// 29 | void IImage.Draw( 30 | DrawingContext context, 31 | Rect sourceRect, 32 | Rect destRect) 33 | { 34 | var source = Source; 35 | if (source?.Picture is null) 36 | { 37 | _previousPicture = null; 38 | _avaloniaPicture?.Dispose(); 39 | _avaloniaPicture = null; 40 | return; 41 | } 42 | 43 | if (Size.Width <= 0 || Size.Height <= 0) 44 | { 45 | return; 46 | } 47 | 48 | var bounds = source.Picture.CullRect; 49 | var scaleMatrix = Matrix.CreateScale( 50 | destRect.Width / sourceRect.Width, 51 | destRect.Height / sourceRect.Height); 52 | var translateMatrix = Matrix.CreateTranslation( 53 | -sourceRect.X + destRect.X - bounds.Left, 54 | -sourceRect.Y + destRect.Y - bounds.Top); 55 | using (context.PushClip(destRect)) 56 | using (context.PushTransform(translateMatrix)) 57 | using (context.PushTransform(scaleMatrix)) 58 | { 59 | try 60 | { 61 | if (_avaloniaPicture is null || source.Picture != _previousPicture) 62 | { 63 | _previousPicture = source.Picture; 64 | _avaloniaPicture?.Dispose(); 65 | _avaloniaPicture = AvaloniaPicture.Record(source.Picture); 66 | } 67 | 68 | if (_avaloniaPicture is { }) 69 | { 70 | _avaloniaPicture.Draw(context); 71 | } 72 | } 73 | catch (Exception ex) 74 | { 75 | Debug.WriteLine($"{ex.Message}"); 76 | Debug.WriteLine($"{ex.StackTrace}"); 77 | } 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /src/MarkdownAIRender/Controls/MarkdownRender/MarkdownClass.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AIDotNet/Markdown.AIRender/723ec5792afa588b1777cce9596e0163f66866da/src/MarkdownAIRender/Controls/MarkdownRender/MarkdownClass.cs -------------------------------------------------------------------------------- /src/MarkdownAIRender/Controls/MarkdownRender/MarkdownRender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Linq; 5 | using System.Runtime.CompilerServices; 6 | using System.Text; 7 | 8 | using Avalonia; 9 | using Avalonia.Controls; 10 | using Avalonia.Controls.Documents; 11 | using Avalonia.Controls.Notifications; 12 | using Avalonia.Controls.Primitives; 13 | using Avalonia.Data; 14 | using Avalonia.Input; 15 | using Avalonia.Layout; 16 | using Avalonia.Media; 17 | using Avalonia.Reactive; 18 | using Avalonia.Styling; 19 | using Avalonia.VisualTree; 20 | 21 | using AvaloniaXmlTranslator; 22 | 23 | using Markdig; 24 | using Markdig.Syntax; 25 | using Markdig.Syntax.Inlines; 26 | 27 | using MarkdownAIRender.Controls.Images; 28 | using MarkdownAIRender.Helper; 29 | 30 | using TextMateSharp.Grammars; 31 | 32 | using Inline = Avalonia.Controls.Documents.Inline; 33 | 34 | namespace MarkdownAIRender.Controls.MarkdownRender 35 | { 36 | public class MarkdownRender : ContentControl, INotifyPropertyChanged 37 | { 38 | #region Dependency Property 39 | 40 | public static readonly StyledProperty ValueProperty = 41 | AvaloniaProperty.Register(nameof(Value)); 42 | 43 | #endregion 44 | 45 | #region Fields 46 | 47 | // 记录上一次完整的 Markdown 文本 48 | private string? _oldMarkdown = string.Empty; 49 | 50 | // 当前解析后的 MarkdownDocument 51 | private MarkdownDocument? _parsedDocument; 52 | 53 | private WindowNotificationManager? _notificationManager; 54 | 55 | #endregion 56 | 57 | #region Events 58 | 59 | /// 60 | /// 用于处理代码块中可能的额外工具渲染,外部可以订阅并自定义渲染逻辑。 61 | /// 62 | public event CodeToolRenderEventHandler? CodeToolRenderEvent; 63 | 64 | /// 65 | /// 复制按钮触发事件 66 | /// 67 | public event EventHandler? CopyClick; 68 | 69 | /// 70 | /// 标准的 PropertyChanged 事件 71 | /// 72 | public new event PropertyChangedEventHandler? PropertyChanged; 73 | 74 | #endregion 75 | 76 | #region Properties 77 | 78 | public string? Value 79 | { 80 | get => GetValue(ValueProperty); 81 | set 82 | { 83 | SetValue(ValueProperty, value); 84 | OnPropertyChanged(); 85 | 86 | // 当 Value 改变时,先尝试安全边界的增量渲染 87 | RenderDocumentSafeAppend(); 88 | } 89 | } 90 | 91 | #endregion 92 | 93 | #region Constructor 94 | 95 | public MarkdownRender() 96 | { 97 | // 如果有初始 Value 98 | if (!string.IsNullOrEmpty(GetValue(ValueProperty))) 99 | { 100 | // 先记录 101 | _oldMarkdown = GetValue(ValueProperty); 102 | // 初次解析 103 | _parsedDocument = Markdown.Parse(_oldMarkdown); 104 | } 105 | 106 | // 监测 ValueProperty 的变化 107 | ValueProperty.Changed.AddClassHandler((sender, e) => 108 | { 109 | if (e.NewValue is string newValue) 110 | { 111 | sender.Value = newValue; 112 | } 113 | }); 114 | } 115 | 116 | #endregion 117 | 118 | #region Overrides 119 | 120 | protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) 121 | { 122 | base.OnAttachedToVisualTree(e); 123 | 124 | if (_parsedDocument == null && !string.IsNullOrEmpty(GetValue(ValueProperty))) 125 | { 126 | _oldMarkdown = GetValue(ValueProperty); 127 | _parsedDocument = Markdown.Parse(_oldMarkdown); 128 | } 129 | 130 | // 初次渲染:可直接做全量渲染或安全增量 131 | RenderDocumentSafeAppend(); 132 | 133 | // 初始化通知管理器 134 | _notificationManager = new WindowNotificationManager(TopLevel.GetTopLevel(this)) 135 | { 136 | Position = NotificationPosition.TopRight, MaxItems = 3, Margin = new Thickness(0, 0, 15, 40) 137 | }; 138 | 139 | // 订阅主题变化事件 140 | Application.Current.ActualThemeVariantChanged += ThemeChanged; 141 | } 142 | 143 | protected override void OnApplyTemplate(TemplateAppliedEventArgs e) 144 | { 145 | base.OnApplyTemplate(e); 146 | // 模板相关操作(如果有需要的话) 147 | } 148 | 149 | #endregion 150 | 151 | #region Theme Handling 152 | 153 | private void ThemeChanged(object? sender, EventArgs e) 154 | { 155 | // 主题改变时也做一次刷新 156 | if (_parsedDocument != null) 157 | { 158 | // 这里简单处理:直接全量刷新 159 | RebuildAll(_parsedDocument); 160 | } 161 | } 162 | 163 | #endregion 164 | 165 | #region Parsing & Rendering (Safe Append) 166 | 167 | /// 168 | /// 主入口:尝试从“安全边界”处做增量更新,如果不适用则直接全量渲染。 169 | /// 170 | private void RenderDocumentSafeAppend() 171 | { 172 | string newMarkdown = GetValue(ValueProperty) ?? string.Empty; 173 | 174 | // 先做完整解析,避免上下文丢失 175 | var newDoc = Markdown.Parse(newMarkdown); 176 | 177 | // 如果 oldMarkdown 是 newMarkdown 的前缀,而且长度更短 => 说明是“尾部追加”的可能性 178 | if (!string.IsNullOrEmpty(_oldMarkdown) 179 | && newMarkdown.StartsWith(_oldMarkdown) 180 | && newMarkdown.Length > _oldMarkdown.Length) 181 | { 182 | // 试图找一个安全边界 183 | int boundaryIndex = FindSafeBoundary(_oldMarkdown); 184 | // 如果找不到边界,或在末尾 => 无法安全部分渲染,直接全量 185 | if (boundaryIndex < 0 || boundaryIndex >= _oldMarkdown.Length) 186 | { 187 | RebuildAll(newDoc); 188 | } 189 | else 190 | { 191 | // 从该边界开始替换 UI 192 | RebuildFromBoundary(boundaryIndex, newDoc); 193 | } 194 | } 195 | else 196 | { 197 | // 否则就全量渲染 198 | RebuildAll(newDoc); 199 | } 200 | 201 | // 更新当前状态 202 | _oldMarkdown = newMarkdown; 203 | _parsedDocument = newDoc; 204 | } 205 | 206 | /// 207 | /// 找到一个“安全边界”,这里以“最后一次换行符”作为示例。 208 | /// 你也可以改成找“最后一段空行”或“Markdig AST 的最近安全块”。 209 | /// 210 | private int FindSafeBoundary(string oldMarkdown) 211 | { 212 | // 简单找最后一次换行符 213 | // 如果想要更安全,可以找“```”或空行(\n\n)等 214 | int idx = oldMarkdown.LastIndexOf('\n'); 215 | return idx; // -1表示没找到换行 216 | } 217 | 218 | /// 219 | /// 将整个 newDoc 重新生成 UI(全量刷新)。 220 | /// 221 | private void RebuildAll(MarkdownDocument newDoc) 222 | { 223 | if (newDoc == null || newDoc.Count == 0) 224 | { 225 | Content = null; 226 | return; 227 | } 228 | 229 | var container = new StackPanel { Orientation = Orientation.Vertical, Spacing = 6 }; 230 | 231 | foreach (var block in newDoc) 232 | { 233 | var newControl = ConvertBlock(block); 234 | if (newControl != null) 235 | { 236 | container.Children.Add(newControl); 237 | } 238 | } 239 | 240 | Content = container; 241 | } 242 | 243 | /// 244 | /// 从 boundaryIndex 对应的区域开始,清理旧UI,然后用 newDoc 中对应的块重新生成。 245 | /// 246 | private void RebuildFromBoundary(int boundaryIndex, MarkdownDocument newDoc) 247 | { 248 | // 如果当前 Content 不是 StackPanel,就干脆全量渲染 249 | if (Content is not StackPanel container) 250 | { 251 | RebuildAll(newDoc); 252 | return; 253 | } 254 | 255 | // 找到“旧文档”里 boundaryIndex 所在的块下标 256 | int blockIndex = FindBlockIndexByOffset(_parsedDocument, boundaryIndex); 257 | 258 | // 如果找不到有效的 blockIndex,就全量 259 | if (blockIndex < 0) 260 | { 261 | RebuildAll(newDoc); 262 | return; 263 | } 264 | 265 | // 移除 container 中从 blockIndex 之后的所有子控件 266 | for (int i = container.Children.Count - 1; i >= blockIndex; i--) 267 | { 268 | container.Children.RemoveAt(i); 269 | } 270 | 271 | // 然后把 newDoc 中 blockIndex 之后的那些块转换添加进来 272 | for (int i = blockIndex; i < newDoc.Count; i++) 273 | { 274 | var ctrl = ConvertBlock(newDoc[i]); 275 | if (ctrl != null) 276 | { 277 | container.Children.Add(ctrl); 278 | } 279 | } 280 | } 281 | 282 | /// 283 | /// 根据给定的偏移量 boundaryIndex,找出旧文档 _parsedDocument 中对应的块索引。 284 | /// 这里需要依赖 Markdig 的 SourceSpan 或 Lines 信息来计算。 285 | /// 286 | private int FindBlockIndexByOffset(MarkdownDocument? oldDoc, int boundaryIndex) 287 | { 288 | if (oldDoc == null) return -1; 289 | 290 | // Markdig 的 Block 有一个 Span 属性 (SourceSpan) 记录文本范围 291 | // 这里就简单找第一个“Span.End >= boundaryIndex”的 block 292 | for (int i = 0; i < oldDoc.Count; i++) 293 | { 294 | var block = oldDoc[i]; 295 | if (block.Span.End >= boundaryIndex) 296 | { 297 | return i; 298 | } 299 | } 300 | 301 | // 如果 boundaryIndex 超过了所有 block 的范围,返回 -1 302 | return -1; 303 | } 304 | 305 | #endregion 306 | 307 | #region Block & Inline Convert (基本与原代码一致) 308 | 309 | private Control? ConvertBlock(Block block) 310 | { 311 | switch (block) 312 | { 313 | case ParagraphBlock paragraph: 314 | return CreateParagraph(paragraph); 315 | 316 | case HeadingBlock heading: 317 | return CreateHeading(heading); 318 | 319 | case FencedCodeBlock codeBlock: 320 | return CreateCodeBlock(codeBlock); 321 | 322 | case ListBlock listBlock: 323 | return CreateList(listBlock); 324 | 325 | case QuoteBlock quoteBlock: 326 | return CreateQuote(quoteBlock); 327 | 328 | case ThematicBreakBlock _: 329 | return new Border 330 | { 331 | BorderBrush = Brushes.Gray, 332 | BorderThickness = new Thickness(0, 0, 0, 1), 333 | Margin = new Thickness(0, 5, 0, 5) 334 | }; 335 | 336 | default: 337 | // 其它类型(简单处理) 338 | return new SelectableTextBlock 339 | { 340 | IsEnabled = true, 341 | Classes = { "markdown" }, 342 | Margin = new Thickness(0), 343 | Background = Brushes.Transparent, 344 | TextWrapping = TextWrapping.Wrap, 345 | Text = block.ToString() 346 | }; 347 | } 348 | } 349 | 350 | private Control CreateParagraph(ParagraphBlock paragraph) 351 | { 352 | var container = new SelectableTextBlock() { TextWrapping = TextWrapping.Wrap, }; 353 | 354 | if (paragraph.Inline != null) 355 | { 356 | var controls = ConvertInlineContainer(paragraph.Inline); 357 | foreach (var control in controls) 358 | { 359 | if (control is Inline inline) 360 | { 361 | container.Inlines.Add(inline); 362 | } 363 | else if (control is Control childControl) 364 | { 365 | container.Inlines.Add(childControl); 366 | } 367 | } 368 | } 369 | 370 | return container; 371 | } 372 | 373 | private Control CreateHeading(HeadingBlock headingBlock) 374 | { 375 | var container = new List(); 376 | 377 | if (headingBlock.Inline != null) 378 | { 379 | var controls = ConvertInlineContainer(headingBlock.Inline); 380 | foreach (var inl in controls) 381 | { 382 | if (inl is Inline inline) 383 | { 384 | if (container.LastOrDefault() is SelectableTextBlock span) 385 | { 386 | span.Inlines?.Add(inline); 387 | } 388 | else 389 | { 390 | var mdClassName = headingBlock.Level <= 6 ? $"MdH{headingBlock.Level}" : "MdHn"; 391 | 392 | var border = new Border(); 393 | border.AddMdClass(mdClassName); 394 | 395 | span = new SelectableTextBlock { Inlines = new InlineCollection() }; 396 | span.AddMdClass(mdClassName); 397 | span.Inlines?.Add(inline); 398 | 399 | border.Child = span; 400 | container.Add(border); 401 | } 402 | } 403 | else if (inl is Control ctrl) 404 | { 405 | container.Add(ctrl); 406 | } 407 | } 408 | } 409 | 410 | var panel = new SelectableTextBlock() 411 | { 412 | TextWrapping = TextWrapping.Wrap, Inlines = new InlineCollection() 413 | }; 414 | foreach (var item in container) 415 | { 416 | switch (item) 417 | { 418 | case SelectableTextBlock span: 419 | panel.Inlines.Add(span); 420 | break; 421 | case Control control: 422 | panel.Inlines.Add(control); 423 | break; 424 | } 425 | } 426 | 427 | return panel; 428 | } 429 | 430 | private Control CreateCodeBlock(FencedCodeBlock fencedCodeBlock) 431 | { 432 | var border = new Border 433 | { 434 | BorderBrush = Brushes.Gray, 435 | BorderThickness = new Thickness(1), 436 | CornerRadius = new CornerRadius(6), 437 | Padding = new Thickness(5), 438 | Margin = new Thickness(0, 5, 0, 5) 439 | }; 440 | 441 | var stackPanel = new StackPanel { Orientation = Orientation.Vertical }; 442 | 443 | // 头部面板:语言标签 + Copy 按钮 444 | var headerPanel = new StackPanel 445 | { 446 | Orientation = Orientation.Horizontal, 447 | HorizontalAlignment = HorizontalAlignment.Right, 448 | Margin = new Thickness(0, 0, 0, 5) 449 | }; 450 | 451 | // 如果 CodeToolRenderEvent == null,走默认处理,否则交给外部 452 | if (fencedCodeBlock.Lines.Count > 0 && CodeToolRenderEvent == null) 453 | { 454 | var languageText = new SelectableTextBlock 455 | { 456 | Text = fencedCodeBlock.Info, 457 | TextWrapping = TextWrapping.Wrap, 458 | Margin = new Thickness(0, 2, 10, 0) 459 | }; 460 | 461 | var copyButton = new Button 462 | { 463 | Content = "Copy", 464 | Classes = { "MdCopyButton" }, 465 | FontSize = 12, 466 | Height = 24, 467 | Padding = new Thickness(3), 468 | Margin = new Thickness(0) 469 | }; 470 | 471 | copyButton.Click += (sender, e) => 472 | { 473 | CopyClick?.Invoke(this, e); 474 | var clipboard = TopLevel.GetTopLevel(this).Clipboard; 475 | clipboard.SetTextAsync(fencedCodeBlock.Lines.ToString()); 476 | 477 | _notificationManager?.Show(new Notification( 478 | I18nManager.Instance.GetResource(Localization.MarkdownRender.CopyNotificationTitle), 479 | I18nManager.Instance.GetResource(Localization.MarkdownRender.CopyNotificationMessage), 480 | NotificationType.Success)); 481 | }; 482 | 483 | headerPanel.Children.Add(languageText); 484 | headerPanel.Children.Add(copyButton); 485 | stackPanel.Children.Add(headerPanel); 486 | 487 | // 添加代码高亮控件 (假设你有 CodeRender.CodeRender) 488 | if (Application.Current.RequestedThemeVariant == ThemeVariant.Light) 489 | { 490 | stackPanel.Children.Add(CodeRender.CodeRender.Render( 491 | fencedCodeBlock.Lines.ToString(), 492 | fencedCodeBlock.Info ?? "text", 493 | ThemeName.LightPlus)); 494 | } 495 | else 496 | { 497 | stackPanel.Children.Add(CodeRender.CodeRender.Render( 498 | fencedCodeBlock.Lines.ToString(), 499 | fencedCodeBlock.Info ?? "text", 500 | ThemeName.DarkPlus)); 501 | } 502 | } 503 | else 504 | { 505 | // 如果有外部订阅 CodeToolRenderEvent,则交给外部自定义 506 | stackPanel.Children.Add(headerPanel); 507 | CodeToolRenderEvent?.Invoke(headerPanel, stackPanel, fencedCodeBlock); 508 | } 509 | 510 | border.Child = stackPanel; 511 | return border; 512 | } 513 | 514 | 515 | private Control CreateList(ListBlock listBlock) 516 | { 517 | var panel = new StackPanel 518 | { 519 | Orientation = Orientation.Vertical, HorizontalAlignment = HorizontalAlignment.Left, Spacing = 4 520 | }; 521 | int orderIndex = 1; // 有序列表的起始索引 522 | 523 | foreach (var item in listBlock) 524 | { 525 | if (item is ListItemBlock listItemBlock) 526 | { 527 | var itemPanel = new Grid() 528 | { 529 | VerticalAlignment = VerticalAlignment.Top, HorizontalAlignment = HorizontalAlignment.Left, 530 | }; 531 | 532 | // Define columns with fixed width or auto-width 533 | itemPanel.ColumnDefinitions.Add(new ColumnDefinition 534 | { 535 | Width = new GridLength(1, GridUnitType.Auto) 536 | }); 537 | itemPanel.ColumnDefinitions.Add(new ColumnDefinition 538 | { 539 | Width = new GridLength(1, GridUnitType.Star) 540 | }); 541 | // Add more columns as needed... 542 | 543 | var prefix = listBlock.IsOrdered ? $"{orderIndex++}." : "• "; 544 | 545 | var prefixBlock = new SelectableTextBlock 546 | { 547 | Text = prefix, TextWrapping = TextWrapping.Wrap, FontWeight = FontWeight.Bold 548 | }; 549 | 550 | // Place the prefixBlock in the first column (row 0) 551 | Grid.SetColumn(prefixBlock, 0); 552 | itemPanel.Children.Add(prefixBlock); 553 | 554 | int columnIndex = 1; // Start placing other blocks in the next column 555 | foreach (var subControl in listItemBlock.Select(ConvertBlock)) 556 | { 557 | if (subControl is SelectableTextBlock textBlock) 558 | { 559 | // Place each textBlock in the next column 560 | Grid.SetColumn(textBlock, columnIndex); 561 | itemPanel.Children.Add(textBlock); 562 | columnIndex++; 563 | } 564 | else if (subControl != null) 565 | { 566 | // Handle other subControls 567 | panel.Children.Add(subControl); 568 | } 569 | } 570 | 571 | // Add the itemPanel to the main panel 572 | panel.Children.Add(itemPanel); 573 | } 574 | } 575 | 576 | return panel; 577 | } 578 | 579 | 580 | private Control CreateQuote(QuoteBlock quoteBlock) 581 | { 582 | var border = new Border(); 583 | border.AddMdClass(MarkdownClassConst.MdQuoteBorder); 584 | 585 | var stackPanel = new StackPanel { Orientation = Orientation.Vertical }; 586 | 587 | var headerPanel = new StackPanel 588 | { 589 | Orientation = Orientation.Horizontal, 590 | HorizontalAlignment = HorizontalAlignment.Right, 591 | Margin = new Thickness(0, 0, 0, 5), 592 | }; 593 | 594 | stackPanel.Children.Add(headerPanel); 595 | border.Child = stackPanel; 596 | 597 | foreach (Block block in quoteBlock) 598 | { 599 | var control = ConvertBlock(block); 600 | if (control != null) 601 | { 602 | stackPanel.Children.Add(control); 603 | } 604 | } 605 | 606 | return border; 607 | } 608 | 609 | #endregion 610 | 611 | #region Inline Handling 612 | 613 | private List ConvertInlineContainer(ContainerInline containerInline) 614 | { 615 | var results = new List(); 616 | var child = containerInline?.FirstChild; 617 | 618 | while (child != null) 619 | { 620 | var controls = ConvertInline(child); 621 | results.AddRange(controls); 622 | child = child.NextSibling; 623 | } 624 | 625 | return results; 626 | } 627 | 628 | private List ConvertInline(Markdig.Syntax.Inlines.Inline mdInline) 629 | { 630 | switch (mdInline) 631 | { 632 | case EmphasisInline emphasisInline: 633 | return CreateEmphasisInline(emphasisInline); 634 | 635 | case LinkDelimiterInline linkDelimiterInline: 636 | return CreateLinkDelimiterInline(linkDelimiterInline); 637 | 638 | case CodeInline codeInline: 639 | return [CreateCodeInline(codeInline)]; 640 | 641 | case LinkInline { IsImage: true } linkImg: 642 | { 643 | var img = CreateImageInline(linkImg); 644 | return img != null ? [img] : []; 645 | } 646 | case LinkInline linkInline: 647 | return [CreateHyperlinkInline(linkInline)]; 648 | 649 | case LineBreakInline _: 650 | return [new LineBreak()]; 651 | 652 | case LiteralInline literalInline: 653 | return [new Run(literalInline.Content.ToString())]; 654 | 655 | case HtmlInline htmlInline: 656 | return [CreateHtmlInline(htmlInline)]; 657 | 658 | default: 659 | // 其它情况:简单转成文字 660 | return [new Run(mdInline.ToString())]; 661 | } 662 | } 663 | 664 | private List CreateLinkDelimiterInline(LinkDelimiterInline linkDelimiterInline) 665 | { 666 | // 用一个 Avalonia 的 Run 显示文本 667 | if (linkDelimiterInline.FirstChild is LiteralInline literalInline) 668 | { 669 | return [new Run(literalInline.Content.Text)]; 670 | } 671 | 672 | return [new Run(linkDelimiterInline.FirstChild?.ToPositionText())]; 673 | } 674 | 675 | 676 | private List CreateEmphasisInline(EmphasisInline emphasis) 677 | { 678 | var results = new List(); 679 | var controls = ConvertInlineContainer(emphasis); 680 | 681 | foreach (var c in controls) 682 | { 683 | if (c is Inline inline) 684 | { 685 | if (results.LastOrDefault() is Span lastSpan) 686 | { 687 | lastSpan.Inlines.Add(inline); 688 | } 689 | else 690 | { 691 | var spanNew = new Span { Inlines = { inline } }; 692 | if (emphasis.DelimiterCount == 2) 693 | spanNew.FontWeight = FontWeight.Bold; 694 | else if (emphasis.DelimiterCount == 1) 695 | spanNew.FontStyle = FontStyle.Italic; 696 | 697 | results.Add(spanNew); 698 | } 699 | } 700 | else if (c is Control ctrl) 701 | { 702 | results.Add(ctrl); 703 | } 704 | } 705 | 706 | return results; 707 | } 708 | 709 | private object CreateCodeInline(CodeInline codeInline) 710 | { 711 | return new Border() 712 | { 713 | Classes = { "MdCodeBorder" }, 714 | Child = new SelectableTextBlock() 715 | { 716 | Text = codeInline.Content, FontFamily = new FontFamily("Consolas"), Classes = { "MdCode" } 717 | } 718 | }; 719 | } 720 | 721 | private Control? CreateImageInline(LinkInline linkInline) 722 | { 723 | if (!string.IsNullOrEmpty(linkInline.Url)) 724 | { 725 | return new ImagesRender { Value = linkInline.Url }; 726 | } 727 | 728 | return null; 729 | } 730 | 731 | private Inline CreateHtmlInline(HtmlInline htmlInline) 732 | { 733 | // 简单处理:实际可进一步解析 htmlInline.Tag 734 | return new Run(); 735 | } 736 | 737 | private Inline CreateHyperlinkInline(LinkInline linkInline) 738 | { 739 | foreach (var inline in linkInline) 740 | { 741 | if (inline is LiteralInline literalInline) 742 | { 743 | var span = new Span(); 744 | var label = new SelectableTextBlock 745 | { 746 | Classes = { "MdLink" }, 747 | Text = literalInline.Content.ToString(), 748 | TextWrapping = TextWrapping.Wrap, 749 | Cursor = new Cursor(StandardCursorType.Hand) 750 | }; 751 | 752 | label.Tapped += (sender, e) => 753 | { 754 | if (!string.IsNullOrEmpty(linkInline.Url)) 755 | UrlHelper.OpenUrl(linkInline.Url); 756 | }; 757 | 758 | span.Inlines.Add(label); 759 | return span; 760 | } 761 | } 762 | 763 | // 如果没有 literalInline,就简单换行 764 | return new LineBreak(); 765 | } 766 | 767 | #endregion 768 | 769 | #region INotifyPropertyChanged Support 770 | 771 | protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) 772 | { 773 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 774 | } 775 | 776 | #endregion 777 | } 778 | } -------------------------------------------------------------------------------- /src/MarkdownAIRender/Helper/UrlHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace MarkdownAIRender.Helper; 4 | 5 | public class UrlHelper 6 | { 7 | /// 8 | /// 使用默认浏览器打开指定链接 9 | /// 10 | /// 11 | public static void OpenUrl(string url) 12 | { 13 | try 14 | { 15 | Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); 16 | } 17 | catch (Exception e) 18 | { 19 | Console.WriteLine(e); 20 | } 21 | } 22 | 23 | /// 24 | /// 打开指定Url 25 | /// 26 | /// 27 | public static void OpenUrlWithBrowser(Uri uri) 28 | { 29 | OpenUrl(uri.AbsoluteUri); 30 | } 31 | } -------------------------------------------------------------------------------- /src/MarkdownAIRender/Index.axaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/MarkdownAIRender/MarkdownAIRender.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | enable 5 | enable 6 | MarkdownAIRender 7 | true 8 | snupkg 9 | true 10 | Avalonia Markdown组件 11 | Avalonia Markdown组件支持基础功能 12 | https://github.com/AIDotNet/Markdown.AIRender 13 | https://github.com/AIDotNet/Markdown.AIRender/blob/master/LICENSE 14 | https://github.com/AIDotNet/Markdown.AIRender 15 | git 16 | Avalonia 17 | 实现基础功能 18 | true 19 | MIT 20 | 21 | 22 | 23 | 24 | True 25 | True 26 | Language.tt 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | TextTemplatingFileGenerator 43 | Language.cs 44 | 45 | 46 | PreserveNewest 47 | 48 | 49 | PreserveNewest 50 | 51 | 52 | PreserveNewest 53 | 54 | 55 | PreserveNewest 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | True 66 | True 67 | Language.tt 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/MarkdownAIRender/Themes/Controls/MarkdownRender.axaml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/MarkdownAIRender/Themes/Controls/_index.axaml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/MarkdownAIRender/Themes/MarkdownThemes/Dark.axaml: -------------------------------------------------------------------------------- 1 | 6 | 12 | 13 | 19 | 20 | 26 | 27 | 33 | 34 | 40 | 41 | 47 | 48 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/MarkdownAIRender/Themes/MarkdownThemes/Light.axaml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 14 | 20 | 21 | 27 | 28 | 34 | 35 | 41 | 42 | 48 | 49 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/MarkdownAIRender/Themes/Shared/_index.axaml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/MarkdownAIRender/Themes/Styles/ColorfulPurple.axaml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 17 | 18 | 19 | 26 | 27 | 28 | 34 | 35 | 36 | 41 | 50 | 51 | 52 | 60 | 61 | 62 | 70 | 71 | 72 | 80 | 81 | 82 | 90 | 91 | 92 | 100 | -------------------------------------------------------------------------------- /src/MarkdownAIRender/Themes/Styles/Default.axaml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | #f9f9f9 14 | #e6e8ea 15 | #C6CaCd 16 | #A7ABB0 17 | #888D92 18 | #6B7075 19 | #555B60 20 | #41464C 21 | #2E3238 22 | #1C1F23 23 | 24 | #053170 25 | #0A4694 26 | #135CB8 27 | #1D75DB 28 | #2990FF 29 | #54A9FF 30 | #7FC1FF 31 | #A9D7FF 32 | #D4ECFF 33 | #EFF8FF 34 | 35 | 36 | #313131 37 | 38 | 39 | 40 | #1c1f23 41 | #2E3238 42 | #41464C 43 | #555B60 44 | #6B7075 45 | #888D92 46 | #A7ABB0 47 | #C6CaCd 48 | #e6e8ea 49 | #f9f9f9 50 | 51 | #EFF8FF 52 | #D4ECFF 53 | #A9D7FF 54 | #7FC1FF 55 | #54A9FF 56 | #2990FF 57 | #1D75DB 58 | #135CB8 59 | #0A4694 60 | #053170 61 | 62 | 63 | #F0F0F0 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 77 | 78 | 79 | 87 | 88 | 89 | 94 | 95 | 96 | 99 | 104 | 105 | 106 | 111 | 112 | 113 | 118 | 119 | 120 | 125 | 126 | 127 | 132 | 133 | 134 | 140 | 141 | 147 | 148 | 158 | 159 | 166 | 167 | 174 | 175 | 182 | 183 | 188 | 189 | 192 | 193 | 196 | 197 | 200 | 201 | 204 | 205 | 208 | 209 | 212 | 213 | 217 | -------------------------------------------------------------------------------- /src/MarkdownAIRender/Themes/Styles/Inkiness.axaml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 16 | 17 | 18 | 25 | 26 | 27 | 32 | 33 | 34 | 37 | 47 | 48 | 49 | 56 | 57 | 58 | 65 | 66 | 67 | 74 | 75 | 76 | 83 | 84 | 85 | 92 | 93 | -------------------------------------------------------------------------------- /src/MarkdownAIRender/Themes/Styles/OrangeHeart.axaml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 16 | 17 | 18 | 24 | 25 | 26 | 31 | 32 | 33 | 38 | 39 | 49 | 50 | 51 | 58 | 59 | 60 | 67 | 68 | 69 | 76 | 77 | 78 | 85 | 86 | 87 | 93 | -------------------------------------------------------------------------------- /src/MarkdownAIRender/Themes/Styles/TechnologyBlue.axaml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 17 | 18 | 19 | 20 | 27 | 28 | 29 | 35 | 36 | 37 | 42 | 51 | 52 | 53 | 61 | 62 | 63 | 71 | 72 | 73 | 81 | 82 | 83 | 91 | 92 | 93 | 101 | -------------------------------------------------------------------------------- /src/MarkdownAIRender/Themes/Styles/_index.axaml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/MarkdownAIRender/i18n/Language.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Changes to this file may cause incorrect behavior and will be lost if 5 | // the code is regenerated. 6 | // 7 | //------------------------------------------------------------------------------ 8 | namespace Localization 9 | { 10 | public static class MarkdownRender 11 | { 12 | public static readonly string CopyButtonContent = "Localization.MarkdownRender.CopyButtonContent"; 13 | public static readonly string CopyNotificationTitle = "Localization.MarkdownRender.CopyNotificationTitle"; 14 | public static readonly string CopyNotificationMessage = "Localization.MarkdownRender.CopyNotificationMessage"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/MarkdownAIRender/i18n/Language.tt: -------------------------------------------------------------------------------- 1 | <#@ template debug="false" hostspecific="true" language="C#" #> 2 | <#@ assembly name="System.Core" #> 3 | <#@ assembly name="System.Xml" #> 4 | <#@ assembly name="System.Xml.Linq" #> 5 | <#@ import namespace="System.Linq" #> 6 | <#@ import namespace="System.Text" #> 7 | <#@ import namespace="System.Collections.Generic" #> 8 | <#@ import namespace="System.Xml.Linq" #> 9 | <#@ import namespace="System.IO" #> 10 | <#@ output extension=".cs" #> 11 | //------------------------------------------------------------------------------ 12 | // 13 | // This code was generated by a tool. 14 | // Changes to this file may cause incorrect behavior and will be lost if 15 | // the code is regenerated. 16 | // 17 | //------------------------------------------------------------------------------ 18 | <# 19 | string templateDirectory = Path.GetDirectoryName(Host.TemplateFile); 20 | string xmlFilePath = Directory.GetFiles(templateDirectory, "*.xml").FirstOrDefault(); 21 | if (xmlFilePath!= null) 22 | { 23 | XDocument xdoc = XDocument.Load(xmlFilePath); 24 | var classNodes = xdoc.Nodes().OfType().DescendantsAndSelf().Where(e => e.Descendants().Count() == 0).Select(e => e.Parent).Distinct().ToList(); 25 | foreach (var classNode in classNodes) 26 | { 27 | var namespaceSegments = classNode.Ancestors().Reverse().Select(node => node.Name.LocalName); 28 | string namespaceName = string.Join(".", namespaceSegments); 29 | GenerateClasses(classNode, namespaceName); 30 | } 31 | } 32 | else 33 | { 34 | Write("XML file not found, please ensure that there is an XML file in the current directory"); 35 | } 36 | 37 | void GenerateClasses(XElement element, string namespaceName) 38 | { 39 | string className = element.Name.LocalName; 40 | StringBuilder classBuilder = new StringBuilder(); 41 | classBuilder.AppendLine($"namespace {namespaceName}"); 42 | classBuilder.AppendLine("{"); 43 | classBuilder.AppendLine($" public static class {className}"); 44 | classBuilder.AppendLine(" {"); 45 | var fieldNodes = element.Elements(); 46 | foreach (var fieldNode in fieldNodes) 47 | { 48 | var propertyName = fieldNode.Name.LocalName; 49 | var languageKey = $"{namespaceName}.{className}.{propertyName}"; 50 | classBuilder.AppendLine($" public static readonly string {propertyName} = \"{languageKey}\";"); 51 | } 52 | classBuilder.AppendLine(" }"); 53 | classBuilder.AppendLine("}"); 54 | Write(classBuilder.ToString()); 55 | } 56 | #> 57 | -------------------------------------------------------------------------------- /src/MarkdownAIRender/i18n/MarkdownAIRender.en-US.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Copy 5 | Copy succeeded 6 | Copy succeeded 7 | 8 | -------------------------------------------------------------------------------- /src/MarkdownAIRender/i18n/MarkdownAIRender.ja-JP.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | コピー 5 | コピーに成功しました 6 | コピーに成功しました 7 | 8 | -------------------------------------------------------------------------------- /src/MarkdownAIRender/i18n/MarkdownAIRender.zh-CN.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 复制 6 | 复制成功 7 | 复制成功 8 | 9 | -------------------------------------------------------------------------------- /src/MarkdownAIRender/i18n/MarkdownAIRender.zh-Hant.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 複製 5 | 複製成功 6 | 複製成功 7 | 8 | -------------------------------------------------------------------------------- /src/MarkdownAIRender/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AIDotNet/Markdown.AIRender/723ec5792afa588b1777cce9596e0163f66866da/src/MarkdownAIRender/logo.png --------------------------------------------------------------------------------