├── .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 | 
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 | 
90 |
91 | 支持 jpg、png、gif、svg 等图片格式,**其中 svg 文件仅可在微信公众平台中使用**,svg 文件示例如下:
92 |
93 | 
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