Compare commits

...

543 Commits

Author SHA1 Message Date
Vladimir Enchev
a2a5916e09 Version updated 2025-09-23 10:05:52 +03:00
Atanas Korchev
3f52e42661 Add Reload method to RadzenSankeyDiagram. 2025-09-22 11:37:34 +03:00
IncaTechnologies
224032d0c2 RadzenDataGrid throws an exception when a filter value is loaded for a sub property (#2290)
* Add tests

* Fix load settings from json

---------

Co-authored-by: noname <noname@nomail>
2025-09-22 08:37:52 +03:00
joriverm
4dece2e58a DataBoundFormComponent: Generate FieldIdentifier if ValueChanged is given (#2291)
Co-authored-by: AI\jvermeyl <joris.vermeylen@uzgent.be>
2025-09-22 08:37:40 +03:00
Atanas Korchev
7eb85d4454 Scheduler does not use the Culture when determining the start of the week. Fixes #2293. 2025-09-20 14:19:18 +03:00
Atanas Korchev
bd750e6f97 RadzenScheduler no longer uses a private Rect class. 2025-09-20 13:31:58 +03:00
Vladimir Enchev
ee0d4d29e1 Source links fixed
Fix #2289
2025-09-16 14:09:14 +03:00
Vladimir Enchev
10d2ea7c0a Version updated 2025-09-16 13:29:53 +03:00
Vladimir Enchev
0ab76d53ec DataGrid GroupFooterTemplate wrong rendering when ShowGroupExpandColumn=false 2025-09-15 15:31:07 +03:00
Vladimir Enchev
3dea0a5f67 Chat model updated 2025-09-15 11:28:13 +03:00
Vladimir Enchev
41c2e04cfa Chat messages cleared in some cases 2025-09-15 10:29:10 +03:00
DinuRoth
d8183feb07 only use the designated fragment when pasting html into a RadzenHtmlEditor (#2285)
* only use the designated fragment when pasting html into a RadzenHtmlEditor

* fragment-extraction inlined

---------

Co-authored-by: Martin Roth <martin.roth@swisscom.com>
2025-09-15 10:10:45 +03:00
Vladimir Enchev
e49fe2bca8 Version updated 2025-09-11 10:34:12 +03:00
Vladimir Enchev
67664a2816 DataGrid CheckBoxList with OData demo updated
Fix https://github.com/radzenhq/radzen-blazor/issues/2252
2025-09-09 09:37:16 +03:00
Gincsai Gábor
eefbe00aec Fix grid settings handling for sub properties column (#2274)
* Fix loading settings when multiple colum has same Property (e.g using sub-property filtering)
Fix loaded settings type handling to handle IEnumerable column with non ValueKind.Array.
Added default value for Unique ID is not set.

Add example.

* Set shouldUpdateState too.

* Make QueryableExtension.Select public

* Change filter mode back to original on demo page.
2025-09-09 09:15:34 +03:00
Atanas Korchev
be67d5f120 JavaScript error is thrown when the user selects an image in RadzenHtmlEditor and presses the insert link tool. 2025-09-04 13:57:50 +03:00
Vladimir Enchev
fa1fe694cf version updated 2025-09-02 13:53:40 +03:00
yordanov
9f681c0c09 Add CSS variables for DataGrid alternating row backgrounds in frozen columns 2025-09-02 12:43:31 +03:00
Vladimir Enchev
18dde2849e DataGrid will look first for column UniqueID before Property when loading settings 2025-08-29 18:25:20 +03:00
Vladimir Enchev
5678774de0 PickList MoveFilteredItemsOnlyOnMoveAll added 2025-08-29 18:19:51 +03:00
Vladimir Enchev
f5370e1384 RadzenColorPicker SelectedColorChanged null exception fixed 2025-08-29 08:46:45 +03:00
Vladimir Enchev
934d7cb104 Version updated 2025-08-28 09:58:56 +03:00
Vladimir Enchev
33b201147d DynamicExtensions Where() should use ExpressionSerializer Value for Format for FilterParameters 2025-08-28 09:58:27 +03:00
yordanov
abe2006e15 RadzenDialog source link should point to DialogService as well. Resolves #2270 2025-08-27 19:25:00 +03:00
Vladimir Enchev
81b5cb386d DataGridLoadColumnFilterDataEventArgs Property setter made public 2025-08-27 08:53:44 +03:00
Vladimir Enchev
c713dd6c87 Version updated 2025-08-26 09:18:01 +03:00
Vladimir Enchev
eda5c490b7 DropDownDataGrid AllowFilteringByAllStringColumns will raise exception with IDictionary binding 2025-08-26 09:17:22 +03:00
yordanov
b6bc9e1257 Fix link to Skeleton animations example source 2025-08-25 17:50:31 +03:00
yordanov
123ffd8886 Update premium themes 2025-08-25 12:51:51 +03:00
Vladimir Enchev
9a2c09c6e8 version updated 2025-08-25 11:38:24 +03:00
Vladimir Enchev
5b5fdde9a2 Skeleton component added (#2268)
* RadzenSkeleton component added

* Update Skeleton component properties, themes, and demos

* Skeleton tests fixed

---------

Co-authored-by: yordanov <vasil@yordanov.info>
2025-08-25 11:31:34 +03:00
Vladimir Enchev
0cc50d70da Fixed RadzenPanelMenuItem will not show tooltip 2025-08-25 11:17:13 +03:00
mtboeke
c79ecddee4 Expose Responsive MaxWidth in RadzenSidebar (#2260)
* Expose ResponsiveMaxWidth

* Change property to a string to be more versatile

---------

Co-authored-by: Matt Boeke <gangolfus@hotmail.com>
2025-08-25 11:07:11 +03:00
Nopke
e929edbd53 ColorPicker: highlight selected color item (#2267) 2025-08-25 11:00:39 +03:00
Atanas Korchev
8de7a1ec70 Wrong value axis display when all values in a series are the same and negative. 2025-08-22 19:17:20 +03:00
Vladimir Enchev
0b6ae4d225 Version updated 2025-08-21 07:48:09 +03:00
yordanov
c69bb8bb2b Add MCP logo 2025-08-20 22:26:08 +03:00
yordanov
e8dda6f946 Update list of components 2025-08-20 18:21:40 +03:00
yordanov
3daebfc0c1 Update AI page layout 2025-08-20 18:09:00 +03:00
Atanas Korchev
d5fcbb97c3 Add AI page. 2025-08-20 16:47:11 +03:00
yordanov
97555e7849 FrozenColumn should have alternating rows. Resolves #2265 2025-08-20 13:43:14 +03:00
Vladimir Enchev
7eb799d2a6 DataGridLoadColumnFilterDataEventArgs Property added 2025-08-20 11:17:08 +03:00
Vladimir Enchev
d06a85a3b6 Fixed DataGrid CheckBoxFilter form id when both Property and FilterProperty are defined 2025-08-20 11:05:32 +03:00
Vladimir Enchev
f616eadccf DataGrid column AlwaysShowAllCheckBoxListData property added 2025-08-20 10:57:57 +03:00
Vladimir Enchev
b69d6c193b Fixed DataGrid endless loop in load settings with columns width 2025-08-19 16:02:57 +03:00
Josh
d12908778e RadzenDropDown/Grid: Modify bound collection in-place (#2261)
* Refactor out code that assigns and notifies for Multiple when items are Selected

* Add alternative handling to support keeping the bound collection instance unchanged. This also enables support for custom collections without default ctors, and HashSets

* Ensure ClearAll maintains collection instance, instead of setting to null

* Ensure SelectdAll uses the same new behavior

* resolve the actual type of the bound collection, in determining if to apply the ReferenceGenericCollectionAssignment

# Conflicts:
#	Radzen.Blazor/DropDownBase.cs

* fixup: Ensure non-multiple/non-collection sources are accomodated

# Conflicts:
#	Radzen.Blazor/DropDownBase.cs

* use the more tactical approach for altering the collection to match selectedItems.  This preserves ordering and will raise expected INotifyCollectionChanged events if implemented

* cleanup: naming convention
2025-08-19 07:44:31 +03:00
Vladimir Enchev
95c42f3266 Version updated 2025-08-18 10:58:09 +03:00
Vladimir Enchev
597e6de682 AIChatService HttpClient resolved runtime 2025-08-18 10:38:57 +03:00
Vladimir Enchev
406141a4f1 Injected HttpClient removed from RadzenAIChat since it's not needed 2025-08-18 10:32:28 +03:00
Ali Yousefi
0db7fe63b0 feat: make ClearFilters virtual (#2263) 2025-08-18 10:09:03 +03:00
Vladimir Enchev
e5a43ef5b6 DatePicker will not display value initially when set to midnight 2025-08-15 10:03:00 +03:00
Petar Slavov
4d9a9c9ac2 Add GetProperty that supports retrieval of interface properties. (#2255)
* Introduces a new static method `GetProperty` in the `Radzen.PropertyAccess` class to retrieve properties by name, including support for interfaces. Updates `DropDownBase.cs` to utilize this new method, enabling property retrieval for collections of interfaces in multiselect DropDowns.

* Add unit tests for property resolution in interfaces

Implemented unit tests in `PropertyAccessTests.cs` to verify
the resolution of `Description`, `Name`, and `Id` properties
from the interfaces `ISimpleInterface`, `ISimpleNestedInterface`,
and `ISimpleBaseInterface`. Defined the interfaces to support
the tests for the `GetProperty` method functionality.
2025-08-15 09:28:52 +03:00
Vladimir Enchev
3cc1f6b994 Version updated 2025-08-14 08:57:50 +03:00
Vladimir Enchev
4322361269 AIChatService added to AddRadzenComponents 2025-08-14 08:57:12 +03:00
Vladimir Enchev
44d3651aa8 chat tests fixed 2025-08-14 07:55:09 +03:00
Atanas Korchev
03423f32c8 Remove the version. 2025-08-13 20:38:23 +03:00
Atanas Korchev
bf8c950a8a Build the API reference without mono. 2025-08-13 19:12:24 +03:00
Vladimir Enchev
f16ae734cc More DocFX removed 2025-08-13 18:18:01 +03:00
Vladimir Enchev
91196dca39 DOCFX removed 2025-08-13 18:17:17 +03:00
Vladimir Enchev
ea36ff5011 update 2025-08-13 18:13:50 +03:00
Vladimir Enchev
dad418140b mono:latest updated 2025-08-13 18:11:43 +03:00
Vladimir Enchev
d5f8877768 docker updated 2025-08-13 17:54:50 +03:00
Vladimir Enchev
95f2dfa8ac docker updated 2025-08-13 17:53:10 +03:00
Vladimir Enchev
d60b14c686 various warnings fixed 2025-08-13 16:23:26 +03:00
Vladimir Enchev
fabcd7b8f4 Version updated 2025-08-13 16:16:36 +03:00
yordanov
045880c234 Update premium themes 2025-08-13 16:09:38 +03:00
Vladimir Enchev
d2071102c5 RadzenChat component added (#2254) 2025-08-13 15:37:01 +03:00
Ehab Hussein
864bc8c7f9 Add Sankey Diagram Component to Radzen Blazor (#2220)
* Add SankeyDiagram Component

---------

Co-authored-by: Atanas Korchev <akorchev@gmail.com>
Co-authored-by: yordanov <vasil@yordanov.info>
2025-08-13 15:20:06 +03:00
Nenad Kovačević
b63b861b8a Fixed a typo in IconPage.razor (#2253) 2025-08-13 13:35:58 +03:00
Vladimir Enchev
23effbb3ec Upload MaxFileCount error handling fixed 2025-08-13 09:55:42 +03:00
Vladimir Enchev
c0a87c834c all tests fixed 2025-08-13 09:29:41 +03:00
Vladimir Enchev
4d02213260 test fixed 2025-08-13 09:19:41 +03:00
Vladimir Enchev
46118964ea Added DataTable support for DataGrid 2025-08-13 09:14:59 +03:00
Vladimir Enchev
ca52c588fd Revert "feature: DataGrid Columns allow Sort and Filter when bound to Array<o… (#2233)"
This reverts commit 4503361877.
2025-08-13 08:52:41 +03:00
Vladimir Enchev
b9905e10de Revert "XML comment fixed"
This reverts commit 62cf565a09.
2025-08-13 08:52:30 +03:00
Vladimir Enchev
7ea08af177 Revert "DataGrid DataTable improved"
This reverts commit c90e3bedb1.
2025-08-13 08:52:25 +03:00
Vladimir Enchev
fa59aff6d5 RadzenDropDown ReadOnly can be bypassed with keyboard select as you type 2025-08-13 07:59:49 +03:00
Vladimir Enchev
c90e3bedb1 DataGrid DataTable improved 2025-08-12 10:39:37 +03:00
Vladimir Enchev
62cf565a09 XML comment fixed 2025-08-12 10:26:49 +03:00
EdJohnstonG9
4503361877 feature: DataGrid Columns allow Sort and Filter when bound to Array<o… (#2233)
* feature: DataGrid Columns allow Sort and Filter when bound to Array<object>[] i.e. DataRow.ItemArray[n]

* Expose Test page and fix AmbiguousMatch in test (ItemArray not Item)

* Working filters before test

* In Sorting Tests

* Filter by ChecBoxList to be done

* Better solution to filter types

* Solution for Enum in DataTable

* Tidy up and fix of Enum Null
Working Enums

* Re-enabled all data sources in tests

* Prevent display of simple filter input when fitler expects enumerable
Show correct demo code, only when merged with Master

* Fix to handle Filter lists with nullable enums
2025-08-12 10:24:53 +03:00
Vladimir Enchev
f2bc7ef3ed Version updated 2025-08-11 09:16:59 +03:00
Vladimir Enchev
f9505e42c4 Fixed DataGrid CheckBoxList Filter error when filtering ICollection property 2025-08-10 09:04:28 +03:00
GAUSS-LVS Dev
d340df758e Fixed timing problem in RadzenTreeItem OnInitializedAsync (#2246)
Fixed an issue that the order of CurrentItems could be wrong when items are expanded simultaneously.

Co-authored-by: mde <m.deppe@gauss-lvs.de>
2025-08-09 10:30:53 +03:00
Vladimir Enchev
741ecf0955 Fixed DataGrid DoesNotContains filter throws No coercion operator is defined between types ‘System.String’ and ‘System.Boolean’ 2025-08-09 10:28:33 +03:00
yordanov
90ab2e468e Preloader should have properly set opacity when the grid is loading. Resolves #2205 2025-08-08 15:01:01 +03:00
yordanov
360b9d7730 Wrong link in image URL demo. Resolves #2245 2025-08-07 18:26:12 +03:00
Vladimir Enchev
bb698dbe9d version updated 2025-08-04 08:31:12 +03:00
Vladimir Enchev
5e120a7328 DataGrid filter clear not visible, for simple FilterMode, when FilterProperty is different from Property
Fix #2237
2025-08-04 08:23:39 +03:00
Vladimir Enchev
206daba741 DataGrid simple filter should be rendered only for relevant columns 2025-08-04 08:08:33 +03:00
Mohammad Mohammadi
79e86e8b76 Improve numeric input handling for all locales (including Persian and Arabic) (#2240)
* Improve numeric input support in JS: accept all Unicode digits using `\p{Nd}`

Replaced hardcoded digit ranges with Unicode-based digit detection using `\p{Nd}‍` to ensure support for Persian, Arabic, and other locale-specific numerals.

* fix: support all Unicode digit inputs in RadzenNumeric

Replaced digit handling logic with Unicode-aware checks using `char.IsDigit` to support numeric input from any language, including Persian and Arabic.

Previously, non-ASCII digits were blocked, which caused issues for users entering Persian or Arabic numerals. This fix makes the component input-agnostic to digit origin.
2025-08-01 08:08:51 +03:00
Atanas Korchev
398c17c75a Add support for immediate changes to RadzenTextBox (enabled via Immediate=true). Closes #909. 2025-07-25 10:03:22 +03:00
Vladimir Enchev
49adc95170 Version updated 2025-07-24 10:10:15 +03:00
Dmitry Solovev
2b71bd1178 Fix IconColor in ProfileMenuItem (#2229)
Co-authored-by: Дмитрий Соловьев <dsolo@codestetic.com>
2025-07-23 10:16:49 +02:00
Frank Folsche
c53938bc0a #1761 offset when filtermenu row is active (#2223) 2025-07-16 15:26:44 +03:00
joriverm
718827d757 feature: implement disabled on panel menu items (#2222)
Co-authored-by: AI\jvermeyl <joris.vermeylen@uzgent.be>
2025-07-16 15:22:20 +03:00
Vladimir Enchev
b4c87e7e10 Role "tab" removed from a element in RadzenTabsItem 2025-07-16 11:54:52 +03:00
Vladimir Enchev
20dd0e9b2d TabsItem role set to "tab" 2025-07-16 11:53:21 +03:00
Vladimir Enchev
397e2d207e Version updated 2025-07-10 15:36:45 +03:00
Vladimir Enchev
29f7b6cb74 DropDownBase should allow filter on SPACE while still be able to select/deselect items 2025-07-10 15:33:33 +03:00
joriverm
a004b55e8c feature: make fieldidentifier settable as parameter (#2216)
Co-authored-by: AI\jvermeyl <joris.vermeylen@uzgent.be>
2025-07-10 14:48:29 +03:00
Vladimir Enchev
221d8f0697 DataGrid should set column CustomFilterExpression to null on Reset() 2025-07-10 14:36:03 +03:00
Vladimir Enchev
e7a243ab7f DataGrid LoadDataArgs.Filter should use it for instance name in case of column CustomFilterExpression 2025-07-10 14:27:45 +03:00
Vladimir Enchev
ce1ffe2070 DataGrid FilterMode.CheckBoxList crashes when selecting all items in a non string column
Fix #2213
2025-07-10 12:05:22 +03:00
Gincsai Gábor
32e2419b67 Suppott filtering List<string> like sub-property. (#2210)
* Suppott filtering List<string> like sub-property. In this case no nested property so use the collectionItemTypeParameter.

* Extend the FilterValueDemo with Awards that uses List<string> property with multiple filter.
The demo works with in memory data, but not with on backend side with Oracle database.
2025-07-09 17:59:15 +03:00
Piotr Mazur
5f7079e526 Fix type assignment in RadzenDataGrid to get the correct type (#2209)
* Add missing Type field when creating FilterDescriptors in RadzenDataGrid InvokeLoadData method

* Fix type assignment in RadzenDataGrid to get the correct type

---------

Co-authored-by: Piotr Mazur <piotr.mazur@lstsoft.com.pl>
2025-07-08 13:34:39 +03:00
Vladimir Enchev
4ffeae11e2 code fixed 2025-07-08 13:32:39 +03:00
Vladimir Enchev
7621e31d38 RadzenAutoComplete will select value on NumpadEnter 2025-07-08 11:32:29 +03:00
Gincsai Gábor
c00c751127 System.Type cannot be serialized out of the box in C# (possible security issue) (#2208)
With ignoring the Type property FilterDescriptor can be used for custom server side filtering.
2025-07-08 08:11:23 +03:00
Vladimir Enchev
a09f550b52 DataGrid double IsEmpty/IsNotEmpty filters fixed 2025-07-07 16:33:52 +03:00
Vladimir Enchev
0aeb1cfa97 Version updated 2025-07-07 14:55:49 +03:00
Piotr Mazur
46da7e45b9 Add missing Type field when creating FilterDescriptors in RadzenDataGrid InvokeLoadData method (#2207)
Co-authored-by: Piotr Mazur <piotr.mazur@lstsoft.com.pl>
2025-07-07 14:54:59 +03:00
Vladimir Enchev
83bf448bdd Add in RadzenTree with async Expand calls error 2025-07-07 10:30:14 +03:00
Atanas Korchev
5ab15fb812 Add crossorigin attribute when preloading the icon font. 2025-07-03 15:32:38 +03:00
mvoxland
0dc70f3e42 DropDownDataGrid null check to prevent error that occurs when using AllowFilteringByAllStringColumns="true" (#2201)
To replicate this bug: just add AllowFilteringByAllStringColumns="true" to the Multiple Selection example for DropDownDataGrid
2025-07-01 13:47:14 +03:00
Vladimir Enchev
5bc86a75bc DataGrid column ShowCellDataAsTooltip property added 2025-07-01 13:46:26 +03:00
Vladimir Enchev
51d3df9282 Version updated 2025-06-30 11:08:57 +03:00
Vladimir Enchev
31925ea099 DropDownBase select item as you type fixed 2025-06-28 09:22:30 +03:00
yordanov
a56e6540e8 Update premium themes 2025-06-27 11:32:34 +03:00
Cosmatevs
03bccb4cb4 DropDown, DropDownDataGrid: fix font family in filter inputs 2025-06-27 11:30:38 +03:00
yordanov
bba9b7b6ec Update Material Icons font. Resolves #2187 2025-06-26 11:47:37 +03:00
yordanov
5b8ea6e4b0 Add font-display rules 2025-06-26 11:14:41 +03:00
yordanov
91676d802c Improve ripple styles for rz-state-disabled 2025-06-25 18:54:33 +03:00
yordanov
395d343f69 Grid cell focus state should be visually distinct from selected state in Standard and Material3 themes 2025-06-25 16:36:38 +03:00
Atanas Korchev
201993ff08 RadzenTheme preloads the icon font. 2025-06-24 13:43:30 +03:00
Vladimir Enchev
f9499904f3 DataGrid simple filter menu will not show string filter operators for string columns 2025-06-24 13:26:07 +03:00
Vladimir Enchev
0a152474a2 Version updated 2025-06-23 14:39:59 +03:00
Lukas Grützmacher
8dc522e054 Function key should not trigger DropDown Change event (#2195) 2025-06-23 07:40:29 +03:00
Vladimir Enchev
eb8209d575 Missing XML comments added 2025-06-20 11:05:29 +03:00
Simon Lissack
96b3ac7435 Support deserializing unsigned integers when loading filter column values (#2190) 2025-06-20 11:00:25 +03:00
Vladimir Enchev
42d7113896 DropDownDataGrid GridLines property added 2025-06-20 07:45:17 +03:00
Vladimir Enchev
7f19786053 ContextMenu should prevent the browser context menu 2025-06-20 07:37:31 +03:00
Lee
52b121a9f4 Updating HtmlEditor methods and properties to public (#2189)
* Updating methods and properties to public, to make it easier to customise and create custom HtmlEditor buttons in your own projects.

* Putting SourceChanged back to private
2025-06-19 16:37:11 +03:00
Atanas Korchev
d191a8223e Events with zero duration are sometimes invisible in month view. 2025-06-19 15:09:52 +03:00
Vincent Schmandt
b371b43a86 fix: avoid accessing parameter view in SetParametersAsync after an async call to prevent a race condition (#2186)
* fix: avoid accessing parameter view in SetParametersAsync after an async call to prevent a race condition

Fixes: https://forum.radzen.com/t/the-parameterview-instance-can-no-longer-be-read-because-it-has-expired/18912/7
See also: https://stackoverflow.com/questions/66717054/the-parameterview-instance-can-no-longer-be-read-because-it-has-expired-paramet

* fix SetParametersAsync reordering
2025-06-18 14:35:59 +03:00
Vladimir Enchev
c243f384a5 DataGrid column filter menu not unique in case of columns with same property names 2025-06-18 14:06:16 +03:00
Vladimir Enchev
e10f8d759c Version updated 2025-06-18 11:15:21 +03:00
Vladimir Enchev
977e5b8cb6 DataGrid LoadDataArgs Filters should respect the column FilterProperty 2025-06-17 14:59:57 +03:00
yordanov
b68f95a7e7 Update main navigation 2025-06-17 10:08:37 +03:00
yordanov
e30b01de23 Update premium themes 2025-06-17 09:39:27 +03:00
Vladimir Enchev
68f87db33e DataGrid CheckBoxList filter on wrong column in some cases 2025-06-16 14:26:21 +03:00
Romaniv Volodymyr
f2bfa462a0 Fix #2167 (#2185)
Set cursor to initial for disabled RadioButtonList elements
2025-06-16 10:07:11 +03:00
yordanov
adffcf5229 RadzenLayout components should not have 100vw on mobile screens. Resolves #1956 2025-06-13 10:43:40 +03:00
yordanov
a4f37cbcfa Fix active state styles in demos navigation menu 2025-06-12 17:21:44 +03:00
Vladimir Enchev
18d8328a9c Version updated 2025-06-12 11:04:17 +03:00
Vladimir Enchev
a466578ebf ExpressionSerializer should respect the DateTime.Kind
Fix #2183
2025-06-12 11:03:51 +03:00
yordanov
c92f8833e2 Fix toc confing in home layout 2025-06-12 10:44:17 +03:00
Vladimir Enchev
2f46c58028 Enabled search on non string properties for DataGrid CheckBoxList filter
Fix #2182
2025-06-12 10:35:47 +03:00
joriverm
a1c24e346c fixes: Let all DataBoundFormComponent's expose value as the actual type that it is (#2179)
Co-authored-by: AI\jvermeyl <joris.vermeylen@uzgent.be>
2025-06-11 15:39:37 +03:00
yordanov
fb7fb62a0e Update links in main nav 2025-06-11 15:28:39 +03:00
Vladimir Enchev
7378ee1a32 Login ShowLoginButton not respected in some cases 2025-06-11 15:25:15 +03:00
yordanov
0bee5b4365 Update demos main navigation and overview layout 2025-06-11 15:23:20 +03:00
yordanov
47bfd6f539 RadzenMenu active state should be visually distinguishable 2025-06-11 15:23:20 +03:00
Erik van der Boom
1b92cb7f86 Reset select on filter (#2180)
* Add reset index on filter option

* remove unwanted spaces

* correct order
2025-06-11 09:34:31 +03:00
Vladimir Enchev
2931d47d05 AllowFilteringByWordCount default value set to 10 2025-06-09 11:12:43 +03:00
Vladimir Enchev
7668a54648 DropDownDataGrid AllowFilteringByWordCount property added 2025-06-06 10:08:03 +03:00
Vladimir Enchev
aa0aaf7c10 Version updated 2025-06-05 10:04:29 +03:00
Vladimir Enchev
4b80da96ab Shift key should not trigger DropDown Change event
Fix #2165
2025-06-05 10:03:20 +03:00
Vladimir Enchev
396fa19d7b ListBox registers two keys on focus
Fix #2172
2025-06-04 07:57:02 +03:00
Vladimir Enchev
4cd235a08e DataGrid filter menu IsEmpty/IsNotEmpty options added for string columns 2025-06-04 07:46:53 +03:00
Vladimir Enchev
edd21956c5 DateTimeWithRoundtripKind test added 2025-05-30 09:58:59 +03:00
mvoxland-MoorheadSchools
f842156253 Datetime parsing fix, stop converting to local Kind (#2171) 2025-05-30 09:44:56 +03:00
Paul Ruston
ce0ce0f229 Supply Appointment data to SchedulerSlotRenderEventArgs - #2050 (#2154)
* Supply Appointment data to SchedulerSlotRenderEventArgs

* Get appointments on demand

* Revert YearView SlotRender code to remove breaking change. Update demo
2025-05-27 10:24:37 +03:00
Vladimir Enchev
ae2e9219c3 Version updated 2025-05-27 09:14:27 +03:00
Vladimir Enchev
146b374ca8 DataGrid CheckBoxList filter not populated properly in case of self-referencing hierarchy 2025-05-27 07:54:53 +03:00
Vladimir Enchev
9264676407 Non generic IDictionary support added for expressions
Close #2169
2025-05-27 07:17:05 +03:00
Victor Ureta
eb74460fcf add icon in Title Dialog (#2170)
* add icon in Title Dialog

* fix IconColor in DialogService.cs

* fix variables
2025-05-26 18:48:47 +03:00
Vladimir Enchev
16ed582052 DataGrid column with CustomFilterExpression will not filter innitially 2025-05-23 09:26:15 +03:00
Vladimir Enchev
a7ddb81ad1 ToFilterString() should serialize only date of DateTime with zero time 2025-05-23 09:02:38 +03:00
Atanas Korchev
d09f3870b1 RadzenCompareValidator validates when the Visible property is set to true. Closes #2148. 2025-05-22 11:58:20 +03:00
Atanas Korchev
9a8e249964 RadzenScheduler throws error on window resize when placed in a hidden element. 2025-05-22 11:47:56 +03:00
Vladimir Enchev
c63d9cf0b9 DataGrid CheckBoxList filter search will work properly when binding to dynamic data 2025-05-22 09:46:06 +03:00
Vladimir Enchev
f6648f80ec Numeric null. reference exception fixed 2025-05-22 09:23:19 +03:00
Vladimir Enchev
250bff7ef3 ToCSV() export method will handle comma 2025-05-22 09:23:18 +03:00
Nopke
fa0b925aa9 TimeSpanPicker: fix missing panel fields if only Min or only Max is set (#2166) 2025-05-21 08:39:43 +03:00
Atanas Korchev
28fe2949f2 Add CloseSideAsync method to DialogService which waits for the close animation to complete. Closes #2161. 2025-05-20 11:49:50 +03:00
Vladimir Enchev
d7f46216e9 ToFilterString() and ToODataFilterString() extension methods added to IEnumerable<CompositeFilterDescriptor> 2025-05-20 11:46:24 +03:00
Vladimir Enchev
3f0d82a427 Version updated 2025-05-20 08:56:30 +03:00
yordanov
d2811c9fba Fix typo. 2025-05-16 22:20:20 +03:00
yordanov
028e26ef59 Add Accessibility Conformance Report 2025-05-16 10:19:42 +03:00
yordanov
4086332857 RadzenAccordion items should be displayed vertically in RadzenStep 2025-05-15 15:32:43 +03:00
yordanov
1eebfccf14 Remove Dialog's scale animation if it has RadzenChart inside 2025-05-15 15:03:32 +03:00
Vladimir Enchev
f1127f917d DataGrid CheckBoxList filter list not populated in some cases 2025-05-15 13:59:47 +03:00
yordanov
6cc8700bb9 Update UI blocks description 2025-05-15 10:44:16 +03:00
yordanov
3cabcd5cf9 Update info on how to use UI Blocks 2025-05-15 10:25:56 +03:00
yordanov
c5aaeb67b5 Update badges on homepage 2025-05-15 10:25:56 +03:00
Vladimir Enchev
47fcd54d1c version updated 2025-05-15 07:28:28 +03:00
Vladimir Enchev
5dd5cd8ae1 DataGrid grouping exception with wrong type discovery
Fix #2153
2025-05-15 07:28:10 +03:00
Vladimir Enchev
8ab18160a5 Demo updated to handle query that cannot be translated to SQL 2025-05-14 10:52:26 +03:00
Vladimir Enchev
b70b697f3f DataGrid column FilterProperty should be respected if specified 2025-05-14 09:18:30 +03:00
Vladimir Enchev
48eb35e20d Version updated 2025-05-13 20:57:58 +03:00
Atanas Korchev
4fd899133f Cannot parse certain expressions that include string and number comparisons. 2025-05-13 20:52:44 +03:00
Vladimir Enchev
3b134cf8b5 code fixed 2025-05-13 09:28:55 +03:00
Atanas Korchev
86bc538601 Some zero-length events appear on top of each other in month view. 2025-05-13 08:42:11 +03:00
Atanas Korchev
fb00fbc850 Add anchor for the day, week and month view demo. 2025-05-13 08:42:11 +03:00
Vladimir Enchev
309f9fcfa1 DataGrid will populate Query.Filter and LoadDataArgs.Filter only if accessed 2025-05-12 13:46:29 +03:00
Vladimir Enchev
253aa63980 Version updated 2025-05-12 13:03:28 +03:00
Vladimir Enchev
93dea13f20 Added support for indexer expressions with sub properties 2025-05-12 12:36:02 +03:00
Atanas Korchev
3d3d1718b7 Add editable source code to dashboard demo. 2025-05-12 11:00:25 +03:00
Cosmatevs
90c2a5d51f fix recognizing standard dark theme as dark by components 2025-05-12 10:34:44 +03:00
Vladimir Enchev
8846a229d8 Added support for Dictionary<string, string> for expressions 2025-05-12 09:31:12 +03:00
Pedro Constantino
acf0591adc [FEAT] - Adding the possibility to return the source in the upload event args when managing file uploads via RadzenUpload. (#2151) 2025-05-12 07:31:42 +03:00
yordanov
95bee47826 PanelMenu item expand arrow should not be displayed when child items are absent. Resolves #2147 2025-05-09 19:44:18 +03:00
Atanas Korchev
b701976356 Expanded panel menu items do not always collapse when Multiple is set to false. 2025-05-09 14:53:54 +03:00
Vladimir Enchev
2b61941e34 Version updated 2025-05-08 17:24:08 +03:00
yordanov
1526ffa3af Update premium themes 2025-05-08 17:19:02 +03:00
Cosmatevs
bb4227e537 fix the bottom border color of a hovered/focused input field in Standard themes 2025-05-08 17:08:01 +03:00
Vladimir Enchev
778691405d version updated 2025-05-08 15:04:17 +03:00
Atanas Korchev
cb7d3b91c6 Add Template property to RadzenTocItem. 2025-05-08 15:01:53 +03:00
Atanas Korchev
fd8cf16336 NullReferenceException thrown by RadzenLayout when AddRadzenComponents is not used. 2025-05-08 14:54:33 +03:00
yordanov
2403cf222f Add TOC to demos homepage 2025-05-08 14:39:31 +03:00
yordanov
20152dd8a6 Fix duration progress position in Notification. Resolves #2104 2025-05-08 11:47:42 +03:00
Vladimir Enchev
b459207b7a Links inside popup should close the popup only on left mouse button click 2025-05-08 07:48:44 +03:00
Atanas Korchev
8b009509c8 Add accordion breaking change to changelog. 2025-05-07 20:13:29 +03:00
Atanas Korchev
c456891a11 Convert to the left operand type if the right type is object during null coalescing. 2025-05-07 15:25:34 +03:00
yordanov
6b044d8086 Update demos' badges 2025-05-07 14:53:07 +03:00
Atanas Korchev
3162de84ed Update the busy dialog demo. 2025-05-07 14:32:20 +03:00
Atanas Korchev
641768240a Update the changelog. 2025-05-07 14:07:30 +03:00
Vladimir Enchev
8b7ade4b9c Version updated 2025-05-07 13:14:37 +03:00
Vladimir Enchev
c7db9394c8 V7 (#2144)
* Remove Microsoft.CodeAnalysis. Add own C# expression parser.

* Popup dialog animations (#2118)

* Experiment with popup and dialog animations.

* Move animations to a separate _animations.scss

* Remove dialog closing animation.

* Support side dialog positions. Prefix keyframes.

* Use --rz-transition for animation function and duration

* Reset site.css.

* PanelMenu animations.

* Display none is toggled a bit late.

* RadzenPanel animations.

* More animations.

* Use transitions for panel menu.

* Remove old code.

* Accordion uses transitions.

* Panel uses transitions.

* Extract expand and collapse implementation in a separate component.

* Set initial expand state earlier to prevent two renders.

* Add open animation to notifications.

* Handle onanimationend before toggling the animation classes.

* Add menu animation.

* Experiment with tree animation.

* Add animations to fieldset.

---------

Co-authored-by: yordanov <vasil@yordanov.info>

* ExpressionSerializer and tests added (#2119)

* Fix failing tests.

* various components RequiresUnreferencedCode attribute added (#2120)

* RequiresUnreferencedCode added to ExpressionSerializer

* Update premium themes

* RequiresUnreferencedCode added to ExpressionParser

* FormComponentWithAutoComplete RequiresUnreferencedCode removed

* Revert "FormComponentWithAutoComplete RequiresUnreferencedCode removed"

This reverts commit ec900a4df8.

* Revert "RequiresUnreferencedCode added to ExpressionParser"

This reverts commit f93b3b159b.

* Revert "RequiresUnreferencedCode added to ExpressionSerializer"

This reverts commit 06fecec9a6.

* Revert "various components RequiresUnreferencedCode attribute added (#2120)"

This reverts commit 2ed1a6cac1.

* Remove RadzenHtml.

* ExpressionSerializer FormatValue updated to use InvariantCulture

* Catch potential JS interop exceptions that could occur during disposing.

* Revert "Remove RadzenHtml."

This reverts commit 319085bf49.

* SelectBar made single tab stop

* RadioButtonList and CheckBoxList made single tab stop

* SelectBar accessibility improved

* RadioButtonList accessibility improved

* CheckBoxList accessibility improved

* Update radio button focus styles

* Update checkbox list focus styles

* Update Checkbox Radio and SelectBar focus styles

* SelectBar, CheckBoxList and RadioButtonList focus state improved

* Check for Multiple added

* Use non-rendering event handlers for transitionend.

* Rename css class rz-selectbutton to rz-selectbar and improve focus states

* Fix selectbar focus outline offset

* Update premium themes

* Selectbar item focus styles should not be visible if the item is disabled.

* CheckBoxList and RadioButtonList item focus should be visible only on keyboard input

* SelectBar, CheckBoxList and RadioButtonList focus logic improved

* Update animations

* RadzenText margin-block should be 0 if it is in RadzenStack. Resolves #2134

* RadzenText margin-block should be 0 if it is first level child in RadzenStack

* CheckBoxList focused fixed

* Add toggle state classes to panel menu icon.

* Update accordion styles to reflect expander changes

* Add animation styles to expand arrow in Menu and ProfileMenu

* Use a instead of NavLink as it seems to cause performance issues.

* Set @bind-Expanded.

* Revert "Set @bind-Expanded."

This reverts commit 994107367bdf09043950f8bbe701eb9edefec676.

* Revert "Use a instead of NavLink as it seems to cause performance issues."

This reverts commit 05d5bef8f421bbeb5828ba1e9c5af6793ea3d32a.

* Reduce rendering of panel menu items.

* Add panel menu component.

* Use ChildContent to render the toggle icon of the panel menu item.

* Sync panel menu item selection in the item itself.

* Rename ExpandedInternal to expanded.

* Move filtering to the panel menu component.

* Remove the transitionend handler to avoid a second rendering pass.

* Build the assets for the net9.0 framework.

* Do not trigger render when Click is used.

* Panel menu keyboard navigation renders only when needed.

* Focus reworked to use AsNonRenderingEventHandler

* Focus the first item.

* Update Panel demo

* Use a more robust algorithm for month view event rendering that handles overlapping of events across a week.

* Use RadzenStack in RadioButtonList

* Add parsing support for `&` and `|`.

* Add parsing support for `^`, `>>` and `<<`.

* Simplify expression parsing tests.

* Use RadzenStack in RadioButtonList and CheckBoxList

* Change defaults for AlignItems and JustifyContent in RadioButtonList and CheckBoxList

* Update RadioButtonList and CheckBoxList demos

* Add --rz-input-border-block-end css variables to improve Fluent theme styles

* Removed AsNonRenderingEventHandler from RadioButtonList and HtmlEditor focus and blur

* Removed AsNonRenderingEventHandler from CheckBoxList

* Simplify RadzenTable rendering.

* Optimize memory usage of the ClassList utility.

* Refactor RadzenButton to use ClassList.

* RadzenSelectBar and RadzenSplitButton use ClassList.

* Refactor RadzenBadge and RadzenAlert to use ClassList.

* Refactor RadzenCard and RadzenFormField to use ClassList.

* Refactor RadzenCardGroup and progress components to use ClassList.

* Refactor RadzenMenu to use ClassList.

* Use ClassList in RadzenBody, RadzenLayout and editor rendering components.

* RadzenDialog uses ClassList.

* RadzenDataGrid uses ClassList.

* RadzenPager uses ClassList.

* RadzenColumn uses ClassList.

* Fix RadzenSplitButtonItem focused state.

---------

Co-authored-by: Atanas Korchev <akorchev@gmail.com>
Co-authored-by: Atanas Korchev <454726+akorchev@users.noreply.github.com>
Co-authored-by: yordanov <vasil@yordanov.info>
Co-authored-by: Quentin H <67709967+quintushr@users.noreply.github.com>
2025-05-07 13:11:30 +03:00
yordanov
513e63329b Fix RadzenSplitButtonItem focused state. Resolves #2137 2025-05-05 16:36:54 +03:00
amir7800h
cfd104385d Update RadzenDatePicker.razor.cs (#2140)
When setting the project culture (e.g., to fa-IR), the year was still being displayed using the Gregorian calendar, which caused incorrect date representation in the UI.
This fix ensures that the DateTimeFormat.Calendar is properly respected, and now the year is displayed correctly based on the assigned calendar (e.g., PersianCalendar).

Before
Culture set to fa-IR

Year displayed as Gregorian (e.g., 2025 instead of 1404)

After
Year display matches the Calendar set in DateTimeFormat

Fully localized and culture-respecting date output
2025-05-05 08:09:59 +03:00
Atanas Korchev
2d9641eecf Toc preserves query string parameters. 2025-05-03 15:18:09 +03:00
Vladimir Enchev
65a78125b2 DropDownBase select item as you type fixed 2025-04-30 11:45:09 +03:00
Vladimir Enchev
28572ba4d3 version updated 2025-04-29 18:06:24 +03:00
Vladimir Enchev
28a603ca1e Dynamic Where() with parameters fixed 2025-04-29 18:06:11 +03:00
Frank
b9fa303f7f NotifyValidationStateChanged() / StateHasChanged() is only be executed if the IsValid field has actually changed. (#2127) 2025-04-28 11:14:11 +03:00
Vladimir Enchev
756dde90ab DataGrid will allow combining advanced and simple filtering for different columns
Close #2128
2025-04-25 11:40:59 +03:00
Vladimir Enchev
c2a396167e DataGrid advanced filter clear fixed 2025-04-24 16:34:23 +03:00
Vladimir Enchev
c1fd207723 Version updated 2025-04-24 13:05:03 +03:00
Vladimir Enchev
8f1fc0a164 Revert "QueryableExtension OrderBy() should not use ExpressionParser"
This reverts commit a389dc702b.
2025-04-24 13:04:31 +03:00
Evan Dixon
538ec3c744 Make OnChipRemove overridable (#2122)
Co-authored-by: Dixon, Evan <edixon@mimeo.com>
2025-04-24 08:43:47 +03:00
Victor Ureta
6fed13bf12 Add TextTemplate parameter to RadzenFormField Blazor component (#2123)
* Update RadzenFormField.razor

* Update RadzenFormField.razor.cs
2025-04-24 08:25:02 +03:00
Vladimir Enchev
104cc7c900 Version updated 2025-04-23 10:00:49 +03:00
Reinhard
8fa699a92e Fixing 1st column with sub-row expander visible not taking column.IsInEditMode callback into account for rendering edit template. (#2097) 2025-04-23 09:06:24 +03:00
Quasmo
1ab7059830 Added DisplayToolTipFor Method to RadzenChart.razor.cs. (#2115)
* Added DisplayToolTipFor Method to RadzenChart.razor.cs. Made Series property public.

* Chnaged Series public accessor back to internal. Added GetSeries Method that returns an IReadOnlyList of the Series. Added XML documentation. Made changes to DisplayTooltipFor Method. Removed null checks and try/catch block.

* removed unnessecary usings.

---------

Co-authored-by: John Tolliver <jtolliver@wbrsllc.com>
2025-04-23 07:44:50 +03:00
Greg Horvath
377b2613db Fixes bug #2113. (#2114) 2025-04-23 07:18:00 +03:00
Atanas Korchev
bc7b0a9bdb Take axis label rotation into account when measuring the category axis size. 2025-04-22 10:36:09 +03:00
Krystian Szatan
8665a351db Update RadzenSplitButton (#2103)
Added ButtonType parameter to allow EditForm submit
2025-04-18 11:29:44 +03:00
Vladimir Enchev
dd02bf8b8d Notification demo reworked with toc 2025-04-16 11:31:03 +03:00
Vladimir Enchev
487c423eef Version updated 2025-04-15 18:20:24 +03:00
Atanas Korchev
87bcfa729c RadzenMarkdown renders HTML attributes incorrectly. 2025-04-15 18:18:50 +03:00
yordanov
0332e8c671 Fix anchors on Accordion demo page 2025-04-15 15:56:37 +03:00
Atanas Korchev
7264354ce6 Soft line breaks are not rendered and strip whitespace. Fixes #2095. 2025-04-15 13:14:05 +03:00
Vladimir Enchev
bde3315994 Version updated again 2025-04-15 12:59:24 +03:00
Vladimir Enchev
7ecd08b0d8 Version raised 2025-04-15 12:58:01 +03:00
Vladimir Enchev
93feb382ca Version updated 2025-04-15 12:56:35 +03:00
yordanov
186d9b3798 Update toc demos 2025-04-15 12:27:23 +03:00
yordanov
1f3e44819d Remove selected rz-toc-item font-weight and fix width of rz-toc-link 2025-04-15 12:06:33 +03:00
Atanas Korchev
4b942f2f45 Add toc to the upload demo. 2025-04-15 11:46:29 +03:00
Vasil Yordanov
7214cd7179 Add RadzenToc component (#2094)
RadzenToc component is a table of contents based on the titles in a page allowing users to quickly navigate the page.

---------

Co-authored-by: Atanas Korchev <akorchev@gmail.com>
2025-04-15 11:14:38 +03:00
Vladimir Enchev
90c11b5c04 Version updated 2025-04-14 11:39:59 +03:00
Atanas Korchev
4007339d26 Sanitize link and image destination. 2025-04-14 11:37:18 +03:00
yordanov
8bc43443a7 Fix contrast with new styles for active filter in DataGrid's simple filter modes. Resolves #2090 2025-04-14 10:26:52 +03:00
Vladimir Enchev
a389dc702b QueryableExtension OrderBy() should not use ExpressionParser 2025-04-14 09:33:02 +03:00
Nopke
1b39fa37e0 TimeSpanPicker: fix not using last changed input value, disable autocomplete (#2089)
* TimeSpanPicker:
* fix occasionally not using last changed input value after clicking the confirmation button,
* disable autocomplete in unit fields

* TimeSpanPicker:
* store last field input and try to parse it only if needed
2025-04-14 09:03:41 +03:00
Atanas Korchev
bfa18f72fa Sanitize HTML content in RadzenMarkdown. Expose a property to disable html content (AllowHtml) and also provide means to provide a custom list of allowed HTML tags and attributes. 2025-04-13 15:58:20 +03:00
Vladimir Enchev
82c2ec0c43 Version updated 2025-04-11 10:00:14 +03:00
Pedro Constantino
3810d088b5 [FEAT] - Add Fill property to RadzenSeriesDataLabels for customizing text color, default behavior is preserved when Fill is not specified. (#2086)
Co-authored-by: Pedro Constantino <pedro.constantino@enerwatt.com.br>
2025-04-11 09:38:43 +03:00
yordanov
7b34b096fe Update premium themes 2025-04-11 08:47:20 +03:00
leon42
831b0bd2d1 Added a ShowInput property to RadzenDatePicker to allow only the button to be shown. See the demo for an example. (#2085)
Added demos for ShowInput and ShowDateBox options.

Co-authored-by: Noel <noel.s@ivoterguide.com>
2025-04-11 08:23:30 +03:00
Vladimir Enchev
5875057282 Fixed Tabs component re-rendered on every focus
Close #2087
2025-04-11 07:56:27 +03:00
Vladimir Enchev
b78df8df2a Fix Ctrl / Alt navigate in DropDownList
Fix #2084
2025-04-10 14:21:47 +03:00
Vladimir Enchev
7fa3d08e61 DataGrid expand/collapse column headers accessibility error fixed 2025-04-10 11:00:00 +03:00
Vladimir Enchev
549303a34c code improved 2025-04-10 09:50:19 +03:00
Vladimir Enchev
9d2cbae115 DropDownBase should not try to handle keypress when not bound 2025-04-09 18:57:55 +03:00
Vladimir Enchev
d1a76922c5 Version updated 2025-04-09 12:23:00 +03:00
yordanov
648889d2d2 Accordion header icon styles should not propagate accross other icons. Resolves #2081 2025-04-09 10:26:51 +03:00
Vladimir Enchev
984e566fe2 RadzenLink cannot be clicked in Popup
Fix #2083
2025-04-09 10:12:30 +03:00
Atanas Korchev
145296ee10 RadzenTheme no longer requires PersistentComponentState but uses it if available. 2025-04-09 10:03:21 +03:00
yordanov
91b91ca96f Add RealEstate app template 2025-04-09 09:46:11 +03:00
Atanas Korchev
5ea1e9d6d5 PagingSummaryFormat does not apply to the bottom pager in RadzenDataGrid. 2025-04-09 09:23:41 +03:00
Vladimir Enchev
78f83fe103 Version updated 2025-04-08 17:32:55 +03:00
Atanas Korchev
1b0ee6a757 Add specific upload examples. 2025-04-08 13:36:52 +03:00
Atanas Korchev
c185853405 Update the changelog to include 6.x changes. 2025-04-08 10:05:24 +03:00
Atanas Korchev
f50b8bceb6 Js minify (#2080)
* Minify the JavaScript.

* Update the terser package.
2025-04-08 09:32:06 +03:00
Atanas Korchev
559a10603a Update the innerHTML of the editor when source changes. 2025-04-08 07:58:48 +03:00
Atanas Korchev
defe38daaa Update the HTML content of RadzenHtmlEditor after running ValueChanged. 2025-04-07 16:54:46 +03:00
Vladimir Enchev
6acdaf0603 Version updated 2025-04-07 16:06:51 +03:00
Vladimir Enchev
e9991fc995 DialogService Alert() and Confirm() overloads with RenderFragment added 2025-04-07 14:55:22 +03:00
Vladimir Enchev
7ad14174d7 code fixed 2025-04-07 10:54:12 +03:00
Vladimir Enchev
a638387dd4 DataGrid FilterOperator changing to Contains on clear even when restricted
Fix #2074
2025-04-07 10:28:59 +03:00
Jeffrey van der Stad
3818d0e607 PR: Add PagingSummaryTemplate to RadzenDataGrid (Fixes [#2077](https://github.com/radzenhq/radzen-blazor/issues/2077)) (#2079)
* fix: added PagingSummaryTemplate as replacement for PagingSummaryFormat

* In `DataGridPagerApi.razor`, added a `PagingSummaryTemplate` within the `RadzenDataGrid` component to provide a visual summary of the current page and total records displayed.
2025-04-05 15:51:16 +03:00
Jeffrey van der Stad
3ce4f8da3a Updated unit tests in DialogServiceTests.cs to assert properties OkButtonText and CancelButtonText in ConfirmOptions and AlertOptions, with default values set to "Ok" and "Cancel". Modified DialogService.cs to initialize these properties if not specified. (#2063) 2025-03-30 10:48:45 +03:00
Vladimir Enchev
9d272a1b19 version updated 2025-03-29 16:48:58 +02:00
Atanas Korchev
3b9224b4da Parse images inside markdown links. Closes #2062. 2025-03-29 16:15:02 +02:00
Atanas Korchev
b254152746 Remove markup string (#2066)
* Remove MarkupString usage from the demos.

* Remove MarkupString from RadzenPanelMenu.

* Remove MarkupString from RadzenAccordion.

* Remove MarkupString from RadzenIcon

* Remove MarkupString from RadzenSplitButton

* Remove MarkupString from RadzenButton

* Add spaces in the keyboard navigation grid.

* Remove MarkupString from RadzenDialog

* Use literal strings for the icons in the Index page.

* Remove MarkupString from RadzenSelectbar

* Remove MarkupString from RadzenFieldset

* Remove MarkupString from everything else.
2025-03-29 12:30:41 +02:00
yordanov
a2d796476e Update premium themes 2025-03-28 15:25:46 +02:00
nielsNocore
898e744767 Fix small dot in rz-ripple on state :active (#2061) 2025-03-28 15:22:25 +02:00
Vladimir Enchev
9ef9c5b3de Numeric MaxLength property added 2025-03-28 14:25:11 +02:00
Vladimir Enchev
b734eeb252 Version updated 2025-03-28 07:39:29 +02:00
Vladimir Enchev
92d69d9053 DialogService Alert and Confirm buttons text missing when no options are specified
Fix #2060
2025-03-28 07:39:12 +02:00
Vladimir Enchev
4879c49476 Version updated 2025-03-26 17:10:50 +02:00
Vladimir Enchev
e81ea15bb0 DropDown select item with SPACE added
Closes #2011, #2054
2025-03-26 09:18:55 +02:00
Vladimir Enchev
647f174b53 Menu should be closed on location changed not toggled 2025-03-25 13:14:39 +02:00
Atanas Korchev
d79e6a7606 Escaped characters in Markdown tables display as a number. Fixes #2053. 2025-03-24 11:39:32 +02:00
Jeffrey van der Stad
3fd3916ef9 Enhance dialog options with property change notifications to support automatic UI updates (#2051)
* Enhance dialog options with property change notifications

* Update dialog properties and functionality

- Changed dialog text in `DialogPage.razor` to reflect new functionality.
- Updated button text in `DialogWithCascadingValueCaller.razor` and modified dialog title.
- Enhanced `DialogWithCascadingValueImplementation.razor` with new buttons for setting titles and toggling close options, and added methods for updating the dialog title and counter.

* Add unit tests and improve DialogService options handling

* Allow internal methods visibility for unit tests

Added an `ItemGroup` in the project file to include an
`InternalsVisibleTo` attribute for the `Radzen.Blazor.Tests`
assembly, enabling unit tests to access internal methods.
2025-03-24 08:10:57 +02:00
yordanov
b9b73d44c2 Fix titles and descriptions of UI Blocks demo pages 2025-03-21 15:01:28 +02:00
Vladimir Enchev
afa7c2030c demos update 2025-03-21 08:02:17 +02:00
Vladimir Enchev
f668a7a629 Version updated 2025-03-21 07:04:37 +02:00
Vladimir Enchev
958fb43ac2 Should not use Coalesce in OrderBy() 2025-03-21 07:04:17 +02:00
Jeffrey van der Stad
9ef8f592ba Add cascading value support to dialog components (#2046)
- Updated `DialogContainer.razor` to wrap the `Dialog` in a `CascadingValue`, enabling access to cascading parameters.
- Enhanced `DialogPage.razor` with explanatory text and a link to documentation regarding cascading values.
- Introduced `DialogWithCascadingValueCaller.razor` to provide a button for opening a dialog that uses cascading values.
- Created `DialogWithCascadingValueImplementation.razor` to implement dynamic dialog functionality with a counter that updates the title.
2025-03-21 07:01:14 +02:00
yordanov
e6feae6c68 Update premium themes 2025-03-20 17:00:43 +02:00
yordanov
9dc5810eaa Update TimeSpanPicker styles 2025-03-20 17:00:43 +02:00
Vladimir Enchev
c960d6d96a RadzenText demo description fixed 2025-03-20 14:42:44 +02:00
Nopke
45d95d7b45 New component: TimeSpanPicker (#2037)
* TimeSpanPicker: initial

* TimeSpanPicker: adjust material themes, handle long labels

* TimeSpanPicker: fix value set, move invoking Change events for consistency with other components, update demos

* TimeSpanPicker: fix preventKeyPress, adjust styles

* TimeSpanPicker: adjust demos and add new

* PopupOrInline: handle Inline parameter change

* TimeSpanPicker: change icon and improve handling Disabled/ReadMe parameters in panel

* TimeSpanPicker: update demos

* TimeSpanPicker: add to the example panel

* DatePicker: add a tag and info about time picker

* TimeSpanPicker: minor code cleanup, adjust styles, open popup on click if input not allowed

* * TimeSpanPicker: minor style and code adjustments
* DatePicker: add readonly class only if not disabled

* TimeSpanPicker: add to the index page

* TimeSpanPicker: adjust style code, use proper minus sign

* TimeSpanPicker:
* read input value without using JS
* fix Value and ConfirmedValue inconsistency

* TimeSpanPicker: always refresh the displayed value on input change

* TimeSpanPicker: add some tests (not finished yet)

* Numeric: fix culture-dependent test fail

* TimeSpanPicker: reorganize parameters

* TimeSpanPicker tests: cover input parameters, add regions

* TimeSpanPicker: tweak regions of input and panel parameters

* TimeSpanPicker tests: reorganize regions, use more specific selectors

* TimeSpanPicker: improve value change and popup toggle prevention

* TimeSpanPicker:
* use culture while parsing
* use consistent culture in tests

* FormField: add TimeSpanPicker in the demo

* TimeSpanPicker: more tests

* revert unrelated and unintended changed

* TimeSpanPicker test: move theory data to properties, add step tests

* TimeSpanPicker test: minor code cleanup, add remaining label tests

* TimeSpanPicker test: use dictionary of unit element selectors

* TimeSpanPicker test: create separate methods instead passing field expressions as theory, add more regions

* TimeSpanPicker: rename properties to match the convention and update their summaries

* TimeSpanPicker: remove UnconfirmedValueChanged param
TimeSpanPicker test: add event-related tests for the panel, simplify some tests related to timespan units

* TimeSpanPicker: pass culture to numeric fields, add panel field rendering tests

* TimeSpanPicker: unify unit fields under one RenderFragment function

* TimeSpanPicker: fix missing microseconds picker, fix classes of input fields

* TimeSpanPicker:
* prevent from changing UnconfirmedValue externally by changing Value,
* prevent from raising ValueChanged and Change events if the value haven't actually change

* revert unintended changes

* TimeSpanPicker, DatePicker: fix icon positions in FormFields having filled and flat variants

* TimeSpanPicker: make AllowClear false by default, like in DatePicker

* revert unintended change in demos project

* * remove PopupOrInline component
* revert changes in Popup component
* TimeSpanPicker: reimplement inline/popup handling
2025-03-20 10:37:43 +02:00
Vladimir Enchev
3d1c32a5b4 Version updated 2025-03-20 08:57:23 +02:00
Atanas Korchev
6a67580c33 Replace AutoLinkHeadings with AutoLinkHeadingDepth. 2025-03-20 08:49:36 +02:00
Vladimir Enchev
dbafa0a473 Should not use Coalesce in OrderBy() for non sub properties 2025-03-20 07:43:12 +02:00
Atanas Korchev
d348e73b55 Append html block elements as content during markdown rendering. 2025-03-19 19:06:43 +02:00
Atanas Korchev
3f3c1bd6e3 Add support for self-closed and void HTML elements in RadzenMarkdown. 2025-03-19 18:12:40 +02:00
Vladimir Enchev
1a3f907c62 DataGrid filtering by DateOnly and TimeOnly settings save/load fixed 2025-03-19 16:37:48 +02:00
Atanas Korchev
4f8027c6f7 Support URL fragments when matching the current URL. 2025-03-19 14:59:28 +02:00
Atanas Korchev
86cd2a976b Improve support for html tags inside RadzenMarkdown. 2025-03-19 11:51:41 +02:00
Vladimir Enchev
52a935bf09 RadzenSplitterPane class attribute not rendered 2025-03-19 11:01:58 +02:00
Atanas Korchev
ea7c3c4a5b RadzenMarkdown fails to parse image after strong node. 2025-03-19 09:15:12 +02:00
Atanas Korchev
9639d98dc3 Allow auto linking of markdown headers. 2025-03-19 08:09:04 +02:00
Vladimir Enchev
3bfc16b6a8 Version updated 2025-03-18 17:54:25 +02:00
Atanas Korchev
1e54619e81 Markdown table alignment support. 2025-03-18 17:46:31 +02:00
yordanov
fc4ac98246 Update demo pages 2025-03-18 17:40:36 +02:00
yordanov
534e3ee98f Card text styles should only affect text components that are placed directly inside RadzenCard 2025-03-18 17:40:10 +02:00
yordanov
9b8b8f020d Add App Templates 2025-03-18 16:49:27 +02:00
Atanas Korchev
aea7fb8069 Markdown (#2039)
* Add markdown parser and tests.

* Add demo.

* Add to home page.

* Add demo for nested blazor components.

* Use Radzen components for markdown rendering.
2025-03-18 16:29:32 +02:00
Vladimir Enchev
b933633cc0 code fixed 2025-03-18 15:15:38 +02:00
Vladimir Enchev
a822eb521d Version updated 2025-03-18 15:15:24 +02:00
Vladimir Enchev
b6bb19538f Fixed Popup IAsyncDisposable implementation caused errors 2025-03-18 15:14:59 +02:00
Vladimir Enchev
c0a1f86801 Version updated 2025-03-17 17:12:16 +02:00
yordanov
439d79c677 Fix Carousel scroll on touch devices 2025-03-17 16:51:38 +02:00
Vladimir Enchev
b6ee0f118e Menu should be closed on location change 2025-03-17 15:11:16 +02:00
Vladimir Enchev
ed037ca6d7 Popup IAsyncDisposable implemented 2025-03-17 13:40:35 +02:00
Vladimir Enchev
58f6239cb2 Version updated 2025-03-17 09:03:43 +02:00
Vladimir Enchev
5e7bfcb591 Accordion item Visible will not show the item if hidden 2025-03-17 07:47:09 +02:00
protoface
03cd99c43c Use RadzenDataGrid.EnumFilterTranslationFunc where applicable (#2036) 2025-03-17 07:35:48 +02:00
Vladimir Enchev
dc1742aca6 SideDialog ContentCssClass not applied 2025-03-17 07:33:53 +02:00
Atanas Korchev
a23142cc3f Scheduler navigation buttons submit containing form. Fixes #2035. 2025-03-15 08:37:27 +02:00
Vladimir Enchev
de47319b00 Version updated 2025-03-14 15:22:40 +02:00
Vladimir Enchev
c9a2549018 DatePicker ReadOnly should not allow selecting a day with keyboard
Fix #2028
2025-03-13 14:54:53 +02:00
Maks
966253f7b6 Enhanced ReloadSettings and LoadSettingsInternal methods with additional parameters and improved documentation (#2027)
- Added `forceReload` parameter to `ReloadSettings` method to allow forced reloading of settings.
- Added `forceUpdate` parameter to `LoadSettingsInternal` method to allow forced updates of settings.
- Updated XML comments for both methods to clarify their purpose, parameters, and behavior.

Co-authored-by: Sacred <Sacred>
2025-03-12 13:28:09 +02:00
Vladimir Enchev
b0ce7fb3cf DropDownDataGrid PopupStyle added 2025-03-11 15:59:03 +02:00
Vladimir Enchev
f1f85a6563 version updated 2025-03-11 13:59:56 +02:00
Vladimir Enchev
c10ee5e0a8 Avoid executing possible IQueryable code on DataGrid RowSelect 2025-03-11 13:59:39 +02:00
Joel Mandell
7e86d3128c (docs): Clarify that ShowHeader needs to be set to true if using NavigationTemplate. (#2026) 2025-03-11 13:29:30 +02:00
Frank
4f3da0ea2e Added SameSite and Secure options for theme cookie. (#2025)
* Added SameSite and Secure options for theme cookie.

* CookieThemeServiceOptionsSameSiteMode renamed to CookieSameSiteMode
2025-03-10 17:35:51 +02:00
Vladimir Enchev
406c7d8c6b Version updated 2025-03-10 10:36:01 +02:00
yordanov
809f1192f4 Update premium themes 2025-03-10 10:06:12 +02:00
Nopke
ff280d27f1 AutoComplete, Numeric: fix font family in inputs (#2022) 2025-03-10 09:37:25 +02:00
Ben Croughs
c566eb95a2 != null causing issues on KeyValuePart<string,int?> (#2021)
Co-authored-by: Ben Croughs <ben.croughs@telenet.be>
2025-03-10 07:49:19 +02:00
Vladimir Enchev
393d80600c OrderBy() by string selector improved to handle better sort strings discovery 2025-03-07 11:14:15 +02:00
Vladimir Enchev
1e44165d18 demo updated 2025-03-07 06:46:58 +02:00
Vladimir Enchev
03f4774810 version updated 2025-03-07 06:24:08 +02:00
Vladimir Enchev
b64e0446a3 DataGrid sorting of non dynamic data fixed 2025-03-07 06:23:48 +02:00
Vladimir Enchev
e51cab2418 version updated 2025-03-06 21:09:47 +02:00
Vladimir Enchev
f533d106c1 DataGrid dynamic data sort by property with space in the name fixed 2025-03-06 21:09:26 +02:00
Vladimir Enchev
2557691053 DataGrid dynamic data source fixed 2025-03-06 21:03:35 +02:00
Vladimir Enchev
5f53f95b5d Version updated 2025-03-06 15:47:04 +02:00
Vladimir Enchev
2c71574072 Revert "Fixed datagrid sort issues due to duplicate sortDescriptor lists. (#1996)"
This reverts commit 57f1417d4c.
2025-03-06 15:46:03 +02:00
Vladimir Enchev
45d38494f2 version updated 2025-03-06 10:32:31 +02:00
Vladimir Enchev
5c03d0d14c QueryableExtension.Select made internal 2025-03-06 10:30:58 +02:00
Vladimir Enchev
8404294f56 minor version raised 2025-03-06 09:14:33 +02:00
Vladimir Enchev
7f519bb71f Version updated 2025-03-06 09:13:06 +02:00
Vladimir Enchev
7a5995002d Select() by string element type discovery improved 2025-03-06 09:12:39 +02:00
Vladimir Enchev
ba8fe8e830 DataGrid SaveSettings() method made public
Fix #2016
2025-03-05 17:56:03 +02:00
Vladimir Enchev
949a5b19cc Popup focus first element made faster 2025-03-05 17:33:18 +02:00
yordanov
5a68a00f38 Optimize Material Symbols variable icon font 2025-03-05 17:06:23 +02:00
Vladimir Enchev
d52d91852a Select() by string will use first element type if source.ElementType is object 2025-03-05 16:18:08 +02:00
Vladimir Enchev
b8b39e1ebd Member access in GetNestedPropertyExpression() should support interfaces
Fix #2015
2025-03-05 13:57:51 +02:00
Vladimir Enchev
e795e63d02 Version updated 2025-03-04 13:31:31 +02:00
Vladimir Enchev
27dc58a90f Progress indicator for Duration of the Notification added (#2014)
* Notification ShowProgress added

* Style Notification with ProgressBar

---------

Co-authored-by: yordanov <vasil@yordanov.info>
2025-03-04 13:30:29 +02:00
Vladimir Enchev
d4679353af demo improved 2025-03-04 10:57:38 +02:00
Vladimir Enchev
73a53c8829 MyCustomDataFilterProperty and MyCustomEnumerableDataFilterProperty removed 2025-03-04 09:48:17 +02:00
Vladimir Enchev
eb51f78f8c Added DataFilter/DataGrid support for filtering enumerable properties with Contains operator and single value 2025-03-04 08:46:10 +02:00
Vladimir Enchev
8667443e64 RadzenDataFilterProperty FilterOperators property added similar to RadzenDataGridColumn 2025-03-03 17:20:20 +02:00
Vladimir Enchev
0650472551 code improved 2025-03-03 16:29:38 +02:00
Vladimir Enchev
7653ea683a Version updated 2025-03-03 16:28:20 +02:00
Vladimir Enchev
d633d2f424 Select() by string should support lambda expressions
Fix #2012
2025-03-03 16:23:08 +02:00
Vladimir Enchev
98ec370545 DataGrid column resize defaults to 0px width 2025-03-03 16:12:43 +02:00
Vladimir Enchev
51b9579d6e Demo update to allow only proper operators
Fix #2013
2025-03-03 16:06:33 +02:00
Vladimir Enchev
af42791fa2 Demo update to use proper operators when comparing collections 2025-03-03 16:00:18 +02:00
Atanas Korchev
f7e9a88575 RadzenStackedColumnSeries and RadzenStackedBar series support negative values. Closes #2001. 2025-03-01 15:45:09 +02:00
Vladimir Enchev
331ac4787e XML comment fixed 2025-03-01 12:20:52 +02:00
Vladimir Enchev
4ac2e0aa49 Version updated 2025-03-01 12:19:06 +02:00
Vladimir Enchev
0cb10c206a Where() method by string query cannot be translated error fixed 2025-03-01 12:18:38 +02:00
Atanas Korchev
5fd031f518 Moving appointments in month view resets their start and end time. Closes #2000. 2025-03-01 12:02:23 +02:00
Vladimir Enchev
48c204a7b0 Where() method by string simplified to avoid provider translation problems 2025-03-01 11:45:46 +02:00
Vladimir Enchev
e61fc1c84e Version updated 2025-03-01 11:10:53 +02:00
Vladimir Enchev
fb64381af9 Where() by string should handle null item 2025-03-01 11:03:07 +02:00
五味 凌一
84954f1919 Added “YearFormatter” property in RadzenDatePicker to allow custom formatting of Year (#2008)
* Added “YearFormatter” property to allow custom formatting of Year

This enhancement aims to enable immediate response to changes in the era name without relying on .NET’s built-in implementation.

* Update YearFormatter property initialization and usage

Changed the initial value of the YearFormatter property from null to the FormatYear method. Added a constructor to RadzenDatePicker where the YearFormatter property is set to the FormatYear method.

* Add xml comment.
2025-03-01 10:44:05 +02:00
Vladimir Enchev
cdabe516eb DialogService non generic open methods added
Fix #1987
2025-02-28 11:08:58 +02:00
yordanov
e5ddb2cef9 Update premium themes 2025-02-28 11:00:55 +02:00
Nopke
963185b927 FormField: fix css selectors for variants to avoid capturing variants of containers (#2002) 2025-02-28 10:58:06 +02:00
Vladimir Enchev
f7ef6a207e The ref parameter may be null in destroyChart()
Fix #1997
2025-02-27 15:52:13 +02:00
Vladimir Enchev
8088d14340 Added MaxWidth property for RadzenDataGridColumn
Fix #1993
2025-02-27 15:44:58 +02:00
Atanas Korchev
f710b782a3 RadzenDatePicker supports custom year formatting via the YearFormat property. 2025-02-27 13:48:58 +02:00
Vladimir Enchev
b13921f9aa demo improved 2025-02-26 17:16:52 +02:00
Vladimir Enchev
522191052a Tree filter demo added
Fix #1948
2025-02-26 16:20:22 +02:00
yordanov
e9e8844342 Add new UI Blocks to the online demos 2025-02-26 15:18:43 +02:00
Vladimir Enchev
ccbf7b24cf Version updated 2025-02-26 09:17:56 +02:00
nielsNocore
57f1417d4c Fixed datagrid sort issues due to duplicate sortDescriptor lists. (#1996)
* refactor datagrid sorting. to only have one ObservableCollection<SortDescriptor instead of 2

* Revert "refactor datagrid sorting. to only have one ObservableCollection<SortDescriptor instead of 2"

This reverts commit 02ef67073c.

* added sort changes without formatting

* added new unit test.

* added datagrid test with load data args.
2025-02-26 09:15:42 +02:00
Atanas Korchev
d5318e6e88 Scheduler does not show some appointments. Fixes #1995. 2025-02-26 08:51:43 +02:00
Vladimir Enchev
978e3fc610 version updated 2025-02-25 10:42:59 +02:00
Vladimir Enchev
dcb5eef814 DropDown/DropDownDataGrid case-insensitive filter cannot be translated by PG SQL provider 2025-02-25 10:42:30 +02:00
Atanas Korchev
518ad838e7 Add Select extension method for IQueryable. 2025-02-24 17:42:33 +02:00
Vladimir Enchev
ba6ced577e minor version updated 2025-02-24 16:22:37 +02:00
Vladimir Enchev
138253e728 Version updated 2025-02-24 16:21:02 +02:00
yordanov
ffd299962b Fix alternating rows color in Fluent themes 2025-02-24 16:16:52 +02:00
Vladimir Enchev
354a147155 GetBodyColumnClass and GetHeaderColumnClass renamed and made internal 2025-02-24 16:00:04 +02:00
Pedro Constantino
a623017325 [IMPROVEMENT] - Adding HeaderWhiteSpaceText and WhiteSpaceText properties to RadzenDataGridColumn. (#1999)
* [IMPROVEMENT] - Adding HeaderWhiteSpaceText and WhiteSpaceText properties to RadzenDataGridColumn.

* [IMPROVEMENT] - Improving the css and readjusting the themes folder.

* [IMPROVEMENT] - Adjusting enumerator documentation.

* [IMPROVEMENT] - Adjusting component documentation

* [IMPROVMENT] - Renaming WhiteSpace and HeaderWhiteSpace properties, renaming WhiteSpace enumerator and removing identation from csproj.

---------

Co-authored-by: Pedro Constantino <pedro.constantino@enerwatt.com.br>
2025-02-24 15:54:27 +02:00
Vladimir Enchev
654bd80abd missing cast added 2025-02-22 12:29:20 +02:00
Vladimir Enchev
3249591351 EnumerableAsString() will handle single values 2025-02-22 11:41:01 +02:00
Vladimir Enchev
40f934d7a6 version updated 2025-02-21 20:07:25 +02:00
Vladimir Enchev
53717dcc46 DataGrid CheckBoxList filter list cast exception fixed
Fix #1998
2025-02-21 20:06:59 +02:00
Vladimir Enchev
f16a00fe2e Fixed exception with DataGrid sorting and property name equal to System Type name
Obsolete code related to Dynamic LINQ deleted
2025-02-21 12:55:18 +02:00
Vladimir Enchev
ab1af0ce28 Version updated 2025-02-21 08:37:58 +02:00
Vladimir Enchev
be444bbbc4 ToFilterString() should export Boolean arrays as valid C# 2025-02-21 08:37:33 +02:00
Vladimir Enchev
e161872604 Fixed Tree is breaking after collapse
Fix ##1983
2025-02-20 18:27:34 +02:00
Vladimir Enchev
e21fa015bf version updated 2025-02-20 12:19:46 +02:00
Vladimir Enchev
fb3275d89f Fixed OrderBy() with complex lambdas 2025-02-20 12:18:05 +02:00
Monsieurvor
f42e4ffc71 fix linear scale step (#1989) 2025-02-20 11:24:39 +02:00
Vladimir Enchev
7b15c190d3 Revert "Fix OnSideClose being called on open (#1977)"
This reverts commit 75da4667b7.
2025-02-20 11:12:05 +02:00
Vladimir Enchev
73c2d27620 Saved DataGrid filter with CheckBoxList filter type will raise exception on settings load 2025-02-20 11:00:53 +02:00
nielsNocore
288db125ce added GetSelectedSources and GetSelectedTargets in the picklist to get wich items are selected. Or to get if any item is selected. (#1982) 2025-02-20 05:01:52 +02:00
Victor Ureta
0effaae435 Update RadzenDropDownDataGrid.razor.cs (#1985) 2025-02-20 05:00:34 +02:00
Vladimir Enchev
a71f1dd3c6 Version updated 2025-02-19 18:42:31 +02:00
Vladimir Enchev
c08843d587 Revert "Update RadzenDatePicker.razor.cs (#1980)"
This reverts commit b16f95a4ba.
2025-02-19 18:42:09 +02:00
Vladimir Enchev
44094cd4f4 comments fixed 2025-02-19 18:09:44 +02:00
Vladimir Enchev
a4bd9d518c Version updated 2025-02-19 17:58:41 +02:00
Vladimir Enchev
a739a6e24a Generic OrderBy<T>() method by string selector fixed to support lambda 2025-02-19 17:58:20 +02:00
Vladimir Enchev
955b52d54b duplicate test removed 2025-02-19 15:25:48 +02:00
Atanas Korchev
004293008c ExpressionParser supports typed arrays. 2025-02-19 15:23:28 +02:00
Vladimir Enchev
9f26a0b512 Should_SupportNullableCollection test added 2025-02-19 15:07:39 +02:00
CreateCode
b16f95a4ba Update RadzenDatePicker.razor.cs (#1980)
In some cultures, such as Portuguese (Portugal), the name of the day of the week appears in full, overlapping one another. This change forces the abbreviation of the name of the day of the week.
2025-02-19 15:03:41 +02:00
Vladimir Enchev
c079531d2e Fixed DataGrid CheckBoxFilter values not sorted 2025-02-19 15:00:24 +02:00
Atanas Korchev
773f2dc074 ExpressionParser supports Enumerable extension methods with IEnumerable parameters. 2025-02-19 11:35:56 +02:00
Vladimir Enchev
64d783179b Should_SupportNestedLambdasWithComplexMethod test added 2025-02-19 10:48:58 +02:00
Vladimir Enchev
3dafd6c277 Version updated 2025-02-19 09:34:05 +02:00
Vladimir Enchev
fb2fdb10b2 OrderBy() with string lambda support fixed 2025-02-19 09:33:49 +02:00
Vladimir Enchev
50dedf84d5 Where() parameters conversion between string and actual type fixed 2025-02-19 08:56:30 +02:00
Vladimir Enchev
d7b96a7dca Where() with Guid parameter fixed 2025-02-19 08:04:43 +02:00
Vladimir Enchev
1d1acdf7c8 Select improved 2025-02-18 23:14:12 +02:00
Vladimir Enchev
440bc76f08 Version updated 2025-02-18 23:10:49 +02:00
Vladimir Enchev
d2a598dbf5 Fixed DataGrid sub properties invalid expressions 2025-02-18 23:10:17 +02:00
Atanas Korchev
931f9dab6f ExpressionParser supports nested property name initializers. 2025-02-18 20:39:29 +02:00
Atanas Korchev
10ec2af398 ExpressionParser supports conditional expressions with different type. 2025-02-18 20:11:13 +02:00
Atanas Korchev
46382ef89f ExpressionParser supports DateTimeOffset.Parse. 2025-02-18 19:57:10 +02:00
Atanas Korchev
c4116feda4 Expression parsing supports the ?? operator. 2025-02-18 19:42:05 +02:00
Vladimir Enchev
4c18342cf4 Version updated 2025-02-18 17:45:21 +02:00
Atanas Korchev
92f103c1dd Support nested conditional access. 2025-02-18 17:19:02 +02:00
Vladimir Enchev
8c9374e00a Version updated 2025-02-18 11:36:53 +02:00
Vladimir Enchev
870f3d2ef9 Select expression should not create resulting type with dot in the property name 2025-02-18 11:34:51 +02:00
Vladimir Enchev
d75b3596aa ToFilterString() should convert enumerable first 2025-02-18 10:38:52 +02:00
Vladimir Enchev
c388d84f57 Version updated 2025-02-18 09:57:45 +02:00
Vladimir Enchev
782f6c9655 DataGrid FilterProperty should be used instead Property only if Property is not enumerable 2025-02-18 09:56:20 +02:00
Vladimir Enchev
078d12f2a1 DataGrid Guid column type with ToFilterStrng() support added 2025-02-18 09:42:33 +02:00
Vladimir Enchev
271e1aa26a DataGrid TimeOnly column type with ToFilterStrng() support added 2025-02-18 09:34:01 +02:00
Vladimir Enchev
c9c11c7ff7 Version updated 2025-02-18 07:51:22 +02:00
Caleb Weeks
75da4667b7 Fix OnSideClose being called on open (#1977)
Co-authored-by: Caleb Weeks <seth.weeks@carrier.com>
2025-02-18 07:50:37 +02:00
Vladimir Enchev
a2591f7f77 DataGrid FilterProperty should be used instead Property if defined 2025-02-17 19:38:13 +02:00
Vladimir Enchev
72b4e8bd39 Fixed DataGrid filtering on nullable string column will throw ReferenceNullException
Fix #1975
2025-02-17 19:05:06 +02:00
Vladimir Enchev
740b0bc3b1 Version updated 2025-02-17 16:58:30 +02:00
Vladimir Enchev
3e48b15db0 RadzenDataGridColumn FilterOperator="FilterOperator.Equals" in case of Type="typeof(string)"
Fix #1974
2025-02-17 16:56:36 +02:00
Vladimir Enchev
f1b8a22cc8 Fixed DataGrid CheckBox filtering with dynamic data 2025-02-17 16:36:03 +02:00
Pedro Constantino
02b75fcb68 [IMPROVEMENT] - Adding new option to disable radzen link, css and test added. (#1970)
* [IMPROVEMENT] - Adding new option to disable radzen link, css and test added.

* [IMPROVEMENT] - Leaving the title with lowercase letter.

* [IMPROVEMENT] - Removing version items.

---------

Co-authored-by: Pedro Constantino <pedro.constantino@enerwatt.com.br>
2025-02-17 15:10:06 +02:00
Vladimir Enchev
9b2541975f comment fixed 2025-02-17 14:38:29 +02:00
Vladimir Enchev
d3ff9a5b7c Version updated 2025-02-17 13:33:05 +02:00
Atanas Korchev
75acf7d132 Support prefix ! operator in expressions. 2025-02-17 13:27:59 +02:00
Vladimir Enchev
d6a9430c20 PrefixUnaryExpressionSyntax test added 2025-02-17 13:22:14 +02:00
Vladimir Enchev
5a89720f59 DropDown filtering by non string property fixed 2025-02-17 13:16:14 +02:00
Vladimir Enchev
bee7133e81 Fixed exception with DropDown filtering by non strong property 2025-02-17 13:01:28 +02:00
Vladimir Enchev
8522f88d66 Fixed Dropdown Filter Not Working When Binding List to Struct Class
Fix #1973
2025-02-17 12:56:57 +02:00
Vladimir Enchev
10ecc5c75d Fixed RadzenDataGrid in simple filter mode shows the date filter value at the bottom of the screen
Fix #1971
2025-02-17 12:24:43 +02:00
Atanas Korchev
cdd722c975 Refactor the ExpressionParser. 2025-02-17 11:56:01 +02:00
Vladimir Enchev
7fd9b258aa InnerException added as well 2025-02-17 11:15:20 +02:00
Vladimir Enchev
5368d398fe Where() and Select() entire exception exposed 2025-02-17 11:14:00 +02:00
Vladimir Enchev
30cc8c8711 missing comments added 2025-02-17 10:49:50 +02:00
Vladimir Enchev
f7d1fea2af Version updated 2025-02-17 10:45:30 +02:00
Vladimir Enchev
ee5674bb6d Where() and Select() methods from strings expression parsing improved (#1972)
* Add ExpressionParser and tests.

* Should_SupportNullableShortParsing test added

* Support conversion for binary operations

* Should_SupportNullablePropertiesWithArray test added

* Should_SupportDateTimeWithArray added

* code fixed

* Compilation moved to Select() only

* DataGrid columns filtering temporary switched to string expressions

* typeLocator added

* Convert arguments if needed.

* Should_SupportNestedLambdasWithEnums added

* locator improved for nested types

* ToFilterString() will not cast enums

* expression fixed

* Revert "demo reworked without strings"

This reverts commit 5e1fa61c55.

* ToFilterString() improved

* Support projections.

* ExpressionParser.ParseProjection added

* code improved

* Select by string.

* Support conditional access expressions.

* null condition added

* should add item instance name only to non indexer properties

* Array and dictionary access.

* Add editorconfig.

* Dynamic property еxpression updated to cast

* ToFilterString() removed

* Where() and Select() exception handling improved

---------

Co-authored-by: Atanas Korchev <akorchev@gmail.com>
2025-02-17 10:42:58 +02:00
Vladimir Enchev
5e1fa61c55 demo reworked without strings 2025-02-15 18:34:36 +02:00
Vladimir Enchev
92f968f933 obsolete code deleted 2025-02-15 11:07:53 +02:00
Vladimir Enchev
9eabd75864 Fixed other items not collapsed with Tree SingleExpand=true 2025-02-15 11:02:17 +02:00
Vladimir Enchev
df121e68b4 OrderBy() should return IOrderedQueryable<T> 2025-02-14 19:01:30 +02:00
Vladimir Enchev
be0721fa5d version updated 2025-02-14 18:33:36 +02:00
Vladimir Enchev
d670872d73 ToFIlterString() should output bool with proper casing 2025-02-14 18:20:40 +02:00
Vladimir Enchev
68eb9162a9 Fixed DataGrid Custom Column Filter Error With FilterCaseSensitivity="FilterCaseSensitivity.CaseInsensitive"
Fix #1969
2025-02-14 17:58:04 +02:00
Vladimir Enchev
a65ce23a10 version updated 2025-02-14 16:04:31 +02:00
Vladimir Enchev
bea9bca891 Fixed DataGrid filtering by Contains() using second filter with or 2025-02-14 16:03:31 +02:00
Vladimir Enchev
9f2c6fe8cc Fixed Empty DataGrid with FilterOperator="FilterOperator.In" and Type="typeof(IEnumerable<Enum>)">
Fix #1968
2025-02-14 15:58:06 +02:00
Vladimir Enchev
07e4276190 Accordion Refresh() method made public. 2025-02-14 15:24:41 +02:00
Vladimir Enchev
3db15c168d Version updated 2025-02-14 14:51:36 +02:00
Vladimir Enchev
7e9f66ec61 Fixed exception with filtering by string columns with null values 2025-02-14 14:51:09 +02:00
Vladimir Enchev
c7ce4ec4e3 Fixed DataGrid filtering with CustomFilterExpression column expression 2025-02-14 14:38:12 +02:00
Vladimir Enchev
ad32aed204 Fixed DataGrid grouping broken for structs
Fix #1967
2025-02-14 13:08:23 +02:00
Nopke
2df9235516 fix a test that fails or succeeds depending on the user's culture (#1963) 2025-02-14 12:09:31 +02:00
Vladimir Enchev
a0efb0b9a4 Select() should handle nested types 2025-02-14 11:38:12 +02:00
Vladimir Enchev
a244741640 version updated 2025-02-14 11:26:13 +02:00
Vladimir Enchev
e3f5360a7f ToFilterString() should not use in operator 2025-02-14 11:25:51 +02:00
Vladimir Enchev
c280bae14f ToFilterString() should cast enum value to enum type 2025-02-14 11:11:35 +02:00
Vladimir Enchev
e99671ba81 Where() should handle nested types 2025-02-14 10:49:33 +02:00
Vladimir Enchev
73338462e5 Where() should handle bools and old single = expressions 2025-02-14 10:36:12 +02:00
Vladimir Enchev
1912e69375 OrderBy() variable reference ignored if present 2025-02-14 10:28:35 +02:00
Vladimir Enchev
60a3789e9c DateTime, DateTimeOffset, DateOnly and Guid filtering by string fixed 2025-02-14 09:53:37 +02:00
Vladimir Enchev
a89bc5a5ce version updated 2025-02-14 07:16:30 +02:00
Vladimir Enchev
08f3126f46 Fixed DataGrid "Translation of method 'string.ToLowerInvariant' failed
Fix #1964
2025-02-14 07:16:14 +02:00
Vladimir Enchev
53e746146b DartSassBuilder PrivateAssets="All" added 2025-02-13 16:56:02 +02:00
Vladimir Enchev
1e7cc0d655 Version updated 2025-02-13 14:57:11 +02:00
Vladimir Enchev
b37a81b297 DataGrid filtering null reference exception fixed in some cases 2025-02-13 14:53:45 +02:00
Vladimir Enchev
6aec8118f2 DataGrid sort by dynamic type fxed 2025-02-13 14:33:53 +02:00
Vladimir Enchev
756732803f Array and List<> member access fixed 2025-02-13 14:28:08 +02:00
Vladimir Enchev
a6b8abb9ce version updated 2025-02-13 13:39:04 +02:00
Vladimir Enchev
38feb54237 Various components reworked without dynamic LINQ (#1960) 2025-02-13 13:35:41 +02:00
Vladimir Enchev
ee828de086 version updated 2025-02-12 18:05:26 +02:00
Vladimir Enchev
bc82638fea DataGrid ReloadSettings() method added 2025-02-12 18:04:20 +02:00
Lynkle
edb6f58f66 Allow AutoComplete with OpenOnFocus to show items even without user input (#1957)
* fix(autocomplete): OpenOnFocus now lets you view items in the dropdown even if you have not provided any searchText. This is for the case where you want the autocomplete but the user does/may not know what it is they want to complete.

* fix(autocomplete): Adjusted it so that removing search text back to null/empty string will reset autocomplete suggestions.
2025-02-11 18:19:59 +02:00
nielsNocore
23b9a6e7bf Fix IsAllSelectedInDropdown is correct when disabled items exists (#1953)
added unit tests for it.
2025-02-10 12:03:52 +02:00
Vladimir Enchev
0b3c6058e2 Fixed Tree recursion error during expand with SingleExpand=true 2025-02-07 14:47:12 +02:00
yordanov
cb6572980e Add demo video for Radzen Blazor for Visual Studio 2025-02-06 11:18:11 +02:00
Vladimir Enchev
78e5bbb9dc version updated 2025-02-06 10:58:16 +02:00
Vladimir Enchev
12b52b3e79 ShowVideoDialog updated 2025-02-06 10:57:41 +02:00
yordanov
e226749388 Add input width: 100%; for rz-helper-hidden-accessible class 2025-02-06 10:47:44 +02:00
yordanov
001cd0cdcd Remove input width: 0; for rz-helper-hidden-accessible class as it breaks default DropDown width 2025-02-06 10:00:52 +02:00
Vladimir Enchev
c3806348a0 Fixed FileInput breaks if both MaxWidth and MaxHeight is set and a non image is selected
Fix #1922
2025-02-06 09:28:56 +02:00
nielsNocore
c6a8d80d45 add DisabledProperty to radzenPickList (#1946)
* add DisabledProperty to radzenPickList

* moved picklist icons to be properties, to be able to custimze the icons in the picklist
2025-02-05 17:23:29 +02:00
nielsNocore
2babacd15a moved picklist icons to be properties, to be able to custimze the icons in the picklist (#1947) 2025-02-05 17:22:51 +02:00
nielsNocore
456ab29a34 added parameter to set "AllowSelectAll" in de radzenPicklist (#1945)
* added parameter to set "AllowSelectAll" in de radzenPicklist

* added default value to radzenPickList.AllowSelectAll to be true
2025-02-05 15:00:25 +02:00
Vladimir Enchev
1de201841a DataGrid CheckBoxList filter mode with EF Core data source crashes application when using keyboard
Fix #1943
2025-02-05 09:14:36 +02:00
yordanov
a5586fd4ea Add demo of RadzenIcon using FontAwesome icons 2025-02-04 17:30:01 +02:00
Vladimir Enchev
a14ac78957 Dynamic and version updated 2025-02-03 14:36:04 +02:00
yordanov
1193ede1ca Update FormField demos 2025-02-03 14:23:47 +02:00
yordanov
8ad75a1773 Update premium themes 2025-02-03 13:58:41 +02:00
rklfss
9ea1b44e64 RadzenDatePicker floating label and RadzenFormField support (#1933)
* Added support to use RadzenDatePicker in RadzenFormField

- added rz-state-empty css class to RadzenDatePicker if there is no value
- added css to handle floating labels for any form field component within RadzenFormField

* RadzenDatePicker minor code fixes and improvements

* RadzenDatePicker Demo show time part
2025-02-03 13:39:40 +02:00
Josh
b92ef7922d Add Notify overload that accepts a Timespan for duration, for improved clarity (#1936) 2025-02-03 13:20:26 +02:00
Vasil Yordanov
3d6eff639f Unify styles and behavior of forms components when invalid css class is present (#1938) 2025-02-03 10:25:13 +02:00
Atanas Korchev
011e761d7f RadzenTheme could throw a null reference exception during dispose in rare cases. 2025-02-03 09:31:52 +02:00
Atanas Korchev
a2fdb687a1 Setting the Value of RadzenHtmlEditorBackground makes it impossible to pick a color. 2025-01-31 17:21:53 +02:00
jamesalainheffer
ff846e0cc1 Added a parameter called "Size" to the "RadzenGravatar" component. (#1932)
* Added a parameter called "Size" to the "RadzenGravatar" component. This will allow for users to adjust the size of the gravatar image coming in, so that they are not stuck with just a 32px image.

* Corrected the default value from 32 to 36, and remove the width variable in favor of the parametised Size variable. (As per suggestion from akorchev - thanks)

* Corrected a typo where the "value" attribute was inside the summary, it is now outside, where it should be.
2025-01-31 09:57:22 +02:00
Vladimir Enchev
df0ad0d9f1 DropDownDataGrid item not selected after change outside of the component. 2025-01-30 16:20:35 +02:00
Vladimir Enchev
716d1a6cba Version updated 2025-01-29 08:58:01 +02:00
Frank
3e7468177d Added ReadOnly parameter for RadzenSwitch component (#1927)
* Added ReadOnly parameter for RadzenSwitch component

* Formatting
2025-01-29 08:57:29 +02:00
Vladimir Enchev
d4113e6715 Dynamic LINQ ParsingConfig initialization moved to RadzenComponent 2025-01-29 08:56:35 +02:00
Vladimir Enchev
7f7ce06d0e Version updated 2025-01-28 06:51:39 +02:00
nielsNocore
7cfea596fd fixed InvokeAsync(Reload); not awaited in datagrid en pagedDataBoundComponent (#1926) 2025-01-28 06:49:29 +02:00
Vladimir Enchev
7eae5d0f6e DataGrid sorting with dynamic data fixed 2025-01-28 06:48:29 +02:00
Vladimir Enchev
a390658ad9 DynamicLinqCustomTypeProvider RestrictOrderByToPropertyOrField set to false 2025-01-27 18:20:03 +02:00
Vladimir Enchev
ecc403cbbc Dynamic LINQ settings improved 2025-01-27 15:59:15 +02:00
Paul Ruston
e870ca856c Add DaySelect event to Day, Week and Month Views (#1910)
* Add DaySelect event to Day, Week and Month Views

* Resolution of comments

* Resolve incorrect code example. Also anomolies in Month example

* Changed AppointmentsInSlot call as per comment
2025-01-27 10:12:27 +02:00
547 changed files with 35382 additions and 5219 deletions

244
.editorconfig Normal file
View File

@@ -0,0 +1,244 @@
# Remove the line below if you want to inherit .editorconfig settings from higher directories
root = true
# C# files
[*.cs]
#### Core EditorConfig Options ####
# Indentation and spacing
indent_size = 4
indent_style = space
tab_width = 4
# New line preferences
end_of_line = crlf
insert_final_newline = false
#### .NET Code Actions ####
# Type members
dotnet_hide_advanced_members = false
dotnet_member_insertion_location = with_other_members_of_the_same_kind
dotnet_property_generation_behavior = prefer_throwing_properties
# Symbol search
dotnet_search_reference_assemblies = true
#### .NET Coding Conventions ####
# Organize usings
dotnet_separate_import_directive_groups = false
dotnet_sort_system_directives_first = false
file_header_template = unset
# this. and Me. preferences
dotnet_style_qualification_for_event = false
dotnet_style_qualification_for_field = false
dotnet_style_qualification_for_method = false
dotnet_style_qualification_for_property = false
# Language keywords vs BCL types preferences
dotnet_style_predefined_type_for_locals_parameters_members = true
dotnet_style_predefined_type_for_member_access = true
# Parentheses preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_operators = never_if_unnecessary
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members
# Expression-level preferences
dotnet_prefer_system_hash_code = true
dotnet_style_coalesce_expression = true
dotnet_style_collection_initializer = true
dotnet_style_explicit_tuple_names = true
dotnet_style_namespace_match_folder = true
dotnet_style_null_propagation = true
dotnet_style_object_initializer = true
dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_prefer_auto_properties = true
dotnet_style_prefer_collection_expression = when_types_loosely_match
dotnet_style_prefer_compound_assignment = true
dotnet_style_prefer_conditional_expression_over_assignment = true
dotnet_style_prefer_conditional_expression_over_return = true
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
dotnet_style_prefer_inferred_anonymous_type_member_names = true
dotnet_style_prefer_inferred_tuple_names = true
dotnet_style_prefer_is_null_check_over_reference_equality_method = true
dotnet_style_prefer_simplified_boolean_expressions = true
dotnet_style_prefer_simplified_interpolation = true
# Field preferences
dotnet_style_readonly_field = true
# Parameter preferences
dotnet_code_quality_unused_parameters = all
# Suppression preferences
dotnet_remove_unnecessary_suppression_exclusions = none
# New line preferences
dotnet_style_allow_multiple_blank_lines_experimental = true
dotnet_style_allow_statement_immediately_after_block_experimental = true
#### C# Coding Conventions ####
# var preferences
csharp_style_var_elsewhere = false
csharp_style_var_for_built_in_types = false
csharp_style_var_when_type_is_apparent = false
# Expression-bodied members
csharp_style_expression_bodied_accessors = true
csharp_style_expression_bodied_constructors = false
csharp_style_expression_bodied_indexers = true
csharp_style_expression_bodied_lambdas = true
csharp_style_expression_bodied_local_functions = false
csharp_style_expression_bodied_methods = false
csharp_style_expression_bodied_operators = false
csharp_style_expression_bodied_properties = true
# Pattern matching preferences
csharp_style_pattern_matching_over_as_with_null_check = true
csharp_style_pattern_matching_over_is_with_cast_check = true
csharp_style_prefer_extended_property_pattern = true
csharp_style_prefer_not_pattern = true
csharp_style_prefer_pattern_matching = true
csharp_style_prefer_switch_expression = true
# Null-checking preferences
csharp_style_conditional_delegate_call = true
# Modifier preferences
csharp_prefer_static_anonymous_function = true
csharp_prefer_static_local_function = true
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async
csharp_style_prefer_readonly_struct = true
csharp_style_prefer_readonly_struct_member = true
# Code-block preferences
csharp_prefer_braces = true
csharp_prefer_simple_using_statement = true
csharp_prefer_system_threading_lock = true
csharp_style_namespace_declarations = block_scoped
csharp_style_prefer_method_group_conversion = true
csharp_style_prefer_primary_constructors = true
csharp_style_prefer_top_level_statements = true
# Expression-level preferences
csharp_prefer_simple_default_expression = true
csharp_style_deconstructed_variable_declaration = true
csharp_style_implicit_object_creation_when_type_is_apparent = true
csharp_style_inlined_variable_declaration = true
csharp_style_prefer_index_operator = true
csharp_style_prefer_local_over_anonymous_function = true
csharp_style_prefer_null_check_over_type_check = true
csharp_style_prefer_range_operator = true
csharp_style_prefer_tuple_swap = true
csharp_style_prefer_utf8_string_literals = true
csharp_style_throw_expression = true
csharp_style_unused_value_assignment_preference = discard_variable
csharp_style_unused_value_expression_statement_preference = discard_variable
# 'using' directive preferences
csharp_using_directive_placement = outside_namespace
# New line preferences
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true
csharp_style_allow_embedded_statements_on_same_line_experimental = true
#### C# Formatting Rules ####
# New line preferences
csharp_new_line_before_catch = true
csharp_new_line_before_else = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_open_brace = all
csharp_new_line_between_query_expression_clauses = true
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_labels = no_change
csharp_indent_switch_labels = true
# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# Wrapping preferences
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
#### Naming styles ####
# Naming rules
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
# Symbol specifications
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
# Naming styles
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case

6
.gitignore vendored
View File

@@ -341,3 +341,9 @@ Radzen.DocFX/_exported_templates
Radzen.DocFX/api/*.yml
!Radzen.DocFX/api/index.md
Radzen.DocFX/api/.manifest
Radzen.Blazor.min.js
/.claude
/SANKEY_PATTERN_COMPARISON.md
*.md
/.gitignore
/.gitignore

View File

@@ -1,31 +1,24 @@
# syntax=docker/dockerfile:1
FROM mono:latest
ENV DOCFX_VER 2.58.4
RUN apt-get update && apt-get install unzip wget git -y && wget -q -P /tmp https://github.com/dotnet/docfx/releases/download/v${DOCFX_VER}/docfx.zip && \
mkdir -p /opt/docfx && \
unzip /tmp/docfx.zip -d /opt/docfx && \
echo '#!/bin/bash\nmono /opt/docfx/docfx.exe $@' > /usr/bin/docfx && \
chmod +x /usr/bin/docfx && \
rm -rf /tmp/*
COPY Radzen.Blazor /app/Radzen.Blazor
COPY Radzen.DocFX /app/DocFX
COPY RadzenBlazorDemos /app/RadzenBlazorDemos
COPY RadzenBlazorDemos.Host /app/RadzenBlazorDemos.Host
WORKDIR /app
RUN docfx DocFX/docfx.json
FROM mcr.microsoft.com/dotnet/sdk:9.0
COPY --from=0 /app/RadzenBlazorDemos.Host /app/RadzenBlazorDemos.Host
COPY --from=0 /app/RadzenBlazorDemos /app/RadzenBlazorDemos
COPY Radzen.Blazor /app/Radzen.Blazor
COPY Radzen.DocFX /app/Radzen.DocFX
COPY RadzenBlazorDemos /app/RadzenBlazorDemos
COPY RadzenBlazorDemos.Host /app/RadzenBlazorDemos.Host
WORKDIR /app
RUN dotnet tool install -g docfx
ENV PATH="$PATH:/root/.dotnet/tools"
RUN wget https://dot.net/v1/dotnet-install.sh \
&& bash dotnet-install.sh --channel 8.0 --runtime dotnet --install-dir /usr/share/dotnet
RUN dotnet build -c Release Radzen.Blazor/Radzen.Blazor.csproj -f net8.0
RUN docfx Radzen.DocFX/docfx.json
WORKDIR /app/RadzenBlazorDemos.Host
RUN dotnet publish -c Release -o out
ENV ASPNETCORE_URLS http://*:5000
ENV ASPNETCORE_URLS=http://*:5000
WORKDIR /app/RadzenBlazorDemos.Host/out
ENTRYPOINT ["dotnet", "RadzenBlazorDemos.Host.dll"]

View File

@@ -0,0 +1,171 @@
using Bunit;
using Microsoft.Extensions.DependencyInjection;
using Radzen;
using System;
using System.Net.Http;
using Microsoft.Extensions.Options;
using Xunit;
namespace Radzen.Blazor.Tests
{
public class AIChatTests
{
private void RegisterChatService(TestContext ctx)
{
// Register a dummy HttpClient and default options for AIChatService
ctx.Services.AddSingleton(new HttpClient());
ctx.Services.AddScoped<IAIChatService, AIChatService>();
}
[Fact]
public void RadzenAIChat_ShouldRenderWithDefaultProperties()
{
using var ctx = new TestContext();
RegisterChatService(ctx);
var component = ctx.RenderComponent<RadzenAIChat>();
Assert.Contains("Type your message...", component.Markup);
}
[Fact]
public void RadzenAIChat_ShouldRenderWithCustomTitle()
{
using var ctx = new TestContext();
RegisterChatService(ctx);
var component = ctx.RenderComponent<RadzenAIChat>(parameters => parameters
.Add(p => p.Title, "Custom Chat"));
Assert.Contains("Custom Chat", component.Markup);
}
[Fact]
public void RadzenAIChat_ShouldRenderWithCustomPlaceholder()
{
using var ctx = new TestContext();
RegisterChatService(ctx);
var component = ctx.RenderComponent<RadzenAIChat>(parameters => parameters
.Add(p => p.Placeholder, "Enter your message here..."));
Assert.Contains("Enter your message here...", component.Markup);
}
[Fact]
public void RadzenAIChat_ShouldRenderWithCustomEmptyMessage()
{
using var ctx = new TestContext();
RegisterChatService(ctx);
var component = ctx.RenderComponent<RadzenAIChat>(parameters => parameters
.Add(p => p.EmptyMessage, "No messages yet"));
Assert.Contains("No messages yet", component.Markup);
}
[Fact]
public void RadzenAIChat_ShouldShowClearButtonByDefault()
{
using var ctx = new TestContext();
RegisterChatService(ctx);
var component = ctx.RenderComponent<RadzenAIChat>();
Assert.Contains("rz-chat-header-clear", component.Markup);
}
[Fact]
public void RadzenAIChat_ShouldHideClearButtonWhenShowClearButtonIsFalse()
{
using var ctx = new TestContext();
RegisterChatService(ctx);
var component = ctx.RenderComponent<RadzenAIChat>(parameters => parameters
.Add(p => p.ShowClearButton, false));
Assert.DoesNotContain("clear_all", component.Markup);
}
[Fact]
public void RadzenAIChat_ShouldBeDisabledWhenDisabledIsTrue()
{
using var ctx = new TestContext();
RegisterChatService(ctx);
var component = ctx.RenderComponent<RadzenAIChat>(parameters => parameters
.Add(p => p.Disabled, true));
Assert.Contains("disabled", component.Markup);
}
[Fact]
public void RadzenAIChat_ShouldBeReadOnlyWhenReadOnlyIsTrue()
{
using var ctx = new TestContext();
RegisterChatService(ctx);
var component = ctx.RenderComponent<RadzenAIChat>(parameters => parameters
.Add(p => p.ReadOnly, true));
Assert.Contains("readonly", component.Markup);
}
[Fact]
public void RadzenAIChat_ShouldHaveCorrectCssClass()
{
using var ctx = new TestContext();
RegisterChatService(ctx);
var component = ctx.RenderComponent<RadzenAIChat>();
Assert.Contains("rz-chat", component.Markup);
}
[Fact]
public void ChatMessage_ShouldHaveCorrectProperties()
{
// Arrange
var message = new ChatMessage
{
Content = "Test message",
IsUser = true,
Timestamp = DateTime.Now
};
// Assert
Assert.NotEmpty(message.Id);
Assert.Equal("Test message", message.Content);
Assert.True(message.IsUser);
Assert.False(message.IsStreaming);
}
[Fact]
public void RadzenAIChat_AddMessage_ShouldAddMessageToList()
{
using var ctx = new TestContext();
RegisterChatService(ctx);
var component = ctx.RenderComponent<RadzenAIChat>();
// Act
component.Instance.AddMessage("Test message", true);
// Assert
var messages = component.Instance.GetMessages();
Assert.Single(messages);
Assert.Equal("Test message", messages[0].Content);
Assert.True(messages[0].IsUser);
}
[Fact]
public void RadzenAIChat_ClearChat_ShouldRemoveAllMessages()
{
using var ctx = new TestContext();
RegisterChatService(ctx);
var component = ctx.RenderComponent<RadzenAIChat>();
component.Instance.AddMessage("Test message 1", true);
component.Instance.AddMessage("Test message 2", false);
// Act
component.InvokeAsync(async () => await component.Instance.ClearChat()).Wait();
// Assert
Assert.Empty(component.Instance.GetMessages());
}
[Fact]
public void RadzenAIChat_ShouldLimitMessagesToMaxMessages()
{
using var ctx = new TestContext();
RegisterChatService(ctx);
var component = ctx.RenderComponent<RadzenAIChat>(parameters => parameters.Add(p => p.MaxMessages, 3));
component.Instance.AddMessage("Message 1", true);
component.Instance.AddMessage("Message 2", false);
component.Instance.AddMessage("Message 3", true);
component.Instance.AddMessage("Message 4", false);
// Assert
var messages = component.Instance.GetMessages();
Assert.Equal(3, messages.Count);
Assert.Equal("Message 2", messages[0].Content);
Assert.Equal("Message 3", messages[1].Content);
Assert.Equal("Message 4", messages[2].Content);
}
}
}

View File

@@ -5,8 +5,12 @@ using Microsoft.AspNetCore.Components.Rendering;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;
using static System.Reflection.Metadata.BlobBuilder;
namespace Radzen.Blazor.Tests
{
@@ -838,5 +842,152 @@ namespace Radzen.Blazor.Tests
Assert.True(newArgs.Skip == 20);
Assert.True(newArgs.Top == 20);
}
// Filter tests
/// <summary>
/// Utility class for testing.
/// </summary>
/// <remarks>
/// Tests that involves filtering on <see cref="RadzenDataGrid{TItem}"/> requires the generic parameter to be a specific type.
/// They do not work using <c>object</c> or <c>dynamic</c>.
/// </remarks>
/// <param name="Name"></param>
/// <param name="Roles"></param>
private sealed record User(string Name, IEnumerable<Role> Roles);
/// <summary>
/// Utility class for testing.
/// </summary>
/// <remarks>
/// Tests that involves filtering on <see cref="RadzenDataGrid{TItem}"/> requires the generic parameter to be a specific type.
/// They do not work using <c>object</c> or <c>dynamic</c>.
/// </remarks>
/// <param name="Id"></param>
/// <param name="Description"></param>
private sealed record Role(int Id, string Description);
[Fact]
public async Task DataGrid_FilterBySubProperties_ReturnsDataFiltered()
{
// Arrange
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
Role admin = new(0, "Admin");
Role guest = new(1, "Guest");
User moe = new("Moe", [admin]);
User tom = new("Tom", [admin, guest]);
User sam = new("Sam", [guest]);
User[] data = [moe, tom, sam];
var component = ctx.RenderComponent<RadzenDataGrid<User>>(parameters =>
{
parameters.Add(p => p.Data, data);
parameters.Add(p => p.AllowFiltering, true);
parameters.Add(p => p.FilterMode, FilterMode.CheckBoxList);
parameters.Add(p => p.Columns, builder =>
{
builder.OpenComponent(0, typeof(RadzenDataGridColumn<User>));
builder.AddAttribute(1, "Property", "Name");
builder.AddAttribute(2, "Title", "User");
builder.CloseComponent();
builder.OpenComponent(0, typeof(RadzenDataGridColumn<User>));
builder.AddAttribute(1, "Property", "Roles");
builder.AddAttribute(2, "FilterProperty", "Id");
builder.AddAttribute(3, "Type", typeof(IEnumerable<Role>));
builder.AddAttribute(4, "Title", "Roles");
builder.CloseComponent();
});
});
// Act
await component.InvokeAsync(() => component
.Instance
.ColumnsCollection
.First(c => c.Property == "Roles")
.SetFilterValueAsync(new[] { 1 })
);
component.Render();
var filteredData = await component.InvokeAsync(component.Instance.View.ToArray);
// Assert
Assert.DoesNotContain(moe, filteredData);
Assert.Contains(sam, filteredData);
Assert.Contains(tom, filteredData);
}
[Fact]
public async Task DataGrid_LoadFilterSettingsFromJson_ReturnsDataFiltered()
{
// Arrange
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
Role admin = new(0, "Admin");
Role guest = new(1, "Guest");
User moe = new("Moe", [admin]);
User tom = new("Tom", [admin, guest]);
User sam = new("Sam", [guest]);
User[] data = [moe, tom, sam];
string settings = string.Empty;
var component = ctx.RenderComponent<RadzenDataGrid<User>>(parameters =>
{
parameters.Add(p => p.Data, data);
parameters.Add(p => p.AllowFiltering, true);
parameters.Add(p => p.FilterMode, FilterMode.CheckBoxList);
parameters.Add(p => p.LoadSettings, OnLoadSettings);
parameters.Add(p => p.SettingsChanged, OnSettingsChanged);
parameters.Add(p => p.Columns, builder =>
{
builder.OpenComponent(0, typeof(RadzenDataGridColumn<User>));
builder.AddAttribute(1, "Property", "Name");
builder.AddAttribute(2, "Title", "User");
builder.CloseComponent();
builder.OpenComponent(0, typeof(RadzenDataGridColumn<User>));
builder.AddAttribute(1, "Property", "Roles");
builder.AddAttribute(2, "FilterProperty", "Id");
builder.AddAttribute(3, "Type", typeof(IEnumerable<Role>));
builder.AddAttribute(4, "Title", "Roles");
builder.CloseComponent();
});
});
void OnSettingsChanged(DataGridSettings args)
{
settings = JsonSerializer.Serialize(args);
}
void OnLoadSettings(DataGridLoadSettingsEventArgs args)
{
if (string.IsNullOrEmpty(settings)) return;
args.Settings = JsonSerializer.Deserialize<DataGridSettings>(settings);
}
// Act
await component.InvokeAsync(() => component
.Instance
.ColumnsCollection
.First(c => c.Property == "Roles")
.SetFilterValueAsync(new[] { 1 })
);
component.Render();
var filteredData = await component.InvokeAsync(component.Instance.View.ToArray);
// Assert
Assert.DoesNotContain(moe, filteredData);
Assert.Contains(sam, filteredData);
Assert.Contains(tom, filteredData);
}
}
}

View File

@@ -174,6 +174,34 @@ namespace Radzen.Blazor.Tests
Assert.Contains(@$"tabindex=""{value}""", component.Markup);
}
[Fact]
public void DatePicker_Renders_EmptyCssClass_WhenValueIsEmpty()
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
var component = ctx.RenderComponent<RadzenDatePicker<DateTime>>();
component.SetParametersAndRender(parameters => parameters.Add(p => p.Value, null));
Assert.Contains(@$"rz-state-empty", component.Markup);
}
[Fact]
public void DatePicker_DoesNotRender_EmptyCssClass_WhenValueIsNotEmpty()
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
var component = ctx.RenderComponent<RadzenDatePicker<DateTime>>();
component.SetParametersAndRender(parameters => parameters.Add(p => p.Value, DateTime.Now));
Assert.DoesNotContain(@$"rz-state-empty", component.Markup);
}
[Fact]
public void DatePicker_Renders_DisabledParameter()
{

View File

@@ -0,0 +1,341 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace Radzen.Blazor.Tests
{
public class DialogServiceTests
{
public class OpenDialogTests
{
[Fact(DisplayName = "DialogOptions default values are set correctly")]
public void DialogOptions_DefaultValues_AreSetCorrectly()
{
// Arrange
var options = new DialogOptions();
var dialogService = new DialogService(null, null);
// Act
dialogService.OpenDialog<DialogServiceTests>("Test", [], options);
// Assert
Assert.Equal("600px", options.Width);
Assert.Equal("", options.Left);
Assert.Equal("", options.Top);
Assert.Equal("", options.Bottom);
Assert.Equal("", options.Height);
Assert.Equal("", options.Style);
Assert.Equal("", options.CssClass);
Assert.Equal("", options.WrapperCssClass);
Assert.Equal("", options.ContentCssClass);
}
[Fact(DisplayName = "DialogOptions values are retained after OpenDialog call")]
public void DialogOptions_Values_AreRetained_AfterOpenDialogCall()
{
// Arrange
var options = new DialogOptions
{
Width = "800px",
Left = "10px",
Top = "20px",
Bottom = "30px",
Height = "400px",
Style = "background-color: red;",
CssClass = "custom-class",
WrapperCssClass = "wrapper-class",
ContentCssClass = "content-class"
};
var dialogService = new DialogService(null, null);
// Act
dialogService.OpenDialog<DialogServiceTests>("Test", [], options);
// Assert
Assert.Equal("800px", options.Width);
Assert.Equal("10px", options.Left);
Assert.Equal("20px", options.Top);
Assert.Equal("30px", options.Bottom);
Assert.Equal("400px", options.Height);
Assert.Equal("background-color: red;", options.Style);
Assert.Equal("custom-class", options.CssClass);
Assert.Equal("wrapper-class", options.WrapperCssClass);
Assert.Equal("content-class", options.ContentCssClass);
}
[Fact(DisplayName = "DialogOptions is null and default values are set correctly")]
public void DialogOptions_IsNull_DefaultValues_AreSetCorrectly()
{
// Arrange
DialogOptions resultingOptions = null;
var dialogService = new DialogService(null, null);
dialogService.OnOpen += (title, type, parameters, options) => resultingOptions = options;
// Act
dialogService.OpenDialog<DialogServiceTests>("Test", [], null);
// Assert
Assert.NotNull(resultingOptions);
Assert.Equal("600px", resultingOptions.Width);
Assert.Equal("", resultingOptions.Left);
Assert.Equal("", resultingOptions.Top);
Assert.Equal("", resultingOptions.Bottom);
Assert.Equal("", resultingOptions.Height);
Assert.Equal("", resultingOptions.Style);
Assert.Equal("", resultingOptions.CssClass);
Assert.Equal("", resultingOptions.WrapperCssClass);
Assert.Equal("", resultingOptions.ContentCssClass);
}
[Fact(DisplayName = "Open with dynamic component type reflective calls are resolved without exception")]
public void Open_DynamicComponentType_Reflective_Calls_Resolve()
{
// Arrange
string resultingTitle = null;
Type resultingType = null;
var dialogService = new DialogService(null, null);
dialogService.OnOpen += (title, type, _, _) =>
{
resultingTitle = title;
resultingType = type;
};
dialogService.Open("Dynamic Open", typeof(RadzenButton), []);
// Assert
Assert.Equal("Dynamic Open", resultingTitle);
Assert.Equal(typeof(RadzenButton), resultingType);
}
[Fact(DisplayName = "OpenAsync with dynamic component type reflective calls are resolved without exception")]
public async Task OpenAsync_DynamicComponentType_Reflective_Calls_Resolve()
{
// Arrange
string resultingTitle = null;
Type resultingType = null;
var dialogService = new DialogService(null, null);
dialogService.OnOpen += (title, type, _, _) =>
{
resultingTitle = title;
resultingType = type;
};
var openTask = dialogService.OpenAsync("Dynamic Open", typeof(RadzenButton), []);
dialogService.Close();
await openTask;
// Assert
Assert.Equal("Dynamic Open", resultingTitle);
Assert.Equal(typeof(RadzenButton), resultingType);
}
}
public class ConfirmTests
{
[Fact(DisplayName = "ConfirmOptions is null and default values are set correctly")]
public async Task ConfirmOptions_IsNull_AreSetCorrectly()
{
// Arrange
var dialogService = new DialogService(null, null);
ConfirmOptions resultingOptions = null;
dialogService.OnOpen += (title, type, parameters, options) => resultingOptions = options as ConfirmOptions;
using var cancellationTokenSource = new CancellationTokenSource();
cancellationTokenSource.Cancel();
// Act
try
{
await dialogService.Confirm(cancellationToken: cancellationTokenSource.Token);
}
catch (TaskCanceledException)
{
// this is expected
}
// Assert
Assert.NotNull(resultingOptions);
Assert.Equal("Ok", resultingOptions.OkButtonText);
Assert.Equal("Cancel", resultingOptions.CancelButtonText);
Assert.Equal("600px", resultingOptions.Width);
Assert.Equal("", resultingOptions.Style);
Assert.Equal("rz-dialog-confirm", resultingOptions.CssClass);
Assert.Equal("rz-dialog-wrapper", resultingOptions.WrapperCssClass);
}
[Fact(DisplayName = "ConfirmOptions default values are set correctly")]
public async Task ConfirmOptions_DefaultValues_AreSetCorrectly()
{
// Arrange
var dialogService = new DialogService(null, null);
ConfirmOptions resultingOptions = null;
dialogService.OnOpen += (title, type, parameters, options) => resultingOptions = options as ConfirmOptions;
using var cancellationTokenSource = new CancellationTokenSource();
cancellationTokenSource.Cancel();
// Act
try
{
await dialogService.Confirm(options: new(), cancellationToken: cancellationTokenSource.Token);
}
catch (TaskCanceledException)
{
// this is expected
}
// Assert
Assert.NotNull(resultingOptions);
Assert.Equal("Ok", resultingOptions.OkButtonText);
Assert.Equal("Cancel", resultingOptions.CancelButtonText);
Assert.Equal("600px", resultingOptions.Width);
Assert.Equal("", resultingOptions.Style);
Assert.Equal("rz-dialog-confirm", resultingOptions.CssClass);
Assert.Equal("rz-dialog-wrapper", resultingOptions.WrapperCssClass);
}
[Fact(DisplayName = "ConfirmOptions values are retained after Confirm call")]
public async Task Confirm_ProvidedValues_AreRetained()
{
// Arrange
var dialogService = new DialogService(null, null);
var options = new ConfirmOptions
{
OkButtonText = "XXX",
CancelButtonText = "YYY",
Width = "800px",
Style = "background-color: red;",
CssClass = "custom-class",
WrapperCssClass = "wrapper-class"
};
ConfirmOptions resultingOptions = null;
dialogService.OnOpen += (title, type, parameters, options) => resultingOptions = options as ConfirmOptions;
// We break out of the dialog immediately, but the options should still be set
using var cancellationTokenSource = new CancellationTokenSource();
cancellationTokenSource.Cancel();
// Act
try
{
await dialogService.Confirm("Confirm?", "Confirm", options, cancellationToken: cancellationTokenSource.Token);
}
catch (TaskCanceledException)
{
// this is expected
}
// Assert
Assert.NotNull(resultingOptions);
Assert.Equal("XXX", resultingOptions.OkButtonText);
Assert.Equal("YYY", resultingOptions.CancelButtonText);
Assert.Equal("800px", resultingOptions.Width);
Assert.Equal("background-color: red;", resultingOptions.Style);
Assert.Equal("rz-dialog-confirm custom-class", resultingOptions.CssClass);
Assert.Equal("rz-dialog-wrapper wrapper-class", resultingOptions.WrapperCssClass);
}
}
public class AlertTests
{
[Fact(DisplayName = "AlertOptions is null and default values are set correctly")]
public async Task AlertOptions_IsNull_AreSetCorrectly()
{
// Arrange
var dialogService = new DialogService(null, null);
AlertOptions resultingOptions = null;
dialogService.OnOpen += (title, type, parameters, options) => resultingOptions = options as AlertOptions;
using var cancellationTokenSource = new CancellationTokenSource();
cancellationTokenSource.Cancel();
// Act
try
{
await dialogService.Alert(cancellationToken: cancellationTokenSource.Token);
}
catch (TaskCanceledException)
{
// this is expected
}
// Assert
Assert.NotNull(resultingOptions);
Assert.Equal("Ok", resultingOptions.OkButtonText);
Assert.Equal("600px", resultingOptions.Width);
Assert.Equal("", resultingOptions.Style);
Assert.Equal("rz-dialog-alert", resultingOptions.CssClass);
Assert.Equal("rz-dialog-wrapper", resultingOptions.WrapperCssClass);
}
[Fact(DisplayName = "AlertOptions default values are set correctly")]
public async Task AlertOptions_DefaultValues_AreSetCorrectly()
{
// Arrange
var dialogService = new DialogService(null, null);
AlertOptions resultingOptions = null;
dialogService.OnOpen += (title, type, parameters, options) => resultingOptions = options as AlertOptions;
using var cancellationTokenSource = new CancellationTokenSource();
cancellationTokenSource.Cancel();
// Act
try
{
await dialogService.Alert(options: new(), cancellationToken: cancellationTokenSource.Token);
}
catch (TaskCanceledException)
{
// this is expected
}
// Assert
Assert.NotNull(resultingOptions);
Assert.Equal("Ok", resultingOptions.OkButtonText);
Assert.Equal("600px", resultingOptions.Width);
Assert.Equal("", resultingOptions.Style);
Assert.Equal("rz-dialog-alert", resultingOptions.CssClass);
Assert.Equal("rz-dialog-wrapper", resultingOptions.WrapperCssClass);
}
[Fact(DisplayName = "AlertOptions values are retained after Alert call")]
public async Task Alert_ProvidedValues_AreRetained()
{
// Arrange
var dialogService = new DialogService(null, null);
var options = new AlertOptions
{
OkButtonText = "XXX",
Width = "800px",
Style = "background-color: red;",
CssClass = "custom-class",
WrapperCssClass = "wrapper-class"
};
AlertOptions resultingOptions = null;
dialogService.OnOpen += (title, type, parameters, options) => resultingOptions = options as AlertOptions;
// We break out of the dialog immediately, but the options should still be set
using var cancellationTokenSource = new CancellationTokenSource();
cancellationTokenSource.Cancel();
// Act
try
{
await dialogService.Alert("Alert?", "Alert", options, cancellationToken: cancellationTokenSource.Token);
}
catch (TaskCanceledException)
{
// this is expected
}
// Assert
Assert.NotNull(resultingOptions);
Assert.Equal("XXX", resultingOptions.OkButtonText);
Assert.Equal("800px", resultingOptions.Width);
Assert.Equal("background-color: red;", resultingOptions.Style);
Assert.Equal("rz-dialog-alert custom-class", resultingOptions.CssClass);
Assert.Equal("rz-dialog-wrapper wrapper-class", resultingOptions.WrapperCssClass);
}
}
}
}

View File

@@ -43,7 +43,7 @@ public class DollarsTypeConverter : TypeConverter
return new Dollars(d);
if (value is string s)
return decimal.TryParse(s, out var val) ? new Dollars(val) : null;
return decimal.TryParse(s, culture, out var val) ? new Dollars(val) : null;
return base.ConvertFrom(context, culture, value);
}

View File

@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using AngleSharp.Dom;
using Bunit;
using Microsoft.AspNetCore.Components;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
namespace Radzen.Blazor.Tests
@@ -14,18 +16,20 @@ namespace Radzen.Blazor.Tests
{
public string Text { get; set; }
public int Id { get; set; }
public bool Disabled { get; set; }
}
private static IRenderedComponent<RadzenDropDown<T>> DropDown<T>(TestContext ctx, Action<ComponentParameterCollectionBuilder<RadzenDropDown<T>>> configure = null)
{
var data = new [] {
var data = new[] {
new DataItem { Text = "Item 1", Id = 1 },
new DataItem { Text = "Item 2", Id = 2 },
};
var component = ctx.RenderComponent<RadzenDropDown<T>>();
component.SetParametersAndRender(parameters => {
component.SetParametersAndRender(parameters =>
{
parameters.Add(p => p.Data, data);
parameters.Add(p => p.TextProperty, nameof(DataItem.Text));
@@ -42,6 +46,7 @@ namespace Radzen.Blazor.Tests
return component;
}
[Fact]
public async Task Dropdown_SelectItem_Method_Should_Not_Throw()
{
@@ -100,7 +105,8 @@ namespace Radzen.Blazor.Tests
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
var component = DropDown<string>(ctx, parameters => {
var component = DropDown<string>(ctx, parameters =>
{
parameters.Add(p => p.ValueProperty, nameof(DataItem.Text));
});
@@ -123,7 +129,8 @@ namespace Radzen.Blazor.Tests
List<DataItem> boundCollection = [new() { Text = "Item 2" }];
var component = DropDown<string>(ctx, parameters => {
var component = DropDown<List<DataItem>>(ctx, parameters =>
{
parameters.Add(p => p.ItemComparer, new DataItemComparer());
parameters.Add(p => p.Multiple, true);
parameters.Add(p => p.Value, boundCollection);
@@ -150,7 +157,8 @@ namespace Radzen.Blazor.Tests
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
var component = DropDown<string>(ctx, parameters => {
var component = DropDown<string>(ctx, parameters =>
{
parameters.Add(p => p.ValueProperty, nameof(DataItem.Text));
parameters.Add(p => p.Multiple, true);
});
@@ -275,6 +283,365 @@ namespace Radzen.Blazor.Tests
Assert.Collection(selectedItems, item => Assert.Contains("value: Item 1", item.Text()), item => Assert.Contains("value: Item 2", item.Text()));
}
[Theory]
[InlineData(false, true, false, true, "false")]
[InlineData(true, false, true, false, "true")]
[InlineData(true, false, false, false, "false")]
[InlineData(true, false, false, true, "true")]
[InlineData(false, false, false, true, "false")]
public void DropDown_AllSelectedFalseIfListIsAllDisabled(bool item1Selected, bool item1Disabled, bool item2Selected, bool item2Disabled, string expectedAriaCheckedValue)
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
var data = new[] {
new DataItem { Text = "Item 1", Id = 1, Disabled = item1Disabled },
new DataItem { Text = "Item 2", Id = 2, Disabled = item2Disabled },
};
List<int> selectedValues = [];
if (item1Selected)
{
selectedValues.Add(data[0].Id);
}
if (item2Selected)
{
selectedValues.Add(data[1].Id);
}
var component = ctx.RenderComponent<RadzenDropDown<List<int>>>(parameters => parameters
.Add(p => p.Data, data)
.Add(p => p.Value, selectedValues)
.Add(p => p.Multiple, true)
.Add(p => p.AllowSelectAll, true)
.Add(p => p.TextProperty, nameof(DataItem.Text))
.Add(p => p.DisabledProperty, nameof(DataItem.Disabled))
.Add(p => p.ValueProperty, nameof(DataItem.Id)));
Assert.NotNull(component);
var highlightedItems = component.FindAll(".rz-state-highlight");
Assert.Equal(selectedValues.Count, highlightedItems.Count);
var selectAllCheckBox = component.Find(".rz-multiselect-header input[type='checkbox']");
Assert.Equal(expectedAriaCheckedValue, selectAllCheckBox.GetAttribute("aria-checked"));
}
[Fact]
public void DropDown_ReferenceGenericCollectionAssignment_HashSet_ReferencesInstance()
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
var originalHashSet = new HashSet<int>();
var capturedValue = (HashSet<int>)null;
var component = DropDownWithReferenceCollection<HashSet<int>>(ctx, parameters =>
{
parameters.Add(p => p.Multiple, true);
parameters.Add(p => p.Value, originalHashSet);
parameters.Add(p => p.ValueChanged, EventCallback.Factory.Create<HashSet<int>>(this, value => capturedValue = value));
parameters.Add(p => p.ValueProperty, nameof(DataItem.Id));
});
var items = component.FindAll(".rz-multiselect-item");
// Select first item
items[0].Click();
component.Render();
// Verify the same HashSet instance is Referenced
Assert.Same(originalHashSet, capturedValue);
// Verify the item was added correctly
Assert.Single(originalHashSet);
Assert.Contains(1, originalHashSet);
}
[Fact]
public void DropDown_ReferenceGenericCollectionAssignment_HashSet_MultipleSelections()
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
var originalHashSet = new HashSet<int> { 2 }; // Pre-populate with Item 2
var capturedValues = new List<HashSet<int>>();
var component = DropDownWithReferenceCollection<HashSet<int>>(ctx, parameters =>
{
parameters.Add(p => p.Multiple, true);
parameters.Add(p => p.Value, originalHashSet);
parameters.Add(p => p.ValueChanged, EventCallback.Factory.Create<HashSet<int>>(this, value => capturedValues.Add(value)));
parameters.Add(p => p.ValueProperty, nameof(DataItem.Id));
});
var items = component.FindAll(".rz-multiselect-item");
// Select first item (should add to existing collection)
items[0].Click();
component.Render();
// Verify the same HashSet instance is Referenced
Assert.Single(capturedValues);
Assert.Same(originalHashSet, capturedValues[0]);
// Verify both items are now in the collection
Assert.Equal(2, originalHashSet.Count);
Assert.Contains(1, originalHashSet);
Assert.Contains(2, originalHashSet);
// Deselect second item (should remove from collection)
items = component.FindAll(".rz-multiselect-item"); // Re-find items after render
items[1].Click();
component.Render();
// Verify the same HashSet instance is still Referenced
Assert.Equal(2, capturedValues.Count);
Assert.Same(originalHashSet, capturedValues[1]);
// Verify only first item remains
Assert.Single(originalHashSet);
Assert.Contains(1, originalHashSet);
Assert.DoesNotContain(2, originalHashSet);
}
[Fact]
public void DropDown_ReferenceGenericCollectionAssignment_SortedSet_ReferencesInstance()
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
var originalSortedSet = new SortedSet<int>();
var capturedValue = (SortedSet<int>)null;
var component = DropDownWithReferenceCollection<SortedSet<int>>(ctx, parameters =>
{
parameters.Add(p => p.Multiple, true);
parameters.Add(p => p.Value, originalSortedSet);
parameters.Add(p => p.ValueChanged, EventCallback.Factory.Create<SortedSet<int>>(this, value => capturedValue = value));
parameters.Add(p => p.ValueProperty, nameof(DataItem.Id));
});
var items = component.FindAll(".rz-multiselect-item");
// Select both items
items[0].Click();
component.Render();
items = component.FindAll(".rz-multiselect-item"); // Re-find items after first click
items[1].Click();
component.Render();
// Verify the same SortedSet instance is Referenced
Assert.Same(originalSortedSet, capturedValue);
// Verify items are sorted correctly
Assert.Equal(2, originalSortedSet.Count);
var sortedItems = originalSortedSet.ToList();
Assert.Equal(1, sortedItems[0]);
Assert.Equal(2, sortedItems[1]);
}
[Fact]
public void DropDown_ReferenceGenericCollectionAssignment_CustomCollection_ReferencesInstance()
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
var originalCollection = new CustomCollection<int>();
var capturedValue = (CustomCollection<int>)null;
var component = DropDownWithReferenceCollection<CustomCollection<int>>(ctx, parameters =>
{
parameters.Add(p => p.Multiple, true);
parameters.Add(p => p.Value, originalCollection);
parameters.Add(p => p.ValueChanged, EventCallback.Factory.Create<CustomCollection<int>>(this, value => capturedValue = value));
parameters.Add(p => p.ValueProperty, nameof(DataItem.Id));
});
var items = component.FindAll(".rz-multiselect-item");
// Select first item
items[0].Click();
component.Render();
// Verify the same custom collection instance is Referenced
Assert.Same(originalCollection, capturedValue);
// Verify the item was added correctly
Assert.Single(originalCollection);
Assert.Contains(1, originalCollection);
}
[Fact]
public void DropDown_ReferenceGenericCollectionAssignment_List_ReferencesInstance()
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
var originalList = new List<int>();
var capturedValue = (List<int>)null;
var component = DropDownWithReferenceCollection<List<int>>(ctx, parameters =>
{
parameters.Add(p => p.Multiple, true);
parameters.Add(p => p.Value, originalList);
parameters.Add(p => p.ValueChanged, EventCallback.Factory.Create<List<int>>(this, value => capturedValue = value));
parameters.Add(p => p.ValueProperty, nameof(DataItem.Id));
});
var items = component.FindAll(".rz-multiselect-item");
// Select first item
items[0].Click();
component.Render();
// For List<T>, it should now Reference the instance since we removed the IList exclusion
// Arrays are now excluded instead
Assert.Same(originalList, capturedValue);
// And the content should be correct
Assert.Single(capturedValue);
Assert.Contains(1, capturedValue);
}
[Fact]
public void DropDown_ReferenceGenericCollectionAssignment_DisabledByDefault()
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
var originalList = new List<int>();
var capturedValue = (List<int>)null;
var component = DropDown<List<int>>(ctx, parameters =>
{
parameters.Add(p => p.Multiple, true);
parameters.Add(p => p.Value, originalList);
parameters.Add(p => p.ValueChanged, EventCallback.Factory.Create<List<int>>(this, value => capturedValue = value));
parameters.Add(p => p.ValueProperty, nameof(DataItem.Id));
});
var items = component.FindAll(".rz-multiselect-item");
// Select first item
items[0].Click();
component.Render();
// When ReferenceCollectionOnSelection is false (default), a new instance should be created
Assert.NotSame(originalList, capturedValue);
// But the content should still be correct
Assert.Single(capturedValue);
Assert.Contains(1, capturedValue);
}
[Fact]
public void DropDown_Reset_PreservesCollectionInstanceButClears()
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
var originalHashSet = new HashSet<int> { 1, 2 }; // Pre-populate
var capturedValues = new List<HashSet<int>>();
var component = DropDownWithReferenceCollection<HashSet<int>>(ctx, parameters =>
{
parameters.Add(p => p.Multiple, true);
parameters.Add(p => p.Value, originalHashSet);
parameters.Add(p => p.ValueChanged, EventCallback.Factory.Create<HashSet<int>>(this, value => capturedValues.Add(value)));
parameters.Add(p => p.ValueProperty, nameof(DataItem.Id));
});
// Verify initial state - collection should have 2 items
Assert.Equal(2, originalHashSet.Count);
Assert.Contains(1, originalHashSet);
Assert.Contains(2, originalHashSet);
// Call Reset (public method that calls ClearAll internally)
component.InvokeAsync(() => component.Instance.Reset());
component.Render();
// Verify the same HashSet instance is preserved
Assert.Single(capturedValues);
Assert.Same(originalHashSet, capturedValues[0]);
// Verify the collection is now cleared
Assert.Empty(originalHashSet);
}
[Fact]
public void DropDown_SelectAll_PreservesCollectionInstanceAndPopulates()
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
var originalHashSet = new HashSet<int>(); // Start empty
var capturedValues = new List<HashSet<int>>();
var component = DropDownWithReferenceCollection<HashSet<int>>(ctx, parameters =>
{
parameters.Add(p => p.Multiple, true);
parameters.Add(p => p.AllowSelectAll, true);
parameters.Add(p => p.Value, originalHashSet);
parameters.Add(p => p.ValueChanged, EventCallback.Factory.Create<HashSet<int>>(this, value => capturedValues.Add(value)));
parameters.Add(p => p.ValueProperty, nameof(DataItem.Id));
});
// Verify initial state - collection should be empty
Assert.Empty(originalHashSet);
// Find and click the "Select All" checkbox
var selectAllCheckBox = component.Find(".rz-multiselect-header input[type='checkbox']");
selectAllCheckBox.Click();
component.Render();
// Verify the same HashSet instance is preserved
Assert.Single(capturedValues);
Assert.Same(originalHashSet, capturedValues[0]);
// Verify the collection now contains both items
Assert.Equal(2, originalHashSet.Count);
Assert.Contains(1, originalHashSet);
Assert.Contains(2, originalHashSet);
}
class ReferenceCollectionDropDown<T> : Radzen.Blazor.RadzenDropDown<T>
{
protected override void OnInitialized()
{
PreserveCollectionOnSelection = true;
base.OnInitialized();
}
}
private static IRenderedComponent<ReferenceCollectionDropDown<T>> DropDownWithReferenceCollection<T>(TestContext ctx, Action<ComponentParameterCollectionBuilder<ReferenceCollectionDropDown<T>>> configure = null)
{
var data = new[] {
new DataItem { Text = "Item 1", Id = 1 },
new DataItem { Text = "Item 2", Id = 2 },
};
var component = ctx.RenderComponent<ReferenceCollectionDropDown<T>>();
component.SetParametersAndRender(parameters =>
{
parameters.Add(p => p.Data, data);
parameters.Add(p => p.TextProperty, nameof(DataItem.Text));
if (configure != null)
{
configure.Invoke(parameters);
}
else
{
parameters.Add(p => p.ValueProperty, nameof(DataItem.Id));
}
});
return component;
}
class DataItemComparer : IEqualityComparer<DataItem>, IEqualityComparer<object>
{
@@ -303,5 +670,21 @@ namespace Radzen.Blazor.Tests
}
}
class CustomCollection<T> : ICollection<T>
{
private readonly List<T> _items = new();
public int Count => _items.Count;
public bool IsReadOnly => false;
public void Add(T item) => _items.Add(item);
public void Clear() => _items.Clear();
public bool Contains(T item) => _items.Contains(item);
public void CopyTo(T[] array, int arrayIndex) => _items.CopyTo(array, arrayIndex);
public bool Remove(T item) => _items.Remove(item);
public IEnumerator<T> GetEnumerator() => _items.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,323 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using Xunit;
namespace Radzen.Blazor.Tests
{
class TestEntity
{
public string Name { get; set; }
public int Age { get; set; }
public double Salary { get; set; }
public float Score { get; set; }
public decimal Balance { get; set; }
public short Level { get; set; }
public long Population { get; set; }
public Status AccountStatus { get; set; }
public DateTime CreatedAt { get; set; }
public DateTimeOffset LastUpdated { get; set; }
public Guid Id { get; set; }
public TimeOnly StartTime { get; set; }
public DateOnly BirthDate { get; set; }
public int[] Scores { get; set; }
public List<string> Tags { get; set; }
public List<TestEntity> Children { get; set; }
public Address Address { get; set; }
public double[] Salaries { get; set; }
public float[] Heights { get; set; }
public decimal[] Balances { get; set; }
public short[] Levels { get; set; }
public long[] Populations { get; set; }
public string[] Names { get; set; }
public Guid[] Ids { get; set; }
public DateTime[] CreatedDates { get; set; }
public DateTimeOffset[] UpdatedDates { get; set; }
public TimeOnly[] StartTimes { get; set; }
public DateOnly[] BirthDates { get; set; }
public Status[] Statuses { get; set; }
}
enum Status
{
Active,
Inactive,
Suspended
}
class Address
{
public string City { get; set; }
public string Country { get; set; }
}
public class ExpressionSerializerTests
{
private readonly ExpressionSerializer _serializer = new ExpressionSerializer();
[Fact]
public void Serializes_SimpleBinaryExpression()
{
Expression<Func<int, bool>> expr = e => e > 10;
Assert.Equal("e => (e > 10)", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_StringEquality()
{
Expression<Func<TestEntity, bool>> expr = e => e.Name == "John";
Assert.Equal("e => (e.Name == \"John\")", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_IntComparison()
{
Expression<Func<TestEntity, bool>> expr = e => e.Age > 18;
Assert.Equal("e => (e.Age > 18)", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_DoubleComparison()
{
Expression<Func<TestEntity, bool>> expr = e => e.Salary < 50000.50;
Assert.Equal("e => (e.Salary < 50000.5)", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_FloatComparison()
{
Expression<Func<TestEntity, bool>> expr = e => e.Score >= 85.3f;
Assert.Equal("e => (e.Score >= 85.3)", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_DecimalComparison()
{
Expression<Func<TestEntity, bool>> expr = e => e.Balance <= 1000.75m;
Assert.Equal("e => (e.Balance <= 1000.75)", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_ShortComparison()
{
Expression<Func<TestEntity, bool>> expr = e => e.Level == 3;
Assert.Equal("e => (e.Level == 3)", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_LongComparison()
{
Expression<Func<TestEntity, bool>> expr = e => e.Population > 1000000L;
Assert.Equal("e => (e.Population > 1000000)", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_EnumComparison()
{
Expression<Func<TestEntity, bool>> expr = e => e.AccountStatus == Status.Inactive;
Assert.Equal("e => (e.AccountStatus == 1)", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_ArrayContainsValue()
{
Expression<Func<TestEntity, bool>> expr = e => e.Scores.Contains(100);
Assert.Equal("e => e.Scores.Contains(100)", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_ArrayNotContainsValue()
{
Expression<Func<TestEntity, bool>> expr = e => !e.Scores.Contains(100);
Assert.Equal("e => (!(e.Scores.Contains(100)))", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_ArrayInValue()
{
Expression<Func<TestEntity, bool>> expr = e => e.Scores.Intersect(new [] { 100 }).Any();
Assert.Equal("e => e.Scores.Intersect(new [] { 100 }).Any()", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_ArrayNotInValue()
{
Expression<Func<TestEntity, bool>> expr = e => e.Scores.Except(new[] { 100 }).Any();
Assert.Equal("e => e.Scores.Except(new [] { 100 }).Any()", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_ArrayInValueOposite()
{
Expression<Func<TestEntity, bool>> expr = e => new[] { 100 }.Intersect(e.Scores).Any();
Assert.Equal("e => new [] { 100 }.Intersect(e.Scores).Any()", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_ArrayNotInValueOposite()
{
Expression<Func<TestEntity, bool>> expr = e => new[] { 100 }.Except(e.Scores).Any();
Assert.Equal("e => new [] { 100 }.Except(e.Scores).Any()", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_IntArrayInValueOposite()
{
Expression<Func<TestEntity, bool>> expr = e => new[] { 100 }.Intersect(e.Scores).Any();
Assert.Equal("e => new [] { 100 }.Intersect(e.Scores).Any()", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_IntArrayNotInValueOposite()
{
Expression<Func<TestEntity, bool>> expr = e => !new[] { 100 }.Intersect(e.Scores).Any();
Assert.Equal("e => (!(new [] { 100 }.Intersect(e.Scores).Any()))", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_DoubleArrayInValueOposite()
{
Expression<Func<TestEntity, bool>> expr = e => new[] { 99.99 }.Intersect(e.Salaries).Any();
Assert.Equal("e => new [] { 99.99 }.Intersect(e.Salaries).Any()", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_FloatArrayInValueOposite()
{
Expression<Func<TestEntity, bool>> expr = e => new[] { 5.5f }.Intersect(e.Heights).Any();
Assert.Equal("e => new [] { 5.5 }.Intersect(e.Heights).Any()", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_DecimalArrayInValueOposite()
{
Expression<Func<TestEntity, bool>> expr = e => new[] { 1000.75m }.Intersect(e.Balances).Any();
Assert.Equal("e => new [] { 1000.75 }.Intersect(e.Balances).Any()", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_ShortArrayInValueOposite()
{
Expression<Func<TestEntity, bool>> expr = e => new [] { (short)3 }.Intersect(e.Levels).Any();
Assert.Equal("e => new [] { 3 }.Intersect(e.Levels).Any()", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_LongArrayInValueOposite()
{
Expression<Func<TestEntity, bool>> expr = e => new [] { 1000000L }.Intersect(e.Populations).Any();
Assert.Equal("e => new [] { 1000000 }.Intersect(e.Populations).Any()", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_StringArrayInValueOposite()
{
Expression<Func<TestEntity, bool>> expr = e => new[] { "Alice", "Bob" }.Intersect(e.Names).Any();
Assert.Equal("e => (new [] { \"Alice\", \"Bob\" }).Intersect(e.Names).Any()", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_GuidArrayInValueOposite()
{
Expression<Func<TestEntity, bool>> expr = e => new[] { Guid.Parse("12345678-1234-1234-1234-123456789abc") }.Intersect(e.Ids).Any();
Assert.Equal("e => (new [] { Guid.Parse(\"12345678-1234-1234-1234-123456789abc\") }).Intersect(e.Ids).Any()", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_DateTimeArrayInValueOposite()
{
Expression<Func<TestEntity, bool>> expr = e => new[] { DateTime.Parse("2023-01-01T00:00:00.000Z") }.Intersect(e.CreatedDates).Any();
Assert.Equal("e => (new [] { DateTime.Parse(\"2023-01-01T00:00:00.000Z\") }).Intersect(e.CreatedDates).Any()", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_DateTimeOffsetArrayInValueOposite()
{
Expression<Func<TestEntity, bool>> expr = e => new[] { DateTimeOffset.Parse("2023-01-01T10:30:00.000+00:00") }.Intersect(e.UpdatedDates).Any();
Assert.Equal("e => (new [] { DateTimeOffset.Parse(\"2023-01-01T10:30:00.000+00:00\") }).Intersect(e.UpdatedDates).Any()", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_DateTimeWithRoundtripKind()
{
Expression<Func<TestEntity, bool>> expr = e =>
DateTime.Parse("2023-01-01T00:00:00.000Z", null, DateTimeStyles.RoundtripKind) > e.CreatedAt;
Assert.Equal(
"e => (DateTime.Parse(\"2023-01-01T00:00:00.000Z\", null, (System.Globalization.DateTimeStyles)128) > e.CreatedAt)",
_serializer.Serialize(expr));
}
[Fact]
public void Serializes_TimeOnlyArrayInValueOposite()
{
Expression<Func<TestEntity, bool>> expr = e => new[] { TimeOnly.Parse("12:00:00") }.Intersect(e.StartTimes).Any();
Assert.Equal("e => (new [] { TimeOnly.Parse(\"12:00:00\") }).Intersect(e.StartTimes).Any()", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_DateOnlyArrayInValueOposite()
{
Expression<Func<TestEntity, bool>> expr = e => new[] { DateOnly.Parse("2000-01-01") }.Intersect(e.BirthDates).Any();
Assert.Equal("e => (new [] { DateOnly.Parse(\"2000-01-01\") }).Intersect(e.BirthDates).Any()", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_EnumArrayInValueOposite()
{
Expression<Func<TestEntity, bool>> expr = e => new[] { Status.Active, Status.Inactive }.Intersect(e.Statuses).Any();
Assert.Equal("e => (new [] { (Radzen.Blazor.Tests.Status)0, (Radzen.Blazor.Tests.Status)1 }).Intersect(e.Statuses).Any()", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_ListContainsValue()
{
Expression<Func<TestEntity, bool>> expr = e => e.Tags.Contains("VIP");
Assert.Equal("e => e.Tags.Contains(\"VIP\")", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_ListNotContainsValue()
{
Expression<Func<TestEntity, bool>> expr = e => !e.Tags.Contains("VIP");
Assert.Equal("e => (!(e.Tags.Contains(\"VIP\")))", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_ListAnyCheck()
{
Expression<Func<TestEntity, bool>> expr = e => e.Children.Any(c => c.Age > 18);
Assert.Equal("e => e.Children.Any(c => (c.Age > 18))", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_ListNotAnyCheck()
{
Expression<Func<TestEntity, bool>> expr = e => !e.Children.Any(c => c.Age > 18);
Assert.Equal("e => (!(e.Children.Any(c => (c.Age > 18))))", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_EntitySubPropertyCheck()
{
Expression<Func<TestEntity, bool>> expr = e => e.Address.City == "New York";
Assert.Equal("e => (e.Address.City == \"New York\")", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_ComplexExpressionWithProperties()
{
Expression<Func<TestEntity, bool>> expr = e => e.Age > 18 && e.Tags.Contains("Member") || e.Address.City == "London";
Assert.Equal("e => (((e.Age > 18) && e.Tags.Contains(\"Member\")) || (e.Address.City == \"London\"))", _serializer.Serialize(expr));
}
[Fact]
public void Serializes_NotContains()
{
Expression<Func<TestEntity, bool>> expr = e => !e.Tags.Contains("Member");
Assert.Equal("e => (!(e.Tags.Contains(\"Member\")))", _serializer.Serialize(expr));
}
}
}

View File

@@ -184,13 +184,13 @@ namespace Radzen.Blazor.Tests
Assert.Contains("SummaryContent", component.Markup);
Assert.Equal(
"",
component.Find(".rz-fieldset-content-summary").ParentElement.Attributes.First(attr => attr.Name == "style").Value
"false",
component.Find(".rz-fieldset-content-summary").ParentElement.ParentElement.Attributes.First(attr => attr.Name == "aria-hidden").Value
);
}
[Fact]
public void Fieldset_DontRenders_SummaryWhenOpen()
public void Fieldset_DoesNotRender_SummaryWhenOpen()
{
using var ctx = new TestContext();
var component = ctx.RenderComponent<RadzenFieldset>();
@@ -210,8 +210,8 @@ namespace Radzen.Blazor.Tests
Assert.Contains("SummaryContent", component.Markup);
Assert.Equal(
"display: none",
component.Find(".rz-fieldset-content-summary").ParentElement.Attributes.First(attr => attr.Name == "style").Value
"true",
component.Find(".rz-fieldset-content-summary").ParentElement.ParentElement.Attributes.First(attr => attr.Name == "aria-hidden").Value
);
}
}

View File

@@ -78,6 +78,20 @@ namespace Radzen.Blazor.Tests
Assert.Contains(@$"target=""{target}""", component.Markup);
}
[Fact]
public void Link_Renders_DisabledParameter()
{
using var ctx = new TestContext();
var component = ctx.RenderComponent<RadzenLink>();
component.SetParametersAndRender(parameters => parameters.Add(p => p.Disabled, true));
Assert.Contains("class=\"rz-link rz-link-disabled active\"", component.Markup);
Assert.DoesNotContain("href=", component.Markup);
}
[Fact]
public void Icon_Renders_UnmatchedParameter()
{

View File

@@ -0,0 +1,314 @@
using Xunit;
namespace Radzen.Blazor.Markdown.Tests;
public class BlockQuoteTests
{
private static string ToXml(string markdown)
{
var document = MarkdownParser.Parse(markdown);
return XmlVisitor.ToXml(document);
}
[Fact]
public void Parse_BasicBlockQuote()
{
Assert.Equal(@"<document>
<block_quote>
<paragraph>
<text>foo</text>
</paragraph>
</block_quote>
</document>", ToXml(@"> foo"));
}
[Theory]
[InlineData(@"> # Foo
> bar
> baz", @"<document>
<block_quote>
<heading level=""1"">
<text>Foo</text>
</heading>
<paragraph>
<text>bar</text>
<softbreak />
<text>baz</text>
</paragraph>
</block_quote>
</document>")]
[InlineData(@"># Foo
>bar
> baz", @"<document>
<block_quote>
<heading level=""1"">
<text>Foo</text>
</heading>
<paragraph>
<text>bar</text>
<softbreak />
<text>baz</text>
</paragraph>
</block_quote>
</document>")]
[InlineData(@" > # Foo
> bar
> baz", @"<document>
<block_quote>
<heading level=""1"">
<text>Foo</text>
</heading>
<paragraph>
<text>bar</text>
<softbreak />
<text>baz</text>
</paragraph>
</block_quote>
</document>")]
[InlineData(@" > # Foo
> bar
> baz", @"<document>
<code_block>&gt; # Foo
&gt; bar
&gt; baz
</code_block>
</document>")]
[InlineData(@"> # Foo
> bar
baz", @"<document>
<block_quote>
<heading level=""1"">
<text>Foo</text>
</heading>
<paragraph>
<text>bar</text>
<softbreak />
<text>baz</text>
</paragraph>
</block_quote>
</document>")]
[InlineData(@"> bar
baz
> foo", @"<document>
<block_quote>
<paragraph>
<text>bar</text>
<softbreak />
<text>baz</text>
<softbreak />
<text>foo</text>
</paragraph>
</block_quote>
</document>")]
[InlineData(@"> foo
---", @"<document>
<block_quote>
<paragraph>
<text>foo</text>
</paragraph>
</block_quote>
<thematic_break />
</document>")]
[InlineData(@"> - foo
- bar", @"<document>
<block_quote>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
</item>
</list>
</block_quote>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>bar</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"> foo
bar", @"<document>
<block_quote>
<code_block>foo
</code_block>
</block_quote>
<code_block>bar
</code_block>
</document>")]
[InlineData(@"> ```
foo
```", @"<document>
<block_quote>
<code_block></code_block>
</block_quote>
<paragraph>
<text>foo</text>
</paragraph>
<code_block></code_block>
</document>")]
[InlineData(@"> foo
- bar", @"<document>
<block_quote>
<paragraph>
<text>foo</text>
<softbreak />
<text>- bar</text>
</paragraph>
</block_quote>
</document>")]
[InlineData(@">", @"<document>
<block_quote />
</document>")]
[InlineData(@">
>
> ", @"<document>
<block_quote />
</document>")]
[InlineData(@">
> foo
> ", @"<document>
<block_quote>
<paragraph>
<text>foo</text>
</paragraph>
</block_quote>
</document>")]
[InlineData(@"> foo
> bar", @"<document>
<block_quote>
<paragraph>
<text>foo</text>
</paragraph>
</block_quote>
<block_quote>
<paragraph>
<text>bar</text>
</paragraph>
</block_quote>
</document>")]
[InlineData(@"> foo
>
> bar", @"<document>
<block_quote>
<paragraph>
<text>foo</text>
</paragraph>
<paragraph>
<text>bar</text>
</paragraph>
</block_quote>
</document>")]
[InlineData(@"foo
> bar", @"<document>
<paragraph>
<text>foo</text>
</paragraph>
<block_quote>
<paragraph>
<text>bar</text>
</paragraph>
</block_quote>
</document>")]
[InlineData(@"> aaa
***
> bbb", @"<document>
<block_quote>
<paragraph>
<text>aaa</text>
</paragraph>
</block_quote>
<thematic_break />
<block_quote>
<paragraph>
<text>bbb</text>
</paragraph>
</block_quote>
</document>")]
[InlineData(@"> bar
baz", @"<document>
<block_quote>
<paragraph>
<text>bar</text>
<softbreak />
<text>baz</text>
</paragraph>
</block_quote>
</document>")]
[InlineData(@"> bar
baz", @"<document>
<block_quote>
<paragraph>
<text>bar</text>
</paragraph>
</block_quote>
<paragraph>
<text>baz</text>
</paragraph>
</document>")]
[InlineData(@"> bar
>
baz", @"<document>
<block_quote>
<paragraph>
<text>bar</text>
</paragraph>
</block_quote>
<paragraph>
<text>baz</text>
</paragraph>
</document>")]
[InlineData(@"> > > foo
bar", @"<document>
<block_quote>
<block_quote>
<block_quote>
<paragraph>
<text>foo</text>
<softbreak />
<text>bar</text>
</paragraph>
</block_quote>
</block_quote>
</block_quote>
</document>")]
[InlineData(@">>> foo
> bar
>>baz", @"<document>
<block_quote>
<block_quote>
<block_quote>
<paragraph>
<text>foo</text>
<softbreak />
<text>bar</text>
<softbreak />
<text>baz</text>
</paragraph>
</block_quote>
</block_quote>
</block_quote>
</document>")]
[InlineData(@"> code
> not code", @"<document>
<block_quote>
<code_block>code
</code_block>
</block_quote>
<block_quote>
<paragraph>
<text>not code</text>
</paragraph>
</block_quote>
</document>")]
public void Parse_BlockQuote(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
}

View File

@@ -0,0 +1,175 @@
using Xunit;
namespace Radzen.Blazor.Markdown.Tests;
public class CodeTests
{
private static string ToXml(string markdown)
{
var document = MarkdownParser.Parse(markdown);
return XmlVisitor.ToXml(document);
}
[Theory]
[InlineData("`foo`",
@"<document>
<paragraph>
<code>foo</code>
</paragraph>
</document>")]
[InlineData("`` foo ` bar ``",
@"<document>
<paragraph>
<code>foo ` bar</code>
</paragraph>
</document>")]
[InlineData("` `` `",
@"<document>
<paragraph>
<code>``</code>
</paragraph>
</document>")]
public void Parse_BasicCode_ReturnsCodeNode(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData("` `` `",
@"<document>
<paragraph>
<code> `` </code>
</paragraph>
</document>")]
[InlineData("` a`",
@"<document>
<paragraph>
<code> a</code>
</paragraph>
</document>")]
[InlineData(@"` `
` `",
@"<document>
<paragraph>
<code> </code>
<softbreak />
<code> </code>
</paragraph>
</document>")]
public void Parse_CodeWithSpaces_PreservesSpaces(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData(@"``
foo
bar
baz
``",
@"<document>
<paragraph>
<code>foo bar baz</code>
</paragraph>
</document>")]
[InlineData(@"``
foo
``",
@"<document>
<paragraph>
<code>foo </code>
</paragraph>
</document>")]
[InlineData(@"`foo bar
baz`",
@"<document>
<paragraph>
<code>foo bar baz</code>
</paragraph>
</document>")]
public void Parse_CodeWithLineBreaks_ConvertsToSpace(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData("`foo\\`bar`", @"<document>
<paragraph>
<code>foo\</code>
<text>bar</text>
<text>`</text>
</paragraph>
</document>")]
[InlineData("``foo`bar``", @"<document>
<paragraph>
<code>foo`bar</code>
</paragraph>
</document>")]
[InlineData("` foo `` bar `", @"<document>
<paragraph>
<code>foo `` bar</code>
</paragraph>
</document>")]
public void Parse_CodeWithBacktics(string mardown, string expected)
{
Assert.Equal(expected, ToXml(mardown));
}
[Theory]
[InlineData("*foo`*`", @"<document>
<paragraph>
<text>*</text>
<text>foo</text>
<code>*</code>
</paragraph>
</document>")]
[InlineData("[not a `link](/foo`)",
@"<document>
<paragraph>
<text>[</text>
<text>not a </text>
<code>link](/foo</code>
<text>)</text>
</paragraph>
</document>")]
[InlineData("`<https://foo.bar.`baz>`",@"<document>
<paragraph>
<code>&lt;https://foo.bar.</code>
<text>baz&gt;</text>
<text>`</text>
</paragraph>
</document>")]
public void Parse_CodePrecedence(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData("```foo``", @"<document>
<paragraph>
<text>```</text>
<text>foo</text>
<text>``</text>
</paragraph>
</document>")]
[InlineData("`foo", @"<document>
<paragraph>
<text>`</text>
<text>foo</text>
</paragraph>
</document>")]
[InlineData("`foo``bar``", @"<document>
<paragraph>
<text>`</text>
<text>foo</text>
<code>bar</code>
</paragraph>
</document>")]
public void Parse_UnmatchingBacktics(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
}

View File

@@ -0,0 +1,272 @@
using Xunit;
namespace Radzen.Blazor.Markdown.Tests;
public class EmphasisTests
{
private static string ToXml(string markdown)
{
var document = MarkdownParser.Parse(markdown);
return XmlVisitor.ToXml(document);
}
[Theory]
[InlineData(@"**foo** bar
baz",@"<document>
<paragraph>
<strong>
<text>foo</text>
</strong>
<text> bar</text>
<softbreak />
<text>baz</text>
</paragraph>
</document>")]
[InlineData("*foo bar*",
@"<document>
<paragraph>
<emph>
<text>foo bar</text>
</emph>
</paragraph>
</document>")]
[InlineData("a * foo bar*",
@"<document>
<paragraph>
<text>a </text>
<text>*</text>
<text> foo bar</text>
<text>*</text>
</paragraph>
</document>")]
[InlineData("a*\"foo\"*",
@"<document>
<paragraph>
<text>a</text>
<text>*</text>
<text>""foo""</text>
<text>*</text>
</paragraph>
</document>")]
[InlineData("* a *",
@"<document>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>a </text>
<text>*</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData("foo*bar*",
@"<document>
<paragraph>
<text>foo</text>
<emph>
<text>bar</text>
</emph>
</paragraph>
</document>")]
[InlineData("5*6*78",
@"<document>
<paragraph>
<text>5</text>
<emph>
<text>6</text>
</emph>
<text>78</text>
</paragraph>
</document>")]
[InlineData("_foo bar_",
@"<document>
<paragraph>
<emph>
<text>foo bar</text>
</emph>
</paragraph>
</document>")]
[InlineData("_ foo bar_",
@"<document>
<paragraph>
<text>_</text>
<text> foo bar</text>
<text>_</text>
</paragraph>
</document>")]
[InlineData("a_\"foo\"_",
@"<document>
<paragraph>
<text>a</text>
<text>_</text>
<text>""foo""</text>
<text>_</text>
</paragraph>
</document>")]
[InlineData("foo_bar_",
@"<document>
<paragraph>
<text>foo</text>
<text>_</text>
<text>bar</text>
<text>_</text>
</paragraph>
</document>")]
[InlineData("5_6_78",
@"<document>
<paragraph>
<text>5</text>
<text>_</text>
<text>6</text>
<text>_</text>
<text>78</text>
</paragraph>
</document>")]
[InlineData("пристаням_стремятся_",
@"<document>
<paragraph>
<text>пристаням</text>
<text>_</text>
<text>стремятся</text>
<text>_</text>
</paragraph>
</document>")]
[InlineData("aa_\"bb\"_cc",
@"<document>
<paragraph>
<text>aa</text>
<text>_</text>
<text>""bb""</text>
<text>_</text>
<text>cc</text>
</paragraph>
</document>")]
[InlineData("foo-_(bar)_",
@"<document>
<paragraph>
<text>foo-</text>
<emph>
<text>(bar)</text>
</emph>
</paragraph>
</document>")]
[InlineData("_foo*",
@"<document>
<paragraph>
<text>_</text>
<text>foo</text>
<text>*</text>
</paragraph>
</document>")]
[InlineData("*foo bar *",
@"<document>
<paragraph>
<text>*</text>
<text>foo bar </text>
<text>*</text>
</paragraph>
</document>")]
[InlineData("*foo bar\nbaz*",
@"<document>
<paragraph>
<emph>
<text>foo bar</text>
<softbreak />
<text>baz</text>
</emph>
</paragraph>
</document>")]
[InlineData("*(*foo)",
@"<document>
<paragraph>
<text>*</text>
<text>(</text>
<text>*</text>
<text>foo)</text>
</paragraph>
</document>")]
[InlineData("*(*foo*)*",
@"<document>
<paragraph>
<emph>
<text>(</text>
<emph>
<text>foo</text>
</emph>
<text>)</text>
</emph>
</paragraph>
</document>")]
[InlineData("*foo*bar",
@"<document>
<paragraph>
<emph>
<text>foo</text>
</emph>
<text>bar</text>
</paragraph>
</document>")]
[InlineData("_foo bar _",
@"<document>
<paragraph>
<text>_</text>
<text>foo bar </text>
<text>_</text>
</paragraph>
</document>")]
[InlineData("_(_foo_)_",
@"<document>
<paragraph>
<emph>
<text>(</text>
<emph>
<text>foo</text>
</emph>
<text>)</text>
</emph>
</paragraph>
</document>")]
[InlineData("_foo_bar",
@"<document>
<paragraph>
<text>_</text>
<text>foo</text>
<text>_</text>
<text>bar</text>
</paragraph>
</document>")]
[InlineData("_пристаням_стремятся",
@"<document>
<paragraph>
<text>_</text>
<text>пристаням</text>
<text>_</text>
<text>стремятся</text>
</paragraph>
</document>")]
[InlineData("_foo_bar_baz_",
@"<document>
<paragraph>
<emph>
<text>foo</text>
<text>_</text>
<text>bar</text>
<text>_</text>
<text>baz</text>
</emph>
</paragraph>
</document>")]
[InlineData("_(bar)_.",
@"<document>
<paragraph>
<emph>
<text>(bar)</text>
</emph>
<text>.</text>
</paragraph>
</document>")]
public void Parse_EmphasisRules_AdheresToCommonMarkSpec(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
}

View File

@@ -0,0 +1,264 @@
using Xunit;
namespace Radzen.Blazor.Markdown.Tests;
public class FencedCodeBlockTests
{
private static string ToXml(string markdown)
{
var document = MarkdownParser.Parse(markdown);
return XmlVisitor.ToXml(document);
}
[Fact]
public void Parse_BasicFencedCodeBlock()
{
Assert.Equal(@"<document>
<code_block>foo
</code_block>
</document>", ToXml(@"```
foo
```"));
}
[Theory]
[InlineData(@"```
<
>
```", @"<document>
<code_block>&lt;
&gt;
</code_block>
</document>")]
[InlineData(@"~~~
<
>
~~~", @"<document>
<code_block>&lt;
&gt;
</code_block>
</document>")]
[InlineData(@"``
foo
``", @"<document>
<paragraph>
<code>foo</code>
</paragraph>
</document>")]
[InlineData(@"```
aaa
~~~
```", @"<document>
<code_block>aaa
~~~
</code_block>
</document>")]
[InlineData(@"~~~
aaa
```
~~~", @"<document>
<code_block>aaa
```
</code_block>
</document>")]
[InlineData(@"````
aaa
```
``````", @"<document>
<code_block>aaa
```
</code_block>
</document>")]
[InlineData(@"~~~~
aaa
~~~
~~~~", @"<document>
<code_block>aaa
~~~
</code_block>
</document>")]
[InlineData(@"```", @"<document>
<code_block></code_block>
</document>")]
[InlineData(@"`````
```
aaa", @"<document>
<code_block>
```
aaa
</code_block>
</document>")]
[InlineData(@"> ```
> aaa
bbb", @"<document>
<block_quote>
<code_block>aaa
</code_block>
</block_quote>
<paragraph>
<text>bbb</text>
</paragraph>
</document>")]
[InlineData(@"```
```", @"<document>
<code_block>
</code_block>
</document>")]
[InlineData(@"```
```", @"<document>
<code_block></code_block>
</document>")]
[InlineData(@" ```
aaa
aaa
```", @"<document>
<code_block>aaa
aaa
</code_block>
</document>")]
[InlineData(@" ```
aaa
aaa
aaa
```", @"<document>
<code_block>aaa
aaa
aaa
</code_block>
</document>")]
[InlineData(@" ```
aaa
aaa
aaa
```", @"<document>
<code_block>aaa
aaa
aaa
</code_block>
</document>")]
[InlineData(@" ```
aaa
```", @"<document>
<code_block>```
aaa
```
</code_block>
</document>")]
[InlineData(@"```
aaa
```", @"<document>
<code_block>aaa
</code_block>
</document>")]
[InlineData(@" ```
aaa
````", @"<document>
<code_block>aaa
</code_block>
</document>")]
[InlineData(@"```
aaa
```", @"<document>
<code_block>aaa
```
</code_block>
</document>")]
[InlineData(@"``` ```
aaa", @"<document>
<paragraph>
<code> </code>
<softbreak />
<text>aaa</text>
</paragraph>
</document>")]
[InlineData(@"~~~~~~
aaa
~~~ ~~", @"<document>
<code_block>aaa
~~~ ~~
</code_block>
</document>")]
[InlineData(@"foo
```
bar
```
baz", @"<document>
<paragraph>
<text>foo</text>
</paragraph>
<code_block>bar
</code_block>
<paragraph>
<text>baz</text>
</paragraph>
</document>")]
[InlineData(@"foo
---
~~~
bar
~~~
# baz", @"<document>
<heading level=""2"">
<text>foo</text>
</heading>
<code_block>bar
</code_block>
<heading level=""1"">
<text>baz</text>
</heading>
</document>")]
[InlineData(@"```ruby
def foo(x)
return 3
end
```", @"<document>
<code_block info=""ruby"">def foo(x)
return 3
end
</code_block>
</document>")]
[InlineData(@"~~~~ ruby startline=3 $%@#$
def foo(x)
return 3
end
~~~~~~~", @"<document>
<code_block info=""ruby startline=3 $%@#$"">def foo(x)
return 3
end
</code_block>
</document>")]
[InlineData(@"````;
````", @"<document>
<code_block info="";""></code_block>
</document>")]
[InlineData(@"``` aa ```
foo", @"<document>
<paragraph>
<code>aa</code>
<softbreak />
<text>foo</text>
</paragraph>
</document>")]
[InlineData(@"~~~ aa ``` ~~~
foo
~~~", @"<document>
<code_block info=""aa ``` ~~~"">foo
</code_block>
</document>")]
[InlineData(@"```
``` aaa
```", @"<document>
<code_block>``` aaa
</code_block>
</document>")]
public void Parse_FencedCodeBlock(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
}

View File

@@ -0,0 +1,132 @@
using Xunit;
namespace Radzen.Blazor.Markdown.Tests;
public class HardLineBreakTests
{
private static string ToXml(string markdown)
{
var document = MarkdownParser.Parse(markdown);
return XmlVisitor.ToXml(document);
}
[Theory]
[InlineData(@"foo
baz", @"<document>
<paragraph>
<text>foo</text>
<linebreak />
<text>baz</text>
</paragraph>
</document>")]
[InlineData("foo \r\nbaz", @"<document>
<paragraph>
<text>foo</text>
<linebreak />
<text>baz</text>
</paragraph>
</document>")]
[InlineData(@"foo\
baz", @"<document>
<paragraph>
<text>foo</text>
<linebreak />
<text>baz</text>
</paragraph>
</document>")]
[InlineData(@"foo
baz", @"<document>
<paragraph>
<text>foo</text>
<linebreak />
<text>baz</text>
</paragraph>
</document>")]
[InlineData(@"foo
bar", @"<document>
<paragraph>
<text>foo</text>
<linebreak />
<text>bar</text>
</paragraph>
</document>")]
[InlineData(@"foo\
bar", @"<document>
<paragraph>
<text>foo</text>
<linebreak />
<text>bar</text>
</paragraph>
</document>")]
[InlineData(@"*foo
bar*", @"<document>
<paragraph>
<emph>
<text>foo</text>
<linebreak />
<text>bar</text>
</emph>
</paragraph>
</document>")]
[InlineData(@"*foo\
bar*", @"<document>
<paragraph>
<emph>
<text>foo</text>
<linebreak />
<text>bar</text>
</emph>
</paragraph>
</document>")]
[InlineData(@"`code\
span`", @"<document>
<paragraph>
<code>code\ span</code>
</paragraph>
</document>")]
[InlineData(@"`code
span`", @"<document>
<paragraph>
<code>code span</code>
</paragraph>
</document>")]
[InlineData(@"<a href=""foo
bar"">", @"<document>
<paragraph>
<html_inline>&lt;a href=""foo
bar""&gt;</html_inline>
</paragraph>
</document>")]
[InlineData(@"<a href=""foo\
bar"">", @"<document>
<paragraph>
<html_inline>&lt;a href=""foo\
bar""&gt;</html_inline>
</paragraph>
</document>")]
[InlineData(@"foo\", @"<document>
<paragraph>
<text>foo\</text>
</paragraph>
</document>")]
[InlineData(@"foo ", @"<document>
<paragraph>
<text>foo</text>
</paragraph>
</document>")]
[InlineData(@"### foo\", @"<document>
<heading level=""3"">
<text>foo\</text>
</heading>
</document>")]
[InlineData(@"### foo ", @"<document>
<heading level=""3"">
<text>foo</text>
</heading>
</document>")]
public void Parse_HardLineBreak(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
}

View File

@@ -0,0 +1,511 @@
using Xunit;
namespace Radzen.Blazor.Markdown.Tests;
public class HeadingTests
{
private static string ToXml(string markdown)
{
var document = MarkdownParser.Parse(markdown);
return XmlVisitor.ToXml(document);
}
[Fact]
public void Parse_BasicAtxHeading()
{
Assert.Equal(@"<document>
<heading level=""1"">
<text>foo</text>
</heading>
</document>", ToXml("# foo"));
}
[Theory]
[InlineData(@"# foo
## foo
### foo
#### foo
##### foo
###### foo", @"<document>
<heading level=""1"">
<text>foo</text>
</heading>
<heading level=""2"">
<text>foo</text>
</heading>
<heading level=""3"">
<text>foo</text>
</heading>
<heading level=""4"">
<text>foo</text>
</heading>
<heading level=""5"">
<text>foo</text>
</heading>
<heading level=""6"">
<text>foo</text>
</heading>
</document>")]
[InlineData(@"####### foo", @"<document>
<paragraph>
<text>####### foo</text>
</paragraph>
</document>")]
[InlineData(@"#5 bolt
#hashtag", @"<document>
<paragraph>
<text>#5 bolt</text>
</paragraph>
<paragraph>
<text>#hashtag</text>
</paragraph>
</document>")]
[InlineData(@"\## foo", @"<document>
<paragraph>
<text>#</text>
<text># foo</text>
</paragraph>
</document>")]
[InlineData(@"# foo *bar* \*baz\*", @"<document>
<heading level=""1"">
<text>foo </text>
<emph>
<text>bar</text>
</emph>
<text> </text>
<text>*</text>
<text>baz</text>
<text>*</text>
</heading>
</document>")]
[InlineData(@"# foo ", @"<document>
<heading level=""1"">
<text>foo</text>
</heading>
</document>")]
[InlineData(@" ### foo
## foo
# foo", @"<document>
<heading level=""3"">
<text>foo</text>
</heading>
<heading level=""2"">
<text>foo</text>
</heading>
<heading level=""1"">
<text>foo</text>
</heading>
</document>")]
[InlineData(@" # foo", @"<document>
<code_block># foo
</code_block>
</document>")]
[InlineData(@"foo
# bar", @"<document>
<paragraph>
<text>foo</text>
<softbreak />
<text># bar</text>
</paragraph>
</document>")]
[InlineData(@"## foo ##
### bar ###", @"<document>
<heading level=""2"">
<text>foo</text>
</heading>
<heading level=""3"">
<text>bar</text>
</heading>
</document>")]
[InlineData(@"# foo ##################################
##### foo ##", @"<document>
<heading level=""1"">
<text>foo</text>
</heading>
<heading level=""5"">
<text>foo</text>
</heading>
</document>")]
[InlineData(@"### foo ### ", @"<document>
<heading level=""3"">
<text>foo</text>
</heading>
</document>")]
[InlineData(@"### foo ### b", @"<document>
<heading level=""3"">
<text>foo ### b</text>
</heading>
</document>")]
[InlineData(@"# foo#", @"<document>
<heading level=""1"">
<text>foo#</text>
</heading>
</document>")]
[InlineData(@"### foo \###
## foo #\##
# foo \#", @"<document>
<heading level=""3"">
<text>foo </text>
<text>#</text>
<text>##</text>
</heading>
<heading level=""2"">
<text>foo #</text>
<text>#</text>
<text>#</text>
</heading>
<heading level=""1"">
<text>foo </text>
<text>#</text>
</heading>
</document>")]
[InlineData(@"****
## foo
****", @"<document>
<thematic_break />
<heading level=""2"">
<text>foo</text>
</heading>
<thematic_break />
</document>")]
[InlineData(@"Foo bar
# baz
Bar foo", @"<document>
<paragraph>
<text>Foo bar</text>
</paragraph>
<heading level=""1"">
<text>baz</text>
</heading>
<paragraph>
<text>Bar foo</text>
</paragraph>
</document>")]
[InlineData(@"##
#
### ###", @"<document>
<heading level=""2"" />
<heading level=""1"" />
<heading level=""3"" />
</document>")]
public void Parse_AtxHeading(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData(@"Foo *bar*
=========
Foo *baz*
---------", @"<document>
<heading level=""1"">
<text>Foo </text>
<emph>
<text>bar</text>
</emph>
</heading>
<heading level=""2"">
<text>Foo </text>
<emph>
<text>baz</text>
</emph>
</heading>
</document>")]
[InlineData(@"Foo *bar
baz*
====", @"<document>
<heading level=""1"">
<text>Foo </text>
<emph>
<text>bar</text>
<softbreak />
<text>baz</text>
</emph>
</heading>
</document>")]
[InlineData(@" Foo *bar
baz*
====", @"<document>
<heading level=""1"">
<text>Foo </text>
<emph>
<text>bar</text>
<softbreak />
<text>baz</text>
</emph>
</heading>
</document>")]
[InlineData(@"Foo
-------------------------
Foo
=", @"<document>
<heading level=""2"">
<text>Foo</text>
</heading>
<heading level=""1"">
<text>Foo</text>
</heading>
</document>")]
[InlineData(@" Foo
---
Foo
-----
Foo
===", @"<document>
<heading level=""2"">
<text>Foo</text>
</heading>
<heading level=""2"">
<text>Foo</text>
</heading>
<heading level=""1"">
<text>Foo</text>
</heading>
</document>")]
[InlineData(@" Foo
---
Foo
---", @"<document>
<code_block>Foo
---
Foo
</code_block>
<thematic_break />
</document>")]
[InlineData(@"Foo
---- ", @"<document>
<heading level=""2"">
<text>Foo</text>
</heading>
</document>")]
[InlineData(@"Foo
---", @"<document>
<paragraph>
<text>Foo</text>
<softbreak />
<text>---</text>
</paragraph>
</document>")]
[InlineData(@"Foo
= =
Foo
--- -", @"<document>
<paragraph>
<text>Foo</text>
<softbreak />
<text>= =</text>
</paragraph>
<paragraph>
<text>Foo</text>
</paragraph>
<thematic_break />
</document>")]
[InlineData(@"Foo
-----", @"<document>
<heading level=""2"">
<text>Foo</text>
</heading>
</document>")]
[InlineData(@"Foo\
----", @"<document>
<heading level=""2"">
<text>Foo\</text>
</heading>
</document>")]
[InlineData(@"`Foo
----
`
<a title=""a lot
---
of dashes""/>", @"<document>
<heading level=""2"">
<text>`</text>
<text>Foo</text>
</heading>
<paragraph>
<text>`</text>
</paragraph>
<heading level=""2"">
<text>&lt;a title=""a lot</text>
</heading>
<paragraph>
<text>of dashes""/&gt;</text>
</paragraph>
</document>")]
[InlineData(@"> Foo
---", @"<document>
<block_quote>
<paragraph>
<text>Foo</text>
</paragraph>
</block_quote>
<thematic_break />
</document>")]
[InlineData(@"> foo
bar
===", @"<document>
<block_quote>
<paragraph>
<text>foo</text>
<softbreak />
<text>bar</text>
<softbreak />
<text>===</text>
</paragraph>
</block_quote>
</document>")]
[InlineData(@"- Foo
---", @"<document>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>Foo</text>
</paragraph>
</item>
</list>
<thematic_break />
</document>")]
[InlineData(@"Foo
Bar
---", @"<document>
<heading level=""2"">
<text>Foo</text>
<softbreak />
<text>Bar</text>
</heading>
</document>")]
[InlineData(@"---
Foo
---
Bar
---
Baz", @"<document>
<thematic_break />
<heading level=""2"">
<text>Foo</text>
</heading>
<heading level=""2"">
<text>Bar</text>
</heading>
<paragraph>
<text>Baz</text>
</paragraph>
</document>")]
[InlineData(@"
====", @"<document>
<paragraph>
<text>====</text>
</paragraph>
</document>")]
[InlineData(@"---
---", @"<document>
<thematic_break />
<thematic_break />
</document>")]
[InlineData(@"- foo
-----", @"<document>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
</item>
</list>
<thematic_break />
</document>")]
[InlineData(@" foo
---", @"<document>
<code_block>foo
</code_block>
<thematic_break />
</document>")]
[InlineData(@"> foo
-----", @"<document>
<block_quote>
<paragraph>
<text>foo</text>
</paragraph>
</block_quote>
<thematic_break />
</document>")]
[InlineData(@"\> foo
------", @"<document>
<heading level=""2"">
<text>&gt; foo</text>
</heading>
</document>")]
[InlineData(@"Foo
bar
---
baz", @"<document>
<paragraph>
<text>Foo</text>
</paragraph>
<heading level=""2"">
<text>bar</text>
</heading>
<paragraph>
<text>baz</text>
</paragraph>
</document>")]
[InlineData(@"Foo
bar
---
baz", @"<document>
<paragraph>
<text>Foo</text>
<softbreak />
<text>bar</text>
</paragraph>
<thematic_break />
<paragraph>
<text>baz</text>
</paragraph>
</document>")]
[InlineData(@"Foo
bar
* * *
baz", @"<document>
<paragraph>
<text>Foo</text>
<softbreak />
<text>bar</text>
</paragraph>
<thematic_break />
<paragraph>
<text>baz</text>
</paragraph>
</document>")]
[InlineData(@"Foo
bar
\---
baz", @"<document>
<paragraph>
<text>Foo</text>
<softbreak />
<text>bar</text>
<softbreak />
<text>-</text>
<text>--</text>
<softbreak />
<text>baz</text>
</paragraph>
</document>")]
public void Parse_SetExtHeading(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
}

View File

@@ -0,0 +1,487 @@
using Xunit;
namespace Radzen.Blazor.Markdown.Tests;
public class HtmlBlockTests
{
private static string ToXml(string markdown)
{
var document = MarkdownParser.Parse(markdown);
return XmlVisitor.ToXml(document);
}
[Theory]
[InlineData(@"<table><tr><td>
<pre>
**Hello**,
_world_.
</pre>
</td></tr></table>", @"<document>
<html_block>&lt;table&gt;&lt;tr&gt;&lt;td&gt;
&lt;pre&gt;
**Hello**,</html_block>
<paragraph>
<emph>
<text>world</text>
</emph>
<text>.</text>
<softbreak />
<html_inline>&lt;/pre&gt;</html_inline>
</paragraph>
<html_block>&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;</html_block>
</document>")]
[InlineData(@"<table>
<tr>
<td>
hi
</td>
</tr>
</table>
okay.", @"<document>
<html_block>&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;
hi
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;</html_block>
<paragraph>
<text>okay.</text>
</paragraph>
</document>")]
[InlineData(@" <div>
*hello*
<foo><a>", @"<document>
<html_block> &lt;div&gt;
*hello*
&lt;foo&gt;&lt;a&gt;</html_block>
</document>")]
[InlineData(@"</div>
*foo*", @"<document>
<html_block>&lt;/div&gt;
*foo*</html_block>
</document>")]
[InlineData(@"<DIV CLASS=""foo"">
*Markdown*
</DIV>", @"<document>
<html_block>&lt;DIV CLASS=""foo""&gt;</html_block>
<paragraph>
<emph>
<text>Markdown</text>
</emph>
</paragraph>
<html_block>&lt;/DIV&gt;</html_block>
</document>")]
[InlineData(@"<div id=""foo""
class=""bar"">
</div>", @"<document>
<html_block>&lt;div id=""foo""
class=""bar""&gt;
&lt;/div&gt;</html_block>
</document>")]
[InlineData(@"<div id=""foo"" class=""bar
baz"">
</div>", @"<document>
<html_block>&lt;div id=""foo"" class=""bar
baz""&gt;
&lt;/div&gt;</html_block>
</document>")]
[InlineData(@"<div>
*foo*
*bar*", @"<document>
<html_block>&lt;div&gt;
*foo*</html_block>
<paragraph>
<emph>
<text>bar</text>
</emph>
</paragraph>
</document>")]
[InlineData(@"<div id=""foo""
*hi*", @"<document>
<html_block>&lt;div id=""foo""
*hi*</html_block>
</document>")]
[InlineData(@"<div class
foo", @"<document>
<html_block>&lt;div class
foo</html_block>
</document>")]
[InlineData(@"<div *???-&&&-<---
*foo*", @"<document>
<html_block>&lt;div *???-&amp;&amp;&amp;-&lt;---
*foo*</html_block>
</document>")]
[InlineData(@"<div><a href=""bar"">*foo*</a></div>", @"<document>
<html_block>&lt;div&gt;&lt;a href=""bar""&gt;*foo*&lt;/a&gt;&lt;/div&gt;</html_block>
</document>")]
[InlineData(@"<table><tr><td>
foo
</td></tr></table>", @"<document>
<html_block>&lt;table&gt;&lt;tr&gt;&lt;td&gt;
foo
&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;</html_block>
</document>")]
[InlineData(@"<div></div>
``` c
int x = 33;
```", @"<document>
<html_block>&lt;div&gt;&lt;/div&gt;
``` c
int x = 33;
```</html_block>
</document>")]
[InlineData(@"<a href=""foo"">
*bar*
</a>", @"<document>
<html_block>&lt;a href=""foo""&gt;
*bar*
&lt;/a&gt;</html_block>
</document>")]
[InlineData(@"<Warning>
*bar*
</Warning>", @"<document>
<html_block>&lt;Warning&gt;
*bar*
&lt;/Warning&gt;</html_block>
</document>")]
[InlineData(@"<i class=""foo"">
*bar*
</i>", @"<document>
<html_block>&lt;i class=""foo""&gt;
*bar*
&lt;/i&gt;</html_block>
</document>")]
[InlineData(@"</ins>
*bar*", @"<document>
<html_block>&lt;/ins&gt;
*bar*</html_block>
</document>")]
[InlineData(@"<del>
*foo*
</del>", @"<document>
<html_block>&lt;del&gt;
*foo*
&lt;/del&gt;</html_block>
</document>")]
[InlineData(@"<del>
*foo*
</del>", @"<document>
<html_block>&lt;del&gt;</html_block>
<paragraph>
<emph>
<text>foo</text>
</emph>
</paragraph>
<html_block>&lt;/del&gt;</html_block>
</document>")]
[InlineData(@"<del>*foo*</del>", @"<document>
<paragraph>
<html_inline>&lt;del&gt;</html_inline>
<emph>
<text>foo</text>
</emph>
<html_inline>&lt;/del&gt;</html_inline>
</paragraph>
</document>")]
[InlineData(@"<pre language=""haskell""><code>
import Text.HTML.TagSoup
main :: IO ()
main = print $ parseTags tags
</code></pre>
okay", @"<document>
<html_block>&lt;pre language=""haskell""&gt;&lt;code&gt;
import Text.HTML.TagSoup
main :: IO ()
main = print $ parseTags tags
&lt;/code&gt;&lt;/pre&gt;</html_block>
<paragraph>
<text>okay</text>
</paragraph>
</document>")]
[InlineData(@"<script type=""text/javascript"">
// JavaScript example
document.getElementById(""demo"").innerHTML = ""Hello JavaScript!"";
</script>
okay", @"<document>
<html_block>&lt;script type=""text/javascript""&gt;
// JavaScript example
document.getElementById(""demo"").innerHTML = ""Hello JavaScript!"";
&lt;/script&gt;</html_block>
<paragraph>
<text>okay</text>
</paragraph>
</document>")]
[InlineData(@"<textarea>
*foo*
_bar_
</textarea>", @"<document>
<html_block>&lt;textarea&gt;
*foo*
_bar_
&lt;/textarea&gt;</html_block>
</document>")]
[InlineData(@"<style
type=""text/css"">
h1 {color:red;}
p {color:blue;}
</style>
okay",@"<document>
<html_block>&lt;style
type=""text/css""&gt;
h1 {color:red;}
p {color:blue;}
&lt;/style&gt;</html_block>
<paragraph>
<text>okay</text>
</paragraph>
</document>")]
[InlineData(@"<style
type=""text/css"">
foo", @"<document>
<html_block>&lt;style
type=""text/css""&gt;
foo</html_block>
</document>")]
[InlineData(@"> <div>
> foo
bar", @"<document>
<block_quote>
<html_block>&lt;div&gt;
foo</html_block>
</block_quote>
<paragraph>
<text>bar</text>
</paragraph>
</document>")]
[InlineData(@"- <div>
- foo", @"<document>
<list type=""bullet"" tight=""true"">
<item>
<html_block>&lt;div&gt;</html_block>
</item>
<item>
<paragraph>
<text>foo</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"<style>p{color:red;}</style>
*foo*", @"<document>
<html_block>&lt;style&gt;p{color:red;}&lt;/style&gt;</html_block>
<paragraph>
<emph>
<text>foo</text>
</emph>
</paragraph>
</document>")]
[InlineData(@"<!-- foo -->*bar*
*baz*", @"<document>
<html_block>&lt;!-- foo --&gt;*bar*</html_block>
<paragraph>
<emph>
<text>baz</text>
</emph>
</paragraph>
</document>")]
[InlineData(@"<script>
foo
</script>1. *bar*", @"<document>
<html_block>&lt;script&gt;
foo
&lt;/script&gt;1. *bar*</html_block>
</document>")]
[InlineData(@"<!-- Foo
bar
baz -->
okay", @"<document>
<html_block>&lt;!-- Foo
bar
baz --&gt;</html_block>
<paragraph>
<text>okay</text>
</paragraph>
</document>")]
[InlineData(@"<?php
echo '>';
?>
okay", @"<document>
<html_block>&lt;?php
echo '&gt;';
?&gt;</html_block>
<paragraph>
<text>okay</text>
</paragraph>
</document>")]
[InlineData(@"<!DOCTYPE html>", @"<document>
<html_block>&lt;!DOCTYPE html&gt;</html_block>
</document>")]
[InlineData(@"<![CDATA[
function matchwo(a,b)
{
if (a < b && a < 0) then {
return 1;
} else {
return 0;
}
}
]]>
okay", @"<document>
<html_block>&lt;![CDATA[
function matchwo(a,b)
{
if (a &lt; b &amp;&amp; a &lt; 0) then {
return 1;
} else {
return 0;
}
}
]]&gt;</html_block>
<paragraph>
<text>okay</text>
</paragraph>
</document>")]
[InlineData(@" <!-- foo -->
<!-- foo -->", @"<document>
<html_block> &lt;!-- foo --&gt;</html_block>
<code_block>&lt;!-- foo --&gt;
</code_block>
</document>")]
[InlineData(@" <div>
<div>", @"<document>
<html_block> &lt;div&gt;</html_block>
<code_block>&lt;div&gt;
</code_block>
</document>")]
[InlineData(@"Foo
<div>
bar
</div>", @"<document>
<paragraph>
<text>Foo</text>
</paragraph>
<html_block>&lt;div&gt;
bar
&lt;/div&gt;</html_block>
</document>")]
[InlineData(@"<div>
bar
</div>
*foo*", @"<document>
<html_block>&lt;div&gt;
bar
&lt;/div&gt;
*foo*</html_block>
</document>")]
[InlineData(@"Foo
<a href=""bar"">
baz", @"<document>
<paragraph>
<text>Foo</text>
<softbreak />
<html_inline>&lt;a href=""bar""&gt;</html_inline>
<softbreak />
<text>baz</text>
</paragraph>
</document>")]
[InlineData(@"<div>
*Emphasized* text.
</div>", @"<document>
<html_block>&lt;div&gt;</html_block>
<paragraph>
<emph>
<text>Emphasized</text>
</emph>
<text> text.</text>
</paragraph>
<html_block>&lt;/div&gt;</html_block>
</document>")]
[InlineData(@"<div>
*Emphasized* text.
</div>", @"<document>
<html_block>&lt;div&gt;
*Emphasized* text.
&lt;/div&gt;</html_block>
</document>")]
[InlineData(@"<table>
<tr>
<td>
Hi
</td>
</tr>
</table>", @"<document>
<html_block>&lt;table&gt;</html_block>
<html_block>&lt;tr&gt;</html_block>
<html_block>&lt;td&gt;
Hi
&lt;/td&gt;</html_block>
<html_block>&lt;/tr&gt;</html_block>
<html_block>&lt;/table&gt;</html_block>
</document>")]
[InlineData(@"<table>
<tr>
<td>
Hi
</td>
</tr>
</table>", @"<document>
<html_block>&lt;table&gt;</html_block>
<html_block> &lt;tr&gt;</html_block>
<code_block>&lt;td&gt;
Hi
&lt;/td&gt;
</code_block>
<html_block> &lt;/tr&gt;</html_block>
<html_block>&lt;/table&gt;</html_block>
</document>")]
public void Parse_HtmlBlock(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
}

View File

@@ -0,0 +1,144 @@
using Xunit;
namespace Radzen.Blazor.Markdown.Tests;
public class HtmlInlineTests
{
private static string ToXml(string markdown)
{
var document = MarkdownParser.Parse(markdown);
return XmlVisitor.ToXml(document);
}
[Theory]
[InlineData(@"<a><bab><c2c>", @"<document>
<paragraph>
<html_inline>&lt;a&gt;</html_inline>
<html_inline>&lt;bab&gt;</html_inline>
<html_inline>&lt;c2c&gt;</html_inline>
</paragraph>
</document>")]
[InlineData(@"<a/><b2/>", @"<document>
<paragraph>
<html_inline>&lt;a/&gt;</html_inline>
<html_inline>&lt;b2/&gt;</html_inline>
</paragraph>
</document>")]
[InlineData(@"<a /><b2
data=""foo"" >", @"<document>
<paragraph>
<html_inline>&lt;a /&gt;</html_inline>
<html_inline>&lt;b2
data=""foo"" &gt;</html_inline>
</paragraph>
</document>")]
[InlineData(@"<a foo=""bar"" bam = 'baz <em>""</em>'
_boolean zoop:33=zoop:33 />", @"<document>
<paragraph>
<html_inline>&lt;a foo=""bar"" bam = 'baz &lt;em&gt;""&lt;/em&gt;'
_boolean zoop:33=zoop:33 /&gt;</html_inline>
</paragraph>
</document>")]
[InlineData(@"Foo <responsive-image src=""foo.jpg"" />", @"<document>
<paragraph>
<text>Foo </text>
<html_inline>&lt;responsive-image src=""foo.jpg"" /&gt;</html_inline>
</paragraph>
</document>")]
[InlineData("<33> <__>", @"<document>
<paragraph>
<text>&lt;33&gt; &lt;</text>
<text>__</text>
<text>&gt;</text>
</paragraph>
</document>")]
[InlineData(@"<a h*#ref=""hi"">", @"<document>
<paragraph>
<text>&lt;a h</text>
<text>*</text>
<text>#ref=""hi""&gt;</text>
</paragraph>
</document>")]
[InlineData(@"<a href='bar'title=title>", @"<document>
<paragraph>
<text>&lt;a href='bar'title=title&gt;</text>
</paragraph>
</document>")]
[InlineData(@"</a></foo >", @"<document>
<paragraph>
<html_inline>&lt;/a&gt;</html_inline>
<html_inline>&lt;/foo &gt;</html_inline>
</paragraph>
</document>")]
[InlineData(@"</a href=""foo"">", @"<document>
<paragraph>
<text>&lt;/a href=""foo""&gt;</text>
</paragraph>
</document>")]
[InlineData(@"foo <!-- this is a --
comment - with hyphens -->", @"<document>
<paragraph>
<text>foo </text>
<html_inline>&lt;!-- this is a --
comment - with hyphens --&gt;</html_inline>
</paragraph>
</document>")]
[InlineData(@"foo <!--> foo -->
foo <!---> foo -->
", @"<document>
<paragraph>
<text>foo </text>
<html_inline>&lt;!--&gt;</html_inline>
<text> foo --&gt;</text>
</paragraph>
<paragraph>
<text>foo </text>
<html_inline>&lt;!---&gt;</html_inline>
<text> foo --&gt;</text>
</paragraph>
</document>")]
[InlineData(@"foo <?php echo $a; ?>", @"<document>
<paragraph>
<text>foo </text>
<html_inline>&lt;?php echo $a; ?&gt;</html_inline>
</paragraph>
</document>")]
[InlineData(@"foo <!ELEMENT br EMPTY>", @"<document>
<paragraph>
<text>foo </text>
<html_inline>&lt;!ELEMENT br EMPTY&gt;</html_inline>
</paragraph>
</document>")]
[InlineData(@"foo <![CDATA[>&<]]>", @"<document>
<paragraph>
<text>foo </text>
<html_inline>&lt;![CDATA[&gt;&amp;&lt;]]&gt;</html_inline>
</paragraph>
</document>")]
[InlineData(@"foo <a href=""&ouml;"">", @"<document>
<paragraph>
<text>foo </text>
<html_inline>&lt;a href=""&amp;ouml;""&gt;</html_inline>
</paragraph>
</document>")]
[InlineData(@"foo <a href=""\*"">", @"<document>
<paragraph>
<text>foo </text>
<html_inline>&lt;a href=""\*""&gt;</html_inline>
</paragraph>
</document>")]
[InlineData(@"foo <a href=""\"""">", @"<document>
<paragraph>
<text>foo &lt;a href=""</text>
<text>""</text>
<text>""&gt;</text>
</paragraph>
</document>")]
public void Parse_Html(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
}

View File

@@ -0,0 +1,85 @@
using Xunit;
namespace Radzen.Blazor.Markdown.Tests;
public class ImageTests
{
private static string ToXml(string markdown)
{
var document = MarkdownParser.Parse(markdown);
return XmlVisitor.ToXml(document);
}
[Theory]
[InlineData(@"![foo](/url ""title"")", @"<document>
<paragraph>
<image destination=""/url"" title=""title"">
<text>foo</text>
</image>
</paragraph>
</document>")]
[InlineData(@"![foo ![bar](/url)](/url2)", @"<document>
<paragraph>
<image destination=""/url2"" title="""">
<text>foo </text>
<image destination=""/url"" title="""">
<text>bar</text>
</image>
</image>
</paragraph>
</document>")]
[InlineData(@"![foo [bar](/url)](/url2)", @"<document>
<paragraph>
<image destination=""/url2"" title="""">
<text>foo </text>
<link destination=""/url"" title="""">
<text>bar</text>
</link>
</image>
</paragraph>
</document>")]
[InlineData(@"![foo](train.jpg)", @"<document>
<paragraph>
<image destination=""train.jpg"" title="""">
<text>foo</text>
</image>
</paragraph>
</document>")]
[InlineData(@"My ![foo bar](/path/to/train.jpg ""title"" )", @"<document>
<paragraph>
<text>My </text>
<image destination=""/path/to/train.jpg"" title=""title"">
<text>foo bar</text>
</image>
</paragraph>
</document>")]
[InlineData(@"![foo](<url>)", @"<document>
<paragraph>
<image destination=""url"" title="""">
<text>foo</text>
</image>
</paragraph>
</document>")]
[InlineData(@"![](/url)", @"<document>
<paragraph>
<image destination=""/url"" title="""" />
</paragraph>
</document>")]
[InlineData(@"__Applications__ ![macOS DMG](/assets/img/macOS-drag-and-drop.png)", @"<document>
<paragraph>
<strong>
<text>Applications</text>
</strong>
<text> </text>
<image destination=""/assets/img/macOS-drag-and-drop.png"" title="""">
<text>macOS DMG</text>
</image>
</paragraph>
</document>")]
public void Parse_BasicImages_ReturnsImageElement(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
}

View File

@@ -0,0 +1,145 @@
using Xunit;
namespace Radzen.Blazor.Markdown.Tests;
public class IndentedCodeBlockTests
{
private static string ToXml(string markdown)
{
var document = MarkdownParser.Parse(markdown);
return XmlVisitor.ToXml(document);
}
[Theory]
[InlineData(@" a simple
indented code block
", @"<document>
<code_block>a simple
indented code block
</code_block>
</document>")]
[InlineData(@" - foo
bar", @"<document>
<list type=""bullet"" tight=""false"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
<paragraph>
<text>bar</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(
@"
1. foo
- bar", @"<document>
<list type=""ordered"" start=""1"" tight=""false"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>bar</text>
</paragraph>
</item>
</list>
</item>
</list>
</document>")]
[InlineData(@" <a/>
*hi*
- one", @"<document>
<code_block>&lt;a/&gt;
*hi*
- one
</code_block>
</document>")]
[InlineData(@" chunk1
chunk2
chunk3", @"<document>
<code_block>chunk1
chunk2
chunk3
</code_block>
</document>")]
[InlineData(@" chunk1
chunk2", @"<document>
<code_block>chunk1
chunk2
</code_block>
</document>")]
[InlineData(@"Foo
bar", @"<document>
<paragraph>
<text>Foo</text>
<softbreak />
<text>bar</text>
</paragraph>
</document>")]
[InlineData(@" foo
bar", @"<document>
<code_block>foo
</code_block>
<paragraph>
<text>bar</text>
</paragraph>
</document>")]
[InlineData(@"# Heading
foo
Heading
------
foo
----", @"<document>
<heading level=""1"">
<text>Heading</text>
</heading>
<code_block>foo
</code_block>
<heading level=""2"">
<text>Heading</text>
</heading>
<code_block>foo
</code_block>
<thematic_break />
</document>")]
[InlineData(@" foo
bar", @"<document>
<code_block> foo
bar
</code_block>
</document>")]
[InlineData(@"
foo
", @"<document>
<code_block>foo
</code_block>
</document>")]
[InlineData(@" foo ", @"<document>
<code_block>foo
</code_block>
</document>")]
public void Parse_IndentedCodeBlock(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
}

View File

@@ -0,0 +1,852 @@
using System.Runtime.InteropServices;
using Xunit;
namespace Radzen.Blazor.Markdown.Tests;
public class LinkTests
{
private static string ToXml(string markdown)
{
var document = MarkdownParser.Parse(markdown);
return XmlVisitor.ToXml(document);
}
[Theory]
[InlineData("[link](/uri \"title\")",
@"<document>
<paragraph>
<link destination=""/uri"" title=""title"">
<text>link</text>
</link>
</paragraph>
</document>")]
[InlineData("[link](/uri)", @"<document>
<paragraph>
<link destination=""/uri"" title="""">
<text>link</text>
</link>
</paragraph>
</document>")]
[InlineData("[](./target.md)", @"<document>
<paragraph>
<link destination=""./target.md"" title="""" />
</paragraph>
</document>")]
[InlineData("[link]()", @"<document>
<paragraph>
<link destination="""" title="""">
<text>link</text>
</link>
</paragraph>
</document>")]
[InlineData("[link](<>)", @"<document>
<paragraph>
<link destination="""" title="""">
<text>link</text>
</link>
</paragraph>
</document>")]
[InlineData("[]()", @"<document>
<paragraph>
<link destination="""" title="""" />
</paragraph>
</document>")]
public void Parse_BasicLinks_ReturnsLinkNode(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData("[link](/my uri)", @"<document>
<paragraph>
<text>[</text>
<text>link</text>
<text>](/my uri)</text>
</paragraph>
</document>")]
[InlineData("[link](</my uri>)", @"<document>
<paragraph>
<link destination=""/my uri"" title="""">
<text>link</text>
</link>
</paragraph>
</document>")]
public void Parse_LinkDestinationWithSpaces(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData(@"[link](foo
bar)", @"<document>
<paragraph>
<text>[</text>
<text>link</text>
<text>](foo</text>
<softbreak />
<text>bar)</text>
</paragraph>
</document>")]
public void Parse_LinkDestinationWithNewLines(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData(@"[a](<b)c>)", @"<document>
<paragraph>
<link destination=""b)c"" title="""">
<text>a</text>
</link>
</paragraph>
</document>")]
public void Parse_LinkDestinationWithCloseParenthesis(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData(@"[link](<foo\>)", @"<document>
<paragraph>
<text>[</text>
<text>link</text>
<text>](&lt;foo&gt;)</text>
</paragraph>
</document>")]
[InlineData(@"[a](<b)c
[a](<b)c>
[a](<b>c)", @"<document>
<paragraph>
<text>[</text>
<text>a</text>
<text>](&lt;b)c</text>
<softbreak />
<text>[</text>
<text>a</text>
<text>](&lt;b)c&gt;</text>
<softbreak />
<text>[</text>
<text>a</text>
<text>](</text>
<html_inline>&lt;b&gt;</html_inline>
<text>c)</text>
</paragraph>
</document>")]
public void Parse_LinkDestinationUnclosedPointyBracket(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData(@"[link](\(foo\))", @"<document>
<paragraph>
<link destination=""(foo)"" title="""">
<text>link</text>
</link>
</paragraph>
</document>")]
[InlineData(@"[link](foo\)\:)", @"<document>
<paragraph>
<link destination=""foo):"" title="""">
<text>link</text>
</link>
</paragraph>
</document>")]
[InlineData(@"[link](foo\bar)", @"<document>
<paragraph>
<link destination=""foo\bar"" title="""">
<text>link</text>
</link>
</paragraph>
</document>")]
public void Parse_Escapes(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData(@"[link](foo(and(bar)))", @"<document>
<paragraph>
<link destination=""foo(and(bar))"" title="""">
<text>link</text>
</link>
</paragraph>
</document>")]
[InlineData(@"[link](foo(and(bar))", @"<document>
<paragraph>
<text>[</text>
<text>link</text>
<text>](foo(and(bar))</text>
</paragraph>
</document>")]
[InlineData(@"[link](foo\(and\(bar\))", @"<document>
<paragraph>
<link destination=""foo(and(bar)"" title="""">
<text>link</text>
</link>
</paragraph>
</document>")]
[InlineData(@"[link](<foo(and(bar)>)", @"<document>
<paragraph>
<link destination=""foo(and(bar)"" title="""">
<text>link</text>
</link>
</paragraph>
</document>")]
public void Parse_BallancedParenthesisInLinkDestination(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData(@"[link](#fragment)
[link](https://example.com#fragment)
[link](https://example.com?foo=3#frag)", @"<document>
<paragraph>
<link destination=""#fragment"" title="""">
<text>link</text>
</link>
<softbreak />
<link destination=""https://example.com#fragment"" title="""">
<text>link</text>
</link>
<softbreak />
<link destination=""https://example.com?foo=3#frag"" title="""">
<text>link</text>
</link>
</paragraph>
</document>")]
public void Parse_FragmentAndQueryStringInLinkDestination(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData(@"[link](""title"")", @"<document>
<paragraph>
<link destination=""&quot;title&quot;"" title="""">
<text>link</text>
</link>
</paragraph>
</document>")]
public void Parse_QuotesInDestination(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData(@"[link](/url ""title"")
[link](/url 'title')
[link](/url (title))", @"<document>
<paragraph>
<link destination=""/url"" title=""title"">
<text>link</text>
</link>
<softbreak />
<link destination=""/url"" title=""title"">
<text>link</text>
</link>
<softbreak />
<link destination=""/url"" title=""title"">
<text>link</text>
</link>
</paragraph>
</document>")]
[InlineData(@"[link](/url ""title ""and"" title"")", @"<document>
<paragraph>
<text>[</text>
<text>link</text>
<text>](/url ""title ""and"" title"")</text>
</paragraph>
</document>")]
[InlineData(@"[link](/url 'title ""and"" title')", @"<document>
<paragraph>
<link destination=""/url"" title=""title &quot;and&quot; title"">
<text>link</text>
</link>
</paragraph>
</document>")]
public void Parse_Title(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData(@"[link]( /url
""title"" )", @"<document>
<paragraph>
<link destination=""/url"" title=""title"">
<text>link</text>
</link>
</paragraph>
</document>")]
[InlineData(@"[link] (/uri)", @"<document>
<paragraph>
<text>[</text>
<text>link</text>
<text>] (/uri)</text>
</paragraph>
</document>")]
public void Parse_SpacesInLink(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData(@"[link [foo [bar]]](/uri)", @"<document>
<paragraph>
<link destination=""/uri"" title="""">
<text>link </text>
<text>[</text>
<text>foo </text>
<text>[</text>
<text>bar</text>
<text>]</text>
<text>]</text>
</link>
</paragraph>
</document>")]
[InlineData(@"[link] bar](/uri)", @"<document>
<paragraph>
<text>[</text>
<text>link</text>
<text>] bar](/uri)</text>
</paragraph>
</document>")]
[InlineData(@"[link [bar](/uri)", @"<document>
<paragraph>
<text>[</text>
<text>link </text>
<link destination=""/uri"" title="""">
<text>bar</text>
</link>
</paragraph>
</document>")]
[InlineData(@"[link \[bar](/uri)", @"<document>
<paragraph>
<link destination=""/uri"" title="""">
<text>link </text>
<text>[</text>
<text>bar</text>
</link>
</paragraph>
</document>")]
public void Parse_BracketsInText(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData(@"[link *foo **bar** `#`*](/uri)", @"<document>
<paragraph>
<link destination=""/uri"" title="""">
<text>link </text>
<emph>
<text>foo </text>
<strong>
<text>bar</text>
</strong>
<text> </text>
<code>#</code>
</emph>
</link>
</paragraph>
</document>")]
[InlineData(@"[![alt](img)](url)", @"<document>
<paragraph>
<link destination=""url"" title="""">
<image destination=""img"" title="""">
<text>alt</text>
</image>
</link>
</paragraph>
</document>")]
public void Parse_LinkTextIsInlineContent(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData(@"[foo [bar](/uri)](/uri)", @"<document>
<paragraph>
<text>[</text>
<text>foo </text>
<link destination=""/uri"" title="""">
<text>bar</text>
</link>
<text>](/uri)</text>
</paragraph>
</document>")]
[InlineData(@"[foo *[bar [baz](/uri)](/uri)*](/uri)", @"<document>
<paragraph>
<text>[</text>
<text>foo </text>
<emph>
<text>[</text>
<text>bar </text>
<link destination=""/uri"" title="""">
<text>baz</text>
</link>
<text>](/uri)</text>
</emph>
<text>](/uri)</text>
</paragraph>
</document>")]
public void Parse_NestedLinks(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData(@"*[foo*](/uri)", @"<document>
<paragraph>
<text>*</text>
<link destination=""/uri"" title="""">
<text>foo</text>
<text>*</text>
</link>
</paragraph>
</document>")]
[InlineData(@"[foo *bar](baz*)", @"<document>
<paragraph>
<link destination=""baz*"" title="""">
<text>foo </text>
<text>*</text>
<text>bar</text>
</link>
</paragraph>
</document>")]
[InlineData(@"*foo [bar* baz]", @"<document>
<paragraph>
<emph>
<text>foo </text>
<text>[</text>
<text>bar</text>
</emph>
<text> baz</text>
<text>]</text>
</paragraph>
</document>")]
[InlineData(@"[foo`](/uri)`", @"<document>
<paragraph>
<text>[</text>
<text>foo</text>
<code>](/uri)</code>
</paragraph>
</document>")]
public void Parse_Precedence(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData(@"<http://foo.bar.baz>", @"<document>
<paragraph>
<link destination=""http://foo.bar.baz"" title="""">
<text>http://foo.bar.baz</text>
</link>
</paragraph>
</document>")]
[InlineData(@"<https://foo.bar.baz/test?q=hello&id=22&boolean>", @"<document>
<paragraph>
<link destination=""https://foo.bar.baz/test?q=hello&amp;id=22&amp;boolean"" title="""">
<text>https://foo.bar.baz/test?q=hello&amp;id=22&amp;boolean</text>
</link>
</paragraph>
</document>")]
[InlineData(@"<irc://foo.bar:2233/baz>", @"<document>
<paragraph>
<link destination=""irc://foo.bar:2233/baz"" title="""">
<text>irc://foo.bar:2233/baz</text>
</link>
</paragraph>
</document>")]
[InlineData(@"<MAILTO:FOO@BAR.BAZ>", @"<document>
<paragraph>
<link destination=""MAILTO:FOO@BAR.BAZ"" title="""">
<text>MAILTO:FOO@BAR.BAZ</text>
</link>
</paragraph>
</document>")]
[InlineData(@"<https://foo.bar/baz bim>", @"<document>
<paragraph>
<text>&lt;https://foo.bar/baz bim&gt;</text>
</paragraph>
</document>")]
[InlineData(@"<https://example.com/\[\>", @"<document>
<paragraph>
<link destination=""https://example.com/\[\"" title="""">
<text>https://example.com/\[\</text>
</link>
</paragraph>
</document>")]
[InlineData(@"<foo@bar.example.com>", @"<document>
<paragraph>
<link destination=""mailto:foo@bar.example.com"" title="""">
<text>foo@bar.example.com</text>
</link>
</paragraph>
</document>")]
[InlineData(@"<foo+special@Bar.baz-bar0.com>", @"<document>
<paragraph>
<link destination=""mailto:foo+special@Bar.baz-bar0.com"" title="""">
<text>foo+special@Bar.baz-bar0.com</text>
</link>
</paragraph>
</document>")]
[InlineData(@"<foo\+@bar.example.com>", @"<document>
<paragraph>
<text>&lt;foo+@bar.example.com&gt;</text>
</paragraph>
</document>")]
[InlineData(@"<>", @"<document>
<paragraph>
<text>&lt;&gt;</text>
</paragraph>
</document>")]
[InlineData(@"< https://foo.bar >", @"<document>
<paragraph>
<text>&lt; https://foo.bar &gt;</text>
</paragraph>
</document>")]
[InlineData(@"<m:abc>", @"<document>
<paragraph>
<text>&lt;m:abc&gt;</text>
</paragraph>
</document>")]
[InlineData(@"<foo.bar.baz>", @"<document>
<paragraph>
<text>&lt;foo.bar.baz&gt;</text>
</paragraph>
</document>")]
[InlineData(@"https://example.com", @"<document>
<paragraph>
<text>https://example.com</text>
</paragraph>
</document>")]
[InlineData(@"foo@bar.example.com", @"<document>
<paragraph>
<text>foo@bar.example.com</text>
</paragraph>
</document>")]
public void Parse_AutoLink(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData(@"[foo]: /url ""title""
[foo]", @"<document>
<paragraph>
<link destination=""/url"" title=""title"">
<text>foo</text>
</link>
</paragraph>
</document>")]
[InlineData(@" [foo]:
/url
'the title'
[foo]", @"<document>
<paragraph>
<link destination=""/url"" title=""the title"">
<text>foo</text>
</link>
</paragraph>
</document>")]
[InlineData(@"[Foo*bar\]]:my_(url) 'title (with parens)'
[Foo*bar\]]", @"<document>
<paragraph>
<link destination=""my_(url)"" title=""title (with parens)"">
<text>Foo</text>
<text>*</text>
<text>bar</text>
<text>]</text>
</link>
</paragraph>
</document>")]
[InlineData(@"[Foo bar]:
<my url>
'title'
[Foo bar]", @"<document>
<paragraph>
<link destination=""my url"" title=""title"">
<text>Foo bar</text>
</link>
</paragraph>
</document>")]
[InlineData(@"[foo]: /url '
title
line1
line2
'
[foo]", @"<document>
<paragraph>
<link destination=""/url"" title=""&#xA;title&#xA;line1&#xA;line2&#xA;"">
<text>foo</text>
</link>
</paragraph>
</document>")]
[InlineData(@"[foo]: /url 'title
with blank line'
[foo]", @"<document>
<paragraph>
<text>[</text>
<text>foo</text>
<text>]: /url 'title</text>
</paragraph>
<paragraph>
<text>with blank line'</text>
</paragraph>
<paragraph>
<text>[</text>
<text>foo</text>
<text>]</text>
</paragraph>
</document>")]
[InlineData(@"[foo]:
/url
[foo]", @"<document>
<paragraph>
<link destination=""/url"" title="""">
<text>foo</text>
</link>
</paragraph>
</document>")]
[InlineData(@"[foo]:
[foo]", @"<document>
<paragraph>
<text>[</text>
<text>foo</text>
<text>]:</text>
</paragraph>
<paragraph>
<text>[</text>
<text>foo</text>
<text>]</text>
</paragraph>
</document>")]
[InlineData(@"[foo]: <>
[foo]", @"<document>
<paragraph>
<link destination="""" title="""">
<text>foo</text>
</link>
</paragraph>
</document>")]
[InlineData(@"[foo]: <bar>(baz)
[foo]", @"<document>
<paragraph>
<text>[</text>
<text>foo</text>
<text>]: </text>
<html_inline>&lt;bar&gt;</html_inline>
<text>(baz)</text>
</paragraph>
<paragraph>
<text>[</text>
<text>foo</text>
<text>]</text>
</paragraph>
</document>")]
[InlineData(@"[foo]: /url\bar\*baz ""foo\""bar\baz""
[foo]
", @"<document>
<paragraph>
<link destination=""/url\bar*baz"" title=""foo\&quot;bar\baz"">
<text>foo</text>
</link>
</paragraph>
</document>")]
[InlineData(@"[foo]
[foo]: url", @"<document>
<paragraph>
<link destination=""url"" title="""">
<text>foo</text>
</link>
</paragraph>
</document>")]
[InlineData(@"[foo]
[foo]: first
[foo]: second", @"<document>
<paragraph>
<link destination=""first"" title="""">
<text>foo</text>
</link>
</paragraph>
</document>")]
[InlineData(@"[FOO]: /url
[Foo]", @"<document>
<paragraph>
<link destination=""/url"" title="""">
<text>Foo</text>
</link>
</paragraph>
</document>")]
[InlineData(@"[ΑΓΩ]: /φου
[αγω]", @"<document>
<paragraph>
<link destination=""/φου"" title="""">
<text>αγω</text>
</link>
</paragraph>
</document>")]
[InlineData(@"[foo]: /url", @"<document />")]
[InlineData(@"[
foo
]: /url
bar", @"<document>
<paragraph>
<text>bar</text>
</paragraph>
</document>")]
[InlineData(@"[foo]: /url ""title"" ok", @"<document>
<paragraph>
<text>[</text>
<text>foo</text>
<text>]: /url ""title"" ok</text>
</paragraph>
</document>")]
[InlineData(@"[foo]: /url
""title"" ok", @"<document>
<paragraph>
<text>""title"" ok</text>
</paragraph>
</document>")]
[InlineData(@" [foo]: /url ""title""
[foo]", @"<document>
<code_block>[foo]: /url ""title""
</code_block>
<paragraph>
<text>[</text>
<text>foo</text>
<text>]</text>
</paragraph>
</document>")]
[InlineData(@"```
[foo]: /url
```
[foo]", @"<document>
<code_block>[foo]: /url
</code_block>
<paragraph>
<text>[</text>
<text>foo</text>
<text>]</text>
</paragraph>
</document>")]
[InlineData(@"Foo
[bar]: /baz
[bar]", @"<document>
<paragraph>
<text>Foo</text>
<softbreak />
<text>[</text>
<text>bar</text>
<text>]: /baz</text>
</paragraph>
<paragraph>
<text>[</text>
<text>bar</text>
<text>]</text>
</paragraph>
</document>")]
[InlineData(@"# [Foo]
[foo]: /url
> bar", @"<document>
<heading level=""1"">
<link destination=""/url"" title="""">
<text>Foo</text>
</link>
</heading>
<block_quote>
<paragraph>
<text>bar</text>
</paragraph>
</block_quote>
</document>")]
[InlineData(@"[foo]: /url
bar
===
[foo]", @"<document>
<heading level=""1"">
<text>bar</text>
</heading>
<paragraph>
<link destination=""/url"" title="""">
<text>foo</text>
</link>
</paragraph>
</document>")]
[InlineData(@"[foo]: /url
===
[foo]", @"<document>
<paragraph>
<text>===</text>
<softbreak />
<link destination=""/url"" title="""">
<text>foo</text>
</link>
</paragraph>
</document>")]
[InlineData(@"[foo]: /foo-url ""foo""
[bar]: /bar-url
""bar""
[baz]: /baz-url
[foo],
[bar],
[baz]", @"<document>
<paragraph>
<link destination=""/foo-url"" title=""foo"">
<text>foo</text>
</link>
<text>,</text>
<softbreak />
<link destination=""/bar-url"" title=""bar"">
<text>bar</text>
</link>
<text>,</text>
<softbreak />
<link destination=""/baz-url"" title="""">
<text>baz</text>
</link>
</paragraph>
</document>")]
[InlineData(@"[foo]
> [foo]: /url
", @"<document>
<paragraph>
<link destination=""/url"" title="""">
<text>foo</text>
</link>
</paragraph>
<block_quote />
</document>")]
public void Parse_LinkReference(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
}

View File

@@ -0,0 +1,880 @@
using Xunit;
namespace Radzen.Blazor.Markdown.Tests;
public class ListItemTests
{
private static string ToXml(string markdown)
{
var document = MarkdownParser.Parse(markdown);
return XmlVisitor.ToXml(document);
}
[Theory]
[InlineData(@"1. A paragraph
with two lines.
indented code
> A block quote.", @"<document>
<list type=""ordered"" start=""1"" tight=""false"">
<item>
<paragraph>
<text>A paragraph</text>
<softbreak />
<text>with two lines.</text>
</paragraph>
<code_block>indented code
</code_block>
<block_quote>
<paragraph>
<text>A block quote.</text>
</paragraph>
</block_quote>
</item>
</list>
</document>")]
[InlineData(@"- one
two", @"<document>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>one</text>
</paragraph>
</item>
</list>
<paragraph>
<text>two</text>
</paragraph>
</document>")]
[InlineData(@"- one
two", @"<document>
<list type=""bullet"" tight=""false"">
<item>
<paragraph>
<text>one</text>
</paragraph>
<paragraph>
<text>two</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@" - one
two", @"<document>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>one</text>
</paragraph>
</item>
</list>
<code_block> two
</code_block>
</document>")]
[InlineData(@" - one
two", @"<document>
<list type=""bullet"" tight=""false"">
<item>
<paragraph>
<text>one</text>
</paragraph>
<paragraph>
<text>two</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@" > > 1. one
>>
>> two
", @"<document>
<block_quote>
<block_quote>
<list type=""ordered"" start=""1"" tight=""false"">
<item>
<paragraph>
<text>one</text>
</paragraph>
<paragraph>
<text>two</text>
</paragraph>
</item>
</list>
</block_quote>
</block_quote>
</document>")]
[InlineData(@">>- one
>>
> > two
", @"<document>
<block_quote>
<block_quote>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>one</text>
</paragraph>
</item>
</list>
<paragraph>
<text>two</text>
</paragraph>
</block_quote>
</block_quote>
</document>")]
[InlineData(@"-one
2.two
", @"<document>
<paragraph>
<text>-one</text>
</paragraph>
<paragraph>
<text>2.two</text>
</paragraph>
</document>")]
[InlineData(@"- foo
bar", @"<document>
<list type=""bullet"" tight=""false"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
<paragraph>
<text>bar</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"1. foo
```
bar
```
baz
> bam", @"<document>
<list type=""ordered"" start=""1"" tight=""false"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
<code_block>bar
</code_block>
<paragraph>
<text>baz</text>
</paragraph>
<block_quote>
<paragraph>
<text>bam</text>
</paragraph>
</block_quote>
</item>
</list>
</document>")]
[InlineData(@"- Foo
bar
baz", @"<document>
<list type=""bullet"" tight=""false"">
<item>
<paragraph>
<text>Foo</text>
</paragraph>
<code_block>bar
baz
</code_block>
</item>
</list>
</document>")]
[InlineData(@"123456789. ok", @"<document>
<list type=""ordered"" start=""123456789"" tight=""true"">
<item>
<paragraph>
<text>ok</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"1234567890. not ok", @"<document>
<paragraph>
<text>1234567890. not ok</text>
</paragraph>
</document>")]
[InlineData(@"0. ok", @"<document>
<list type=""ordered"" start=""0"" tight=""true"">
<item>
<paragraph>
<text>ok</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"003. ok", @"<document>
<list type=""ordered"" start=""3"" tight=""true"">
<item>
<paragraph>
<text>ok</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"-1. not ok", @"<document>
<paragraph>
<text>-1. not ok</text>
</paragraph>
</document>")]
[InlineData(@"- foo
bar", @"<document>
<list type=""bullet"" tight=""false"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
<code_block>bar
</code_block>
</item>
</list>
</document>")]
[InlineData(@" 10. foo
bar", @"<document>
<list type=""ordered"" start=""10"" tight=""false"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
<code_block>bar
</code_block>
</item>
</list>
</document>")]
[InlineData(@"1. indented code
paragraph
more code", @"<document>
<list type=""ordered"" start=""1"" tight=""false"">
<item>
<code_block>indented code
</code_block>
<paragraph>
<text>paragraph</text>
</paragraph>
<code_block>more code
</code_block>
</item>
</list>
</document>")]
[InlineData(@"- indented code
paragraph
more code", @"<document>
<list type=""bullet"" tight=""false"">
<item>
<code_block>indented code
</code_block>
<paragraph>
<text>paragraph</text>
</paragraph>
<code_block>more code
</code_block>
</item>
</list>
</document>")]
[InlineData(@"1. indented code
paragraph
more code", @"<document>
<list type=""ordered"" start=""1"" tight=""false"">
<item>
<code_block> indented code
</code_block>
<paragraph>
<text>paragraph</text>
</paragraph>
<code_block>more code
</code_block>
</item>
</list>
</document>")]
[InlineData(@"- foo
bar", @"<document>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
</item>
</list>
<paragraph>
<text>bar</text>
</paragraph>
</document>")]
[InlineData(@"- foo
bar", @"<document>
<list type=""bullet"" tight=""false"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
<paragraph>
<text>bar</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"-
foo
-
```
bar
```
-
baz", @"<document>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
</item>
<item>
<code_block>bar
</code_block>
</item>
<item>
<code_block>baz
</code_block>
</item>
</list>
</document>")]
[InlineData(@"1.
foo", @"<document>
<list type=""ordered"" start=""1"" tight=""true"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"-
foo", @"<document>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"-
foo", @"<document>
<list type=""bullet"" tight=""true"">
<item />
</list>
<paragraph>
<text>foo</text>
</paragraph>
</document>")]
[InlineData(@"- foo
-
- bar", @"<document>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
</item>
<item />
<item>
<paragraph>
<text>bar</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"- foo
-
- bar", @"<document>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
</item>
<item />
<item>
<paragraph>
<text>bar</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"1. foo
2.
3. bar", @"<document>
<list type=""ordered"" start=""1"" tight=""true"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
</item>
<item />
<item>
<paragraph>
<text>bar</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"*", @"<document>
<list type=""bullet"" tight=""true"">
<item />
</list>
</document>")]
[InlineData(@"foo
*
foo
1.", @"<document>
<paragraph>
<text>foo</text>
<softbreak />
<text>*</text>
</paragraph>
<paragraph>
<text>foo</text>
<softbreak />
<text>1.</text>
</paragraph>
</document>")]
[InlineData(@" 1. A paragraph
with two lines.
indented code
> A block quote.", @"<document>
<list type=""ordered"" start=""1"" tight=""false"">
<item>
<paragraph>
<text>A paragraph</text>
<softbreak />
<text>with two lines.</text>
</paragraph>
<code_block>indented code
</code_block>
<block_quote>
<paragraph>
<text>A block quote.</text>
</paragraph>
</block_quote>
</item>
</list>
</document>")]
[InlineData(@" 1. A paragraph
with two lines.
indented code
> A block quote.", @"<document>
<list type=""ordered"" start=""1"" tight=""false"">
<item>
<paragraph>
<text>A paragraph</text>
<softbreak />
<text>with two lines.</text>
</paragraph>
<code_block>indented code
</code_block>
<block_quote>
<paragraph>
<text>A block quote.</text>
</paragraph>
</block_quote>
</item>
</list>
</document>")]
[InlineData(@" 1. A paragraph
with two lines.
indented code
> A block quote.", @"<document>
<list type=""ordered"" start=""1"" tight=""false"">
<item>
<paragraph>
<text>A paragraph</text>
<softbreak />
<text>with two lines.</text>
</paragraph>
<code_block>indented code
</code_block>
<block_quote>
<paragraph>
<text>A block quote.</text>
</paragraph>
</block_quote>
</item>
</list>
</document>")]
[InlineData(@" 1. A paragraph
with two lines.
indented code
> A block quote.", @"<document>
<code_block>1. A paragraph
with two lines.
indented code
&gt; A block quote.
</code_block>
</document>")]
public void Parse_ListItem(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData(@" 1. A paragraph
with two lines.
indented code
> A block quote.", @"<document>
<list type=""ordered"" start=""1"" tight=""false"">
<item>
<paragraph>
<text>A paragraph</text>
<softbreak />
<text>with two lines.</text>
</paragraph>
<paragraph>
<text>indented code</text>
</paragraph>
</item>
</list>
<code_block> &gt; A block quote.
</code_block>
</document>")]
[InlineData(@" 1. A paragraph
with two lines.", @"<document>
<list type=""ordered"" start=""1"" tight=""true"">
<item>
<paragraph>
<text>A paragraph</text>
<softbreak />
<text>with two lines.</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"> 1. > Blockquote
continued here.", @"<document>
<block_quote>
<list type=""ordered"" start=""1"" tight=""true"">
<item>
<block_quote>
<paragraph>
<text>Blockquote</text>
<softbreak />
<text>continued here.</text>
</paragraph>
</block_quote>
</item>
</list>
</block_quote>
</document>")]
[InlineData(@"> 1. > Blockquote
> continued here.", @"<document>
<block_quote>
<list type=""ordered"" start=""1"" tight=""true"">
<item>
<block_quote>
<paragraph>
<text>Blockquote</text>
<softbreak />
<text>continued here.</text>
</paragraph>
</block_quote>
</item>
</list>
</block_quote>
</document>")]
public void Parse_ListItem_WithLazyContinuation(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData(@"- foo
- bar
- baz
- boo", @"<document>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>bar</text>
</paragraph>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>baz</text>
</paragraph>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>boo</text>
</paragraph>
</item>
</list>
</item>
</list>
</item>
</list>
</item>
</list>
</document>")]
[InlineData(@"- foo
- bar
- baz
- boo", @"<document>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
</item>
<item>
<paragraph>
<text>bar</text>
</paragraph>
</item>
<item>
<paragraph>
<text>baz</text>
</paragraph>
</item>
<item>
<paragraph>
<text>boo</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"1. foo
1. bar
1. baz
1. boo", @"<document>
<list type=""ordered"" start=""1"" tight=""true"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
</item>
<item>
<paragraph>
<text>bar</text>
</paragraph>
</item>
<item>
<paragraph>
<text>baz</text>
</paragraph>
</item>
<item>
<paragraph>
<text>boo</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"
1. foo
1. bar
1. baz
1. boo", @"<document>
<list type=""ordered"" start=""1"" tight=""true"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
<list type=""ordered"" start=""1"" tight=""true"">
<item>
<paragraph>
<text>bar</text>
</paragraph>
<list type=""ordered"" start=""1"" tight=""true"">
<item>
<paragraph>
<text>baz</text>
</paragraph>
<list type=""ordered"" start=""1"" tight=""true"">
<item>
<paragraph>
<text>boo</text>
</paragraph>
</item>
</list>
</item>
</list>
</item>
</list>
</item>
</list>
</document>")]
[InlineData(@"10) foo
- bar", @"<document>
<list type=""ordered"" start=""10"" tight=""true"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>bar</text>
</paragraph>
</item>
</list>
</item>
</list>
</document>")]
[InlineData(@"
10) foo
- bar", @"<document>
<list type=""ordered"" start=""10"" tight=""true"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
</item>
</list>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>bar</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"- - foo", @"<document>
<list type=""bullet"" tight=""true"">
<item>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
</item>
</list>
</item>
</list>
</document>")]
[InlineData(@"1. - foo", @"<document>
<list type=""ordered"" start=""1"" tight=""true"">
<item>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
</item>
</list>
</item>
</list>
</document>")]
[InlineData(@"1. 1. foo", @"<document>
<list type=""ordered"" start=""1"" tight=""true"">
<item>
<list type=""ordered"" start=""1"" tight=""true"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
</item>
</list>
</item>
</list>
</document>")]
[InlineData(@"- 1. foo", @"<document>
<list type=""bullet"" tight=""true"">
<item>
<list type=""ordered"" start=""1"" tight=""true"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
</item>
</list>
</item>
</list>
</document>")]
[InlineData(@"1. - 2. foo", @"<document>
<list type=""ordered"" start=""1"" tight=""true"">
<item>
<list type=""bullet"" tight=""true"">
<item>
<list type=""ordered"" start=""2"" tight=""true"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
</item>
</list>
</item>
</list>
</item>
</list>
</document>")]
[InlineData(@"- # Foo
- Bar
---
baz", @"<document>
<list type=""bullet"" tight=""true"">
<item>
<heading level=""1"">
<text>Foo</text>
</heading>
</item>
<item>
<heading level=""2"">
<text>Bar</text>
</heading>
<paragraph>
<text>baz</text>
</paragraph>
</item>
</list>
</document>")]
public void Parse_ListItem_WithNesting(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
}

View File

@@ -0,0 +1,693 @@
using Xunit;
namespace Radzen.Blazor.Markdown.Tests;
public class ListTests
{
private static string ToXml(string markdown)
{
var document = MarkdownParser.Parse(markdown);
return XmlVisitor.ToXml(document);
}
[Theory]
[InlineData(@"- foo
- bar
+ baz", @"<document>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
</item>
<item>
<paragraph>
<text>bar</text>
</paragraph>
</item>
</list>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>baz</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"1. foo
2. bar
3) baz", @"<document>
<list type=""ordered"" start=""1"" tight=""true"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
</item>
<item>
<paragraph>
<text>bar</text>
</paragraph>
</item>
</list>
<list type=""ordered"" start=""3"" tight=""true"">
<item>
<paragraph>
<text>baz</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"Foo
- bar
- baz", @"<document>
<paragraph>
<text>Foo</text>
</paragraph>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>bar</text>
</paragraph>
</item>
<item>
<paragraph>
<text>baz</text>
</paragraph>
</item>
</list>
</document>")]
public void Parse_List(string markdown, string expected)
{
var actual = ToXml(markdown);
Assert.Equal(expected, actual);
}
[Theory]
[InlineData(@"The number of windows in my house is
14. The number of doors is 6.", @"<document>
<paragraph>
<text>The number of windows in my house is</text>
<softbreak />
<text>14. The number of doors is 6.</text>
</paragraph>
</document>")]
[InlineData(@"The number of windows in my house is
1. The number of doors is 6.", @"<document>
<paragraph>
<text>The number of windows in my house is</text>
</paragraph>
<list type=""ordered"" start=""1"" tight=""true"">
<item>
<paragraph>
<text>The number of doors is 6.</text>
</paragraph>
</item>
</list>
</document>")]
public void Parse_OnlyNumberedListsThatStartWithOneCanInterruptParagraphs(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData(@"- foo
- bar
- baz", @"<document>
<list type=""bullet"" tight=""false"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
</item>
<item>
<paragraph>
<text>bar</text>
</paragraph>
</item>
<item>
<paragraph>
<text>baz</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"- foo
- bar
- baz
bim
", @"<document>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>bar</text>
</paragraph>
<list type=""bullet"" tight=""false"">
<item>
<paragraph>
<text>baz</text>
</paragraph>
<paragraph>
<text>bim</text>
</paragraph>
</item>
</list>
</item>
</list>
</item>
</list>
</document>")]
[InlineData(@"- a
- b
- c", @"<document>
<list type=""bullet"" tight=""false"">
<item>
<paragraph>
<text>a</text>
</paragraph>
</item>
<item>
<paragraph>
<text>b</text>
</paragraph>
</item>
<item>
<paragraph>
<text>c</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"* a
*
* c", @"<document>
<list type=""bullet"" tight=""false"">
<item>
<paragraph>
<text>a</text>
</paragraph>
</item>
<item />
<item>
<paragraph>
<text>c</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"- a
- b
c
- d", @"<document>
<list type=""bullet"" tight=""false"">
<item>
<paragraph>
<text>a</text>
</paragraph>
</item>
<item>
<paragraph>
<text>b</text>
</paragraph>
<paragraph>
<text>c</text>
</paragraph>
</item>
<item>
<paragraph>
<text>d</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"- a
- b
[ref]: /url
- d", @"<document>
<list type=""bullet"" tight=""false"">
<item>
<paragraph>
<text>a</text>
</paragraph>
</item>
<item>
<paragraph>
<text>b</text>
</paragraph>
</item>
<item>
<paragraph>
<text>d</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"- a
- ```
b
```
- c", @"<document>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>a</text>
</paragraph>
</item>
<item>
<code_block>b
</code_block>
</item>
<item>
<paragraph>
<text>c</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"- a
- b
c
- d", @"<document>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>a</text>
</paragraph>
<list type=""bullet"" tight=""false"">
<item>
<paragraph>
<text>b</text>
</paragraph>
<paragraph>
<text>c</text>
</paragraph>
</item>
</list>
</item>
<item>
<paragraph>
<text>d</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"* a
> b
>
* c", @"<document>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>a</text>
</paragraph>
<block_quote>
<paragraph>
<text>b</text>
</paragraph>
</block_quote>
</item>
<item>
<paragraph>
<text>c</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"- a
> b
```
c
```
- d
", @"<document>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>a</text>
</paragraph>
<block_quote>
<paragraph>
<text>b</text>
</paragraph>
</block_quote>
<code_block>c
</code_block>
</item>
<item>
<paragraph>
<text>d</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"- a", @"<document>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>a</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"- a
- b", @"<document>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>a</text>
</paragraph>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>b</text>
</paragraph>
</item>
</list>
</item>
</list>
</document>")]
[InlineData(@"1. ```
foo
```
bar", @"<document>
<list type=""ordered"" start=""1"" tight=""false"">
<item>
<code_block>foo
</code_block>
<paragraph>
<text>bar</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"* foo
* bar
baz", @"<document>
<list type=""bullet"" tight=""false"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>bar</text>
</paragraph>
</item>
</list>
<paragraph>
<text>baz</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"- a
- b
- c
- d
- e
- f", @"<document>
<list type=""bullet"" tight=""false"">
<item>
<paragraph>
<text>a</text>
</paragraph>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>b</text>
</paragraph>
</item>
<item>
<paragraph>
<text>c</text>
</paragraph>
</item>
</list>
</item>
<item>
<paragraph>
<text>d</text>
</paragraph>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>e</text>
</paragraph>
</item>
<item>
<paragraph>
<text>f</text>
</paragraph>
</item>
</list>
</item>
</list>
</document>")]
public void Parse_TightAndLooseLists(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData(@"- foo
- bar
<!-- -->
- baz
- bim", @"<document>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
</item>
<item>
<paragraph>
<text>bar</text>
</paragraph>
</item>
</list>
<html_block>&lt;!-- --&gt;</html_block>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>baz</text>
</paragraph>
</item>
<item>
<paragraph>
<text>bim</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"- foo
notcode
- foo
<!-- -->
code", @"<document>
<list type=""bullet"" tight=""false"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
<paragraph>
<text>notcode</text>
</paragraph>
</item>
<item>
<paragraph>
<text>foo</text>
</paragraph>
</item>
</list>
<html_block>&lt;!-- --&gt;</html_block>
<code_block>code
</code_block>
</document>")]
public void Parse_List_Separators(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData(@"- a
- b
- c
- d
- e
- f
- g", @"<document>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>a</text>
</paragraph>
</item>
<item>
<paragraph>
<text>b</text>
</paragraph>
</item>
<item>
<paragraph>
<text>c</text>
</paragraph>
</item>
<item>
<paragraph>
<text>d</text>
</paragraph>
</item>
<item>
<paragraph>
<text>e</text>
</paragraph>
</item>
<item>
<paragraph>
<text>f</text>
</paragraph>
</item>
<item>
<paragraph>
<text>g</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"1. a
2. b
3. c
", @"<document>
<list type=""ordered"" start=""1"" tight=""false"">
<item>
<paragraph>
<text>a</text>
</paragraph>
</item>
<item>
<paragraph>
<text>b</text>
</paragraph>
</item>
<item>
<paragraph>
<text>c</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"- a
- b
- c
- d
- e", @"<document>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>a</text>
</paragraph>
</item>
<item>
<paragraph>
<text>b</text>
</paragraph>
</item>
<item>
<paragraph>
<text>c</text>
</paragraph>
</item>
<item>
<paragraph>
<text>d</text>
<softbreak />
<text>- e</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"1. a
2. b
3. c", @"<document>
<list type=""ordered"" start=""1"" tight=""false"">
<item>
<paragraph>
<text>a</text>
</paragraph>
</item>
<item>
<paragraph>
<text>b</text>
</paragraph>
</item>
</list>
<code_block>3. c
</code_block>
</document>")]
public void Parse_List_Identation(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Fact]
public void Parse_BasicNestedLists()
{
Assert.Equal(@"<document>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>a</text>
</paragraph>
</item>
<item>
<paragraph>
<text>b</text>
</paragraph>
</item>
<item>
<paragraph>
<text>c</text>
</paragraph>
</item>
</list>
</document>", ToXml(@"
- a
- b
- c"));
}
}

View File

@@ -0,0 +1,109 @@
using Xunit;
namespace Radzen.Blazor.Markdown.Tests;
public class ParagraphTests
{
private static string ToXml(string markdown)
{
var document = MarkdownParser.Parse(markdown);
return XmlVisitor.ToXml(document);
}
[Fact]
public void Parse_BasicParagraph()
{
Assert.Equal(@"<document>
<paragraph>
<text>foo</text>
</paragraph>
</document>", ToXml(@"foo"));
}
[Theory]
[InlineData(@"aaa
bbb", @"<document>
<paragraph>
<text>aaa</text>
</paragraph>
<paragraph>
<text>bbb</text>
</paragraph>
</document>")]
[InlineData(@"aaa
bbb
ccc
ddd", @"<document>
<paragraph>
<text>aaa</text>
<softbreak />
<text>bbb</text>
</paragraph>
<paragraph>
<text>ccc</text>
<softbreak />
<text>ddd</text>
</paragraph>
</document>")]
[InlineData(@"aaa
bbb
", @"<document>
<paragraph>
<text>aaa</text>
</paragraph>
<paragraph>
<text>bbb</text>
</paragraph>
</document>")]
[InlineData(@" aaa
bbb", @"<document>
<paragraph>
<text>aaa</text>
<softbreak />
<text>bbb</text>
</paragraph>
</document>")]
[InlineData(@"aaa
bbb
ccc", @"<document>
<paragraph>
<text>aaa</text>
<softbreak />
<text>bbb</text>
<softbreak />
<text>ccc</text>
</paragraph>
</document>")]
[InlineData(@" aaa
bbb", @"<document>
<paragraph>
<text>aaa</text>
<softbreak />
<text>bbb</text>
</paragraph>
</document>")]
[InlineData(@" aaa
bbb", @"<document>
<code_block>aaa
</code_block>
<paragraph>
<text>bbb</text>
</paragraph>
</document>")]
[InlineData(@"aaa
bbb ", @"<document>
<paragraph>
<text>aaa</text>
<linebreak />
<text>bbb</text>
</paragraph>
</document>")]
public void Parse_Paragraph(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
}

View File

@@ -0,0 +1,41 @@
using Xunit;
namespace Radzen.Blazor.Markdown.Tests;
public class SoftLineBreakTests
{
private static string ToXml(string markdown)
{
var document = MarkdownParser.Parse(markdown);
return XmlVisitor.ToXml(document);
}
[Theory]
[InlineData(@"foo
baz", @"<document>
<paragraph>
<text>foo</text>
<softbreak />
<text>baz</text>
</paragraph>
</document>")]
public void Parse_SoftLineBreak(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData(@"foo
baz", @"<document>
<paragraph>
<text>foo</text>
<softbreak />
<text>baz</text>
</paragraph>
</document>")]
public void Parse_SoftLine_RemovesSpacesAtEndAndStartOfLine(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
}

View File

@@ -0,0 +1,135 @@
using Xunit;
namespace Radzen.Blazor.Markdown.Tests;
public class StrongTests
{
private static string ToXml(string markdown)
{
var document = MarkdownParser.Parse(markdown);
return XmlVisitor.ToXml(document);
}
[Theory]
[InlineData("**foo bar**",
@"<document>
<paragraph>
<strong>
<text>foo bar</text>
</strong>
</paragraph>
</document>")]
[InlineData("** foo bar**",
@"<document>
<paragraph>
<text>**</text>
<text> foo bar</text>
<text>**</text>
</paragraph>
</document>")]
[InlineData("a**\"foo\"**",
@"<document>
<paragraph>
<text>a</text>
<text>**</text>
<text>""foo""</text>
<text>**</text>
</paragraph>
</document>")]
[InlineData("foo**bar**",
@"<document>
<paragraph>
<text>foo</text>
<strong>
<text>bar</text>
</strong>
</paragraph>
</document>")]
[InlineData("__foo bar__",
@"<document>
<paragraph>
<strong>
<text>foo bar</text>
</strong>
</paragraph>
</document>")]
[InlineData("__ foo bar__",
@"<document>
<paragraph>
<text>__</text>
<text> foo bar</text>
<text>__</text>
</paragraph>
</document>")]
[InlineData("__\nfoo bar__",
@"<document>
<paragraph>
<text>__</text>
<softbreak />
<text>foo bar</text>
<text>__</text>
</paragraph>
</document>")]
[InlineData("a__\"foo\"__",
@"<document>
<paragraph>
<text>a</text>
<text>__</text>
<text>""foo""</text>
<text>__</text>
</paragraph>
</document>")]
[InlineData("foo__bar__",
@"<document>
<paragraph>
<text>foo</text>
<text>__</text>
<text>bar</text>
<text>__</text>
</paragraph>
</document>")]
[InlineData("5__6__78",
@"<document>
<paragraph>
<text>5</text>
<text>__</text>
<text>6</text>
<text>__</text>
<text>78</text>
</paragraph>
</document>")]
[InlineData("пристаням__стремятся__",
@"<document>
<paragraph>
<text>пристаням</text>
<text>__</text>
<text>стремятся</text>
<text>__</text>
</paragraph>
</document>")]
[InlineData("__foo, __bar__, baz__",
@"<document>
<paragraph>
<strong>
<text>foo, </text>
<strong>
<text>bar</text>
</strong>
<text>, baz</text>
</strong>
</paragraph>
</document>")]
[InlineData("foo-__(bar)__",
@"<document>
<paragraph>
<text>foo-</text>
<strong>
<text>(bar)</text>
</strong>
</paragraph>
</document>")]
public void Parse_StrongEmphasisRules_AdheresToCommonMarkSpec(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
}

View File

@@ -0,0 +1,414 @@
using Xunit;
namespace Radzen.Blazor.Markdown.Tests;
public class TableTests
{
private static string ToXml(string markdown)
{
var document = MarkdownParser.Parse(markdown);
return XmlVisitor.ToXml(document);
}
[Fact]
public void Parse_BasicTable()
{
Assert.Equal(
@"<document>
<table>
<header>
<cell>
<text>foo</text>
</cell>
<cell>
<text>bar</text>
</cell>
</header>
<row>
<cell>
<text>baz</text>
</cell>
<cell>
<text>bim</text>
</cell>
</row>
</table>
</document>",
ToXml(@"
foo|bar
--|--
baz|bim"));
}
[Theory]
[InlineData(@"| foo | bar |
| --- | --- |
| baz | bim |", @"<document>
<table>
<header>
<cell>
<text>foo</text>
</cell>
<cell>
<text>bar</text>
</cell>
</header>
<row>
<cell>
<text>baz</text>
</cell>
<cell>
<text>bim</text>
</cell>
</row>
</table>
</document>")]
[InlineData(@"| f\|oo |
| ------ |
| b `\|` az |
| b **\|** im |", @"<document>
<table>
<header>
<cell>
<text>f|oo</text>
</cell>
</header>
<row>
<cell>
<text>b </text>
<code>|</code>
<text> az</text>
</cell>
</row>
<row>
<cell>
<text>b </text>
<strong>
<text>|</text>
</strong>
<text> im</text>
</cell>
</row>
</table>
</document>")]
[InlineData(@"| abc | defghi |
:-: | -----------:
bar | baz", @"<document>
<table>
<header>
<cell align=""center"">
<text>abc</text>
</cell>
<cell align=""right"">
<text>defghi</text>
</cell>
</header>
<row>
<cell align=""center"">
<text>bar</text>
</cell>
<cell align=""right"">
<text>baz</text>
</cell>
</row>
</table>
</document>")]
[InlineData(@"| abc | def |
| --- | --- |
bar", @"<document>
<table>
<header>
<cell>
<text>abc</text>
</cell>
<cell>
<text>def</text>
</cell>
</header>
<row>
<cell>
<text>bar</text>
</cell>
<cell />
</row>
</table>
</document>")]
[InlineData(@"| abc | def |
| --- | --- |
c:\\foo", @"<document>
<table>
<header>
<cell>
<text>abc</text>
</cell>
<cell>
<text>def</text>
</cell>
</header>
<row>
<cell>
<text>c:</text>
<text>\</text>
<text>foo</text>
</cell>
<cell />
</row>
</table>
</document>")]
public void Parse_Table(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData(@"| abc | def |
| --- | --- |
| bar | baz |
boo", @"<document>
<table>
<header>
<cell>
<text>abc</text>
</cell>
<cell>
<text>def</text>
</cell>
</header>
<row>
<cell>
<text>bar</text>
</cell>
<cell>
<text>baz</text>
</cell>
</row>
</table>
<paragraph>
<text>boo</text>
</paragraph>
</document>")]
[InlineData(@"| foo |
| --- |
# bar", @"<document>
<table>
<header>
<cell>
<text>foo</text>
</cell>
</header>
</table>
<heading level=""1"">
<text>bar</text>
</heading>
</document>")]
[InlineData(@"| foo |
| --- |
- bar", @"<document>
<table>
<header>
<cell>
<text>foo</text>
</cell>
</header>
</table>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>bar</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"| foo |
| --- |
1. bar", @"<document>
<table>
<header>
<cell>
<text>foo</text>
</cell>
</header>
</table>
<list type=""ordered"" start=""1"" tight=""true"">
<item>
<paragraph>
<text>bar</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"| abc | def |
| --- | --- |
| bar | baz |
> bar", @"<document>
<table>
<header>
<cell>
<text>abc</text>
</cell>
<cell>
<text>def</text>
</cell>
</header>
<row>
<cell>
<text>bar</text>
</cell>
<cell>
<text>baz</text>
</cell>
</row>
</table>
<block_quote>
<paragraph>
<text>bar</text>
</paragraph>
</block_quote>
</document>")]
[InlineData(@"| abc | def |
| --- | --- |
| bar | baz |
bar", @"<document>
<table>
<header>
<cell>
<text>abc</text>
</cell>
<cell>
<text>def</text>
</cell>
</header>
<row>
<cell>
<text>bar</text>
</cell>
<cell>
<text>baz</text>
</cell>
</row>
</table>
<code_block>bar
</code_block>
</document>")]
[InlineData(@"| abc | def |
| --- | --- |
| bar | baz |
```
bar
```", @"<document>
<table>
<header>
<cell>
<text>abc</text>
</cell>
<cell>
<text>def</text>
</cell>
</header>
<row>
<cell>
<text>bar</text>
</cell>
<cell>
<text>baz</text>
</cell>
</row>
</table>
<code_block>bar
</code_block>
</document>")]
[InlineData(@"| abc | def |
| --- | --- |
| bar | baz |
<div>
</div>", @"<document>
<table>
<header>
<cell>
<text>abc</text>
</cell>
<cell>
<text>def</text>
</cell>
</header>
<row>
<cell>
<text>bar</text>
</cell>
<cell>
<text>baz</text>
</cell>
</row>
</table>
<html_block>&lt;div&gt;
&lt;/div&gt;</html_block>
</document>")]
[InlineData(@"| abc | def |
| --- | --- |
| bar | baz |
---", @"<document>
<table>
<header>
<cell>
<text>abc</text>
</cell>
<cell>
<text>def</text>
</cell>
</header>
<row>
<cell>
<text>bar</text>
</cell>
<cell>
<text>baz</text>
</cell>
</row>
</table>
<thematic_break />
</document>")]
public void Parse_Table_AnyBlockOrEmptyLineBreaksTable(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData(@"| abc | def |
| --- |", @"<document>
<paragraph>
<text>| abc | def |</text>
<softbreak />
<text>| --- |</text>
</paragraph>
</document>")]
public void Parse_Table_ChecksHeaderAndDelimiter(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
[Theory]
[InlineData(@"| abc |
| --- |
| bar | baz |", @"<document>
<table>
<header>
<cell>
<text>abc</text>
</cell>
</header>
<row>
<cell>
<text>bar</text>
</cell>
</row>
</table>
</document>")]
public void Parse_Table_IgnoresExtraCells(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
}

View File

@@ -0,0 +1,162 @@
using Xunit;
namespace Radzen.Blazor.Markdown.Tests;
public class ThematicBreakTests
{
private static string ToXml(string markdown)
{
var document = MarkdownParser.Parse(markdown);
return XmlVisitor.ToXml(document);
}
[Theory]
[InlineData(@"***
---
___", @"<document>
<thematic_break />
<thematic_break />
<thematic_break />
</document>")]
[InlineData(@"--
**
__", @"<document>
<paragraph>
<text>--</text>
<softbreak />
<text>**</text>
<softbreak />
<text>__</text>
</paragraph>
</document>")]
[InlineData(@" ***
***
***", @"<document>
<thematic_break />
<thematic_break />
<thematic_break />
</document>")]
[InlineData(@" ***", @"<document>
<code_block>***
</code_block>
</document>")]
[InlineData(@"Foo
***", @"<document>
<paragraph>
<text>Foo</text>
<softbreak />
<text>***</text>
</paragraph>
</document>")]
[InlineData(@"_____________________________________", @"<document>
<thematic_break />
</document>")]
[InlineData(@"- - -", @"<document>
<thematic_break />
</document>")]
[InlineData(@" ** * ** * ** * **", @"<document>
<thematic_break />
</document>")]
[InlineData(@"- - - -", @"<document>
<thematic_break />
</document>")]
[InlineData(@"- - - - ", @"<document>
<thematic_break />
</document>")]
[InlineData(@"_ _ _ _ a
a------
---a---", @"<document>
<paragraph>
<text>_</text>
<text> </text>
<text>_</text>
<text> </text>
<text>_</text>
<text> </text>
<text>_</text>
<text> a</text>
</paragraph>
<paragraph>
<text>a------</text>
</paragraph>
<paragraph>
<text>---a---</text>
</paragraph>
</document>")]
[InlineData(@" *-*", @"<document>
<paragraph>
<emph>
<text>-</text>
</emph>
</paragraph>
</document>")]
[InlineData(@"- foo
***
- bar", @"<document>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>foo</text>
</paragraph>
</item>
</list>
<thematic_break />
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>bar</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"Foo
***
bar", @"<document>
<paragraph>
<text>Foo</text>
</paragraph>
<thematic_break />
<paragraph>
<text>bar</text>
</paragraph>
</document>")]
[InlineData(@"* Foo
* * *
* Bar", @"<document>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>Foo</text>
</paragraph>
</item>
</list>
<thematic_break />
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>Bar</text>
</paragraph>
</item>
</list>
</document>")]
[InlineData(@"- Foo
- * * *", @"<document>
<list type=""bullet"" tight=""true"">
<item>
<paragraph>
<text>Foo</text>
</paragraph>
</item>
<item>
<thematic_break />
</item>
</list>
</document>")]
public void Parse_ThematicBreak(string markdown, string expected)
{
Assert.Equal(expected, ToXml(markdown));
}
}

View File

@@ -0,0 +1,210 @@
using System;
using System.Text;
using System.Xml;
namespace Radzen.Blazor.Markdown.Tests;
public class XmlVisitor : NodeVisitorBase, IDisposable
{
private readonly XmlWriter writer;
private XmlVisitor(StringBuilder xml)
{
writer = XmlWriter.Create(xml, new XmlWriterSettings { OmitXmlDeclaration = true, Indent = true, IndentChars = " ", });
}
public void Dispose()
{
writer.Dispose();
}
public void Close()
{
writer.Close();
}
public static string ToXml(Document document)
{
var xml = new StringBuilder();
using var visitor = new XmlVisitor(xml);
document.Accept(visitor);
visitor.Close();
return xml.ToString()!;
}
public override void VisitBlockQuote(BlockQuote blockQuote)
{
writer.WriteStartElement("block_quote");
base.VisitBlockQuote(blockQuote);
writer.WriteEndElement();
}
public override void VisitDocument(Document document)
{
writer.WriteStartDocument();
writer.WriteStartElement("document");
base.VisitDocument(document);
writer.WriteEndElement();
writer.WriteEndDocument();
}
public override void VisitHeading(Heading heading)
{
writer.WriteStartElement($"heading");
writer.WriteAttributeString("level", heading.Level.ToString());
base.VisitHeading(heading);
writer.WriteEndElement();
}
public override void VisitListItem(ListItem listItem)
{
writer.WriteStartElement("item");
base.VisitListItem(listItem);
writer.WriteEndElement();
}
public override void VisitParagraph(Paragraph paragraph)
{
writer.WriteStartElement("paragraph");
base.VisitParagraph(paragraph);
writer.WriteEndElement();
}
public override void VisitUnorderedList(UnorderedList unorderedList)
{
writer.WriteStartElement("list");
writer.WriteAttributeString("type", "bullet");
writer.WriteAttributeString("tight", unorderedList.Tight.ToString().ToLowerInvariant());
base.VisitUnorderedList(unorderedList);
writer.WriteEndElement();
}
public override void VisitText(Text text)
{
writer.WriteElementString("text", text.Value);
}
public override void VisitOrderedList(OrderedList orderedList)
{
writer.WriteStartElement("list");
writer.WriteAttributeString("type", "ordered");
writer.WriteAttributeString("start", orderedList.Start.ToString());
writer.WriteAttributeString("tight", orderedList.Tight.ToString().ToLowerInvariant());
base.VisitOrderedList(orderedList);
writer.WriteEndElement();
}
public override void VisitLink(Link link)
{
writer.WriteStartElement("link");
writer.WriteAttributeString("destination", link.Destination);
writer.WriteAttributeString("title", link.Title);
base.VisitLink(link);
writer.WriteEndElement();
}
public override void VisitImage(Image image)
{
writer.WriteStartElement("image");
writer.WriteAttributeString("destination", image.Destination);
writer.WriteAttributeString("title", image.Title);
base.VisitImage(image);
writer.WriteEndElement();
}
public override void VisitEmphasis(Emphasis emphasis)
{
writer.WriteStartElement("emph");
base.VisitEmphasis(emphasis);
writer.WriteEndElement();
}
public override void VisitStrong(Strong strong)
{
writer.WriteStartElement("strong");
base.VisitStrong(strong);
writer.WriteEndElement();
}
public override void VisitCode(Code code)
{
writer.WriteElementString("code", code.Value);
}
public override void VisitHtmlInline(HtmlInline html)
{
writer.WriteElementString("html_inline", html.Value);
}
public override void VisitLineBreak(LineBreak lineBreak)
{
writer.WriteElementString("linebreak", string.Empty);
}
public override void VisitSoftLineBreak(SoftLineBreak softLineBreak)
{
writer.WriteElementString("softbreak", string.Empty);
}
public override void VisitThematicBreak(ThematicBreak thematicBreak)
{
writer.WriteElementString("thematic_break", string.Empty);
}
public override void VisitIndentedCodeBlock(IndentedCodeBlock codeBlock)
{
writer.WriteElementString("code_block", codeBlock.Value);
}
public override void VisitFencedCodeBlock(FencedCodeBlock fencedCodeBlock)
{
writer.WriteStartElement("code_block");
if (!string.IsNullOrEmpty(fencedCodeBlock.Info))
{
writer.WriteAttributeString("info", fencedCodeBlock.Info);
}
writer.WriteString(fencedCodeBlock.Value);
writer.WriteEndElement();
}
public override void VisitHtmlBlock(HtmlBlock htmlBlock)
{
writer.WriteElementString("html_block", htmlBlock.Value);
}
public override void VisitTable(Table table)
{
writer.WriteStartElement("table");
base.VisitTable(table);
writer.WriteEndElement();
}
public override void VisitTableHeaderRow(TableHeaderRow header)
{
writer.WriteStartElement("header");
base.VisitTableHeaderRow(header);
writer.WriteEndElement();
}
public override void VisitTableRow(TableRow row)
{
writer.WriteStartElement("row");
base.VisitTableRow(row);
writer.WriteEndElement();
}
public override void VisitTableCell(TableCell cell)
{
writer.WriteStartElement("cell");
if (cell.Alignment != TableCellAlignment.None)
{
writer.WriteAttributeString("align", cell.Alignment.ToString().ToLowerInvariant());
}
base.VisitTableCell(cell);
writer.WriteEndElement();
}
}

View File

@@ -16,7 +16,7 @@ namespace Radzen.Blazor.Tests
public string Name { get; set; }
public FieldIdentifier FieldIdentifier => throw new System.NotImplementedException();
public FieldIdentifier FieldIdentifier { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public object GetValue()
{

View File

@@ -612,10 +612,10 @@ namespace Radzen.Blazor.Tests
});
});
component.Find("input").Change("13.53");
component.Find("input").Change(13.53);
var maxDollars = new Dollars(2);
Assert.Contains($" value=\"{maxDollars.ToString()}\"", component.Markup);
var maxDollars = new Dollars(maxValue);
Assert.Contains($" value=\"{maxDollars}\"", component.Markup);
Assert.Equal(component.Instance.Value, maxDollars);
}

View File

@@ -1,8 +1,9 @@
using Bunit;
using Bunit.TestDoubles;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace Radzen.Blazor.Tests
@@ -22,6 +23,9 @@ namespace Radzen.Blazor.Tests
}
private static string CreatePanelMenu(string currentAbsoluteUrl, NavLinkMatch match, params string[] urls)
=> CreatePanelMenu(currentAbsoluteUrl, match, new Dictionary<string, bool>(urls.Select(url => new KeyValuePair<string, bool>(url, false))));
private static string CreatePanelMenu(string currentAbsoluteUrl, NavLinkMatch match, Dictionary<string, bool> urls)
{
using var ctx = new TestContext();
@@ -30,12 +34,13 @@ namespace Radzen.Blazor.Tests
var component = ctx.RenderComponent<RadzenPanelMenu>();
component.SetParametersAndRender(parameters => parameters.Add(p => p.Match, match).AddChildContent(builder =>
component.SetParametersAndRender(parameters => parameters.Add(p => p.Match, match).AddChildContent(builder =>
{
foreach (var url in urls)
{
builder.OpenComponent<RadzenPanelMenuItem>(0);
builder.AddAttribute(1, nameof(RadzenPanelMenuItem.Path), url);
builder.AddAttribute(1, nameof(RadzenPanelMenuItem.Path), url.Key);
builder.AddAttribute(2, nameof(RadzenPanelMenuItem.Disabled), url.Value);
builder.CloseComponent();
}
}));
@@ -55,6 +60,19 @@ namespace Radzen.Blazor.Tests
Assert.Equal(firstIndex, lastIndex);
}
[Fact]
public void RadzenPanelMenu_CanDisableMenuItem()
{
var urls = new Dictionary<string, bool>
{
{"/datagrid", false},
{"/disabled-url", true}
};
var component = CreatePanelMenu("http://www.example.com/", NavLinkMatch.All, urls);
Assert.Contains("rz-state-disabled", component);
}
[Fact]
public void RadzenPanelMenu_MatchesQueryStringParameters()
{

View File

@@ -198,14 +198,10 @@ namespace Radzen.Blazor.Tests
});
Assert.Contains("SummaryContent", component.Markup);
Assert.Equal(
"display: block",
component.Find(".rz-panel-content-summary").ParentElement.Attributes.First(attr => attr.Name == "style").Value
);
}
[Fact]
public void Panel_DontRenders_SummaryWhenOpen()
public void Panel_DoesNotRender_SummaryWhenOpen()
{
using var ctx = new TestContext();
var component = ctx.RenderComponent<RadzenPanel>();
@@ -225,8 +221,8 @@ namespace Radzen.Blazor.Tests
Assert.Contains("SummaryContent", component.Markup);
Assert.Equal(
"display: none",
component.Find(".rz-panel-content-summary").ParentElement.Attributes.First(attr => attr.Name == "style").Value
"true",
component.Find(".rz-panel-content-summary").ParentElement.ParentElement.Attributes.First(attr => attr.Name == "aria-hidden").Value
);
}
}

View File

@@ -1,8 +1,5 @@
using AngleSharp.Css;
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
using Xunit;
namespace Radzen.Blazor.Tests
@@ -116,5 +113,43 @@ namespace Radzen.Blazor.Tests
{
public List<string> Values { get; set; }
}
[Fact]
public void GetProperty_Should_Resolve_DescriptionProperty()
{
var descriptionProperty = PropertyAccess.GetProperty(typeof(ISimpleInterface), nameof(ISimpleInterface.Description));
Assert.NotNull(descriptionProperty);
}
[Fact]
public void GetProperty_Should_Resolve_NameProperty()
{
var nameProperty = PropertyAccess.GetProperty(typeof(ISimpleInterface), nameof(ISimpleInterface.Name));
Assert.NotNull(nameProperty);
}
[Fact]
public void GetProperty_Should_Resolve_IdProperty()
{
var idProperty = PropertyAccess.GetProperty(typeof(ISimpleInterface), nameof(ISimpleBaseInterface.Id));
Assert.NotNull(idProperty);
}
interface ISimpleInterface : ISimpleNestedInterface
{
string Description { get; set; }
}
interface ISimpleNestedInterface : ISimpleBaseInterface
{
string Name { get; set; }
}
interface ISimpleBaseInterface
{
int Id { get; set; }
}
}
}

View File

@@ -0,0 +1,158 @@
using Bunit;
using Radzen.Blazor.Rendering;
using Xunit;
namespace Radzen.Blazor.Tests
{
public class SkeletonTests
{
[Fact]
public void Skeleton_Renders_CssClass()
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
var component = ctx.RenderComponent<RadzenSkeleton>();
Assert.Contains("rz-skeleton", component.Markup);
Assert.Contains("rz-skeleton-text", component.Markup);
}
[Fact]
public void Skeleton_Renders_TypeParameter()
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
var component = ctx.RenderComponent<RadzenSkeleton>();
component.SetParametersAndRender(parameters => parameters.Add(p => p.Variant, SkeletonVariant.Circular));
Assert.Contains("rz-skeleton", component.Markup);
Assert.Contains("rz-skeleton-circular", component.Markup);
}
[Theory]
[InlineData(SkeletonVariant.Text, "rz-skeleton-text")]
[InlineData(SkeletonVariant.Circular, "rz-skeleton-circular")]
[InlineData(SkeletonVariant.Rectangular, "rz-skeleton-rectangular")]
public void Skeleton_Renders_AllTypes(SkeletonVariant type, string expectedClass)
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
var component = ctx.RenderComponent<RadzenSkeleton>(parameters => parameters.Add(p => p.Variant, type));
Assert.Contains("rz-skeleton", component.Markup);
Assert.Contains(expectedClass, component.Markup);
}
[Fact]
public void Skeleton_Renders_AnimationParameter()
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
var component = ctx.RenderComponent<RadzenSkeleton>();
component.SetParametersAndRender(parameters => parameters.Add(p => p.Animation, SkeletonAnimation.Wave));
Assert.Contains("rz-skeleton", component.Markup);
Assert.Contains("rz-skeleton-wave", component.Markup);
}
[Theory]
[InlineData(SkeletonAnimation.Wave, "rz-skeleton-wave")]
[InlineData(SkeletonAnimation.Pulse, "rz-skeleton-pulse")]
public void Skeleton_Renders_AllAnimations(SkeletonAnimation animation, string expectedClass)
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
var component = ctx.RenderComponent<RadzenSkeleton>(parameters => parameters.Add(p => p.Animation, animation));
Assert.Contains("rz-skeleton", component.Markup);
Assert.Contains(expectedClass, component.Markup);
}
[Fact]
public void Skeleton_Renders_StyleParameter()
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
var component = ctx.RenderComponent<RadzenSkeleton>();
var style = "width: 200px; height: 20px;";
component.SetParametersAndRender(parameters => parameters.Add(p => p.Style, style));
Assert.Contains($"style=\"{style}\"", component.Markup);
}
[Fact]
public void Skeleton_Renders_VisibleParameter()
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
var component = ctx.RenderComponent<RadzenSkeleton>();
// Should be visible by default
Assert.Contains("rz-skeleton", component.Markup);
component.SetParametersAndRender(parameters => parameters.Add(p => p.Visible, false));
// Should not render when not visible
Assert.DoesNotContain("rz-skeleton", component.Markup);
}
[Fact]
public void Skeleton_Renders_UnmatchedParameter()
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
var component = ctx.RenderComponent<RadzenSkeleton>();
component.SetParametersAndRender(parameters => parameters.AddUnmatched("data-testid", "skeleton-test"));
Assert.Contains("data-testid=\"skeleton-test\"", component.Markup);
}
[Fact]
public void Skeleton_DefaultType_IsText()
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
var component = ctx.RenderComponent<RadzenSkeleton>();
// Should render with text type by default
Assert.Contains("rz-skeleton-text", component.Markup);
}
[Fact]
public void Skeleton_DefaultAnimation_IsNone()
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
var component = ctx.RenderComponent<RadzenSkeleton>();
// Should not render animation classes by default
Assert.DoesNotContain("rz-skeleton-wave", component.Markup);
Assert.DoesNotContain("rz-skeleton-pulse", component.Markup);
}
}
}

View File

@@ -134,5 +134,18 @@ namespace Radzen.Blazor.Tests
Assert.True(raised);
Assert.True(object.Equals(value, !(bool)newValue));
}
[Fact]
public void Switch_Renders_ReadOnlyParameter()
{
using var ctx = new TestContext();
var component = ctx.RenderComponent<RadzenSwitch>();
component.SetParametersAndRender(parameters => parameters.Add<bool>(p => p.ReadOnly, true));
Assert.Contains(@$"rz-readonly", component.Markup);
}
}
}

View File

@@ -1,4 +1,6 @@
using Bunit;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Xunit;
namespace Radzen.Blazor.Tests
@@ -29,6 +31,26 @@ namespace Radzen.Blazor.Tests
Assert.Contains(@$"value=""{value}""", component.Markup);
}
[Fact]
public void TextboxCanSetFieldIdentifier()
{
using var ctx = new TestContext();
var editContext = new EditContext(ctx);
var fieldIdentifier = new FieldIdentifier(ctx, nameof(RadzenTextBox.Value));
ctx.RenderTree.TryAdd<CascadingValue<EditContext>>(parameters =>
{
parameters.Add(e => e.Value, editContext);
});
var component = ctx.RenderComponent<RadzenTextBox>(parameters =>
{
parameters.Add(p => p.FieldIdentifier, fieldIdentifier);
});
Assert.Equal(component.Instance.FieldIdentifier, fieldIdentifier);
}
[Fact]
public void TextBox_Renders_StyleParameter()
{

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,460 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Net.Http;
using System.Net.Http.Headers;
using System.IO;
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System.Linq;
namespace Radzen;
/// <summary>
/// Represents a chat message in the conversation history.
/// </summary>
public class ChatMessage
{
/// <summary>
/// Gets or sets the role of the message sender (system, user, or assistant).
/// </summary>
public string Role { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the content of the message.
/// </summary>
public string Content { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the timestamp when the message was created.
/// </summary>
public DateTime Timestamp { get; set; } = DateTime.Now;
}
/// <summary>
/// Represents a conversation session with memory.
/// </summary>
public class ConversationSession
{
/// <summary>
/// Gets or sets the unique identifier for the conversation session.
/// </summary>
public string Id { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// Gets or sets the list of messages in the conversation.
/// </summary>
public List<ChatMessage> Messages { get; set; } = new();
/// <summary>
/// Gets or sets the timestamp when the conversation was created.
/// </summary>
public DateTime CreatedAt { get; set; } = DateTime.Now;
/// <summary>
/// Gets or sets the timestamp when the conversation was last updated.
/// </summary>
public DateTime LastUpdated { get; set; } = DateTime.Now;
/// <summary>
/// Gets or sets the maximum number of messages to keep in memory.
/// </summary>
public int MaxMessages { get; set; } = 50;
/// <summary>
/// Adds a message to the conversation and manages memory limits.
/// </summary>
/// <param name="role">The role of the message sender.</param>
/// <param name="content">The message content.</param>
public void AddMessage(string role, string content)
{
Messages.Add(new ChatMessage
{
Role = role,
Content = content,
Timestamp = DateTime.Now
});
LastUpdated = DateTime.Now;
// Remove oldest messages if we exceed the limit
while (Messages.Count > MaxMessages)
{
Messages.RemoveAt(0);
}
}
/// <summary>
/// Clears all messages from the conversation.
/// </summary>
public void Clear()
{
Messages.Clear();
LastUpdated = DateTime.Now;
}
/// <summary>
/// Gets the conversation messages formatted for the AI API.
/// </summary>
/// <param name="systemPrompt">The system prompt to include.</param>
/// <returns>A list of message objects for the AI API.</returns>
public List<object> GetFormattedMessages(string systemPrompt)
{
var messages = new List<object>();
// Add system message
messages.Add(new { role = "system", content = systemPrompt });
// Add conversation messages
foreach (var message in Messages)
{
messages.Add(new { role = message.Role, content = message.Content });
}
return messages;
}
}
/// <summary>
/// Interface for getting chat completions from an AI model with conversation memory.
/// </summary>
public interface IAIChatService
{
/// <summary>
/// Streams chat completion responses from the AI model asynchronously with conversation memory.
/// </summary>
/// <param name="userInput">The user's input message to send to the AI model.</param>
/// <param name="sessionId">Optional session ID to maintain conversation context. If null, a new session will be created.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <param name="model">Optional model name to override the configured model.</param>
/// <param name="systemPrompt">Optional system prompt to override the configured system prompt.</param>
/// <param name="temperature">Optional temperature to override the configured temperature.</param>
/// <param name="maxTokens">Optional maximum tokens to override the configured max tokens.</param>
/// <returns>An async enumerable that yields streaming response chunks from the AI model.</returns>
IAsyncEnumerable<string> GetCompletionsAsync(string userInput, string sessionId = null, CancellationToken cancellationToken = default, string model = null, string systemPrompt = null, double? temperature = null, int? maxTokens = null);
/// <summary>
/// Gets or creates a conversation session.
/// </summary>
/// <param name="sessionId">The session ID. If null, a new session will be created.</param>
/// <returns>The conversation session.</returns>
ConversationSession GetOrCreateSession(string sessionId = null);
/// <summary>
/// Clears the conversation history for a specific session.
/// </summary>
/// <param name="sessionId">The session ID to clear.</param>
void ClearSession(string sessionId);
/// <summary>
/// Gets all active conversation sessions.
/// </summary>
/// <returns>A list of active conversation sessions.</returns>
IEnumerable<ConversationSession> GetActiveSessions();
/// <summary>
/// Removes old conversation sessions based on age.
/// </summary>
/// <param name="maxAgeHours">Maximum age in hours for sessions to keep.</param>
void CleanupOldSessions(int maxAgeHours = 24);
}
/// <summary>
/// Configuration options for the <see cref="AIChatService"/>.
/// </summary>
public class AIChatServiceOptions
{
/// <summary>
/// Gets or sets the endpoint URL for the AI service.
/// </summary>
public string Endpoint { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the proxy URL for the AI service, if any. If set, this will override the Endpoint.
/// </summary>
public string Proxy { get; set; } = null;
/// <summary>
/// Gets or sets the API key for authentication with the AI service.
/// </summary>
public string ApiKey { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the header name for the API key (e.g., 'Authorization' or 'api-key').
/// </summary>
public string ApiKeyHeader { get; set; } = "Authorization";
/// <summary>
/// Gets or sets the model name to use for executing chat completions (e.g., 'gpt-3.5-turbo').
/// </summary>
public string Model { get; set; }
/// <summary>
/// Gets or sets the system prompt for the AI assistant.
/// </summary>
public string SystemPrompt { get; set; } = "You are a helpful AI code assistant.";
/// <summary>
/// Gets or sets the temperature for the AI model (0.0 to 2.0). Set to 0.0 for deterministic responses, higher values for more creative outputs.
/// </summary>
public double Temperature { get; set; } = 0.7;
/// <summary>
/// Gets or sets the maximum number of tokens to generate in the response.
/// </summary>
public int? MaxTokens { get; set; }
/// <summary>
/// Gets or sets the maximum number of messages to keep in conversation memory.
/// </summary>
public int MaxMessages { get; set; } = 50;
/// <summary>
/// Gets or sets the maximum age in hours for conversation sessions before cleanup.
/// </summary>
public int SessionMaxAgeHours { get; set; } = 24;
}
/// <summary>
/// Service for interacting with AI chat models to get completions with conversation memory.
/// </summary>
public class AIChatService(IServiceProvider serviceProvider, IOptions<AIChatServiceOptions> options) : IAIChatService
{
private readonly Dictionary<string, ConversationSession> _sessions = new();
private readonly object _sessionsLock = new();
/// <summary>
/// Gets the configuration options for the chat streaming service.
/// </summary>
public AIChatServiceOptions Options => options.Value;
/// <inheritdoc />
public async IAsyncEnumerable<string> GetCompletionsAsync(string userInput, string sessionId = null, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default, string model = null, string systemPrompt = null, double? temperature = null, int? maxTokens = null)
{
if (string.IsNullOrWhiteSpace(userInput))
{
throw new ArgumentException("User input cannot be null or empty.", nameof(userInput));
}
// Get or create session
var session = GetOrCreateSession(sessionId);
// Add user message to conversation history
session.AddMessage("user", userInput);
var url = Options.Proxy ?? Options.Endpoint;
// Get formatted messages including conversation history
var messages = session.GetFormattedMessages(systemPrompt ?? Options.SystemPrompt);
var payload = new
{
model = model ?? Options.Model,
messages = messages,
temperature = temperature ?? Options.Temperature,
max_tokens = maxTokens ?? Options.MaxTokens,
stream = true
};
var request = new HttpRequestMessage(HttpMethod.Post, url)
{
Content = new StringContent(JsonSerializer.Serialize(payload, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }), Encoding.UTF8, "application/json")
};
if (!string.IsNullOrEmpty(Options.ApiKey))
{
if (string.Equals(Options.ApiKeyHeader, "Authorization", StringComparison.OrdinalIgnoreCase))
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Options.ApiKey);
}
else
{
request.Headers.Add(Options.ApiKeyHeader, Options.ApiKey);
}
}
var httpClient = serviceProvider.GetRequiredService<HttpClient>();
var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
if (!response.IsSuccessStatusCode)
{
throw new Exception($"Chat stream failed: {await response.Content.ReadAsStringAsync(cancellationToken)}");
}
using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
using var reader = new StreamReader(stream);
var assistantResponse = new StringBuilder();
while (!reader.EndOfStream && !cancellationToken.IsCancellationRequested)
{
var line = await reader.ReadLineAsync();
if (string.IsNullOrWhiteSpace(line) || !line.StartsWith("data:"))
{
continue;
}
var json = line["data:".Length..].Trim();
if (json == "[DONE]")
{
break;
}
var content = ParseStreamingResponse(json);
if (!string.IsNullOrEmpty(content))
{
assistantResponse.Append(content);
yield return content;
}
}
// Add assistant response to conversation history
if (assistantResponse.Length > 0)
{
session.AddMessage("assistant", assistantResponse.ToString());
}
}
/// <inheritdoc />
public ConversationSession GetOrCreateSession(string sessionId = null)
{
lock (_sessionsLock)
{
if (string.IsNullOrEmpty(sessionId))
{
sessionId = Guid.NewGuid().ToString();
}
if (!_sessions.TryGetValue(sessionId, out var session))
{
session = new ConversationSession
{
Id = sessionId,
MaxMessages = Options.MaxMessages
};
_sessions[sessionId] = session;
}
return session;
}
}
/// <inheritdoc />
public void ClearSession(string sessionId)
{
lock (_sessionsLock)
{
if (_sessions.TryGetValue(sessionId, out var session))
{
session.Clear();
}
}
}
/// <inheritdoc />
public IEnumerable<ConversationSession> GetActiveSessions()
{
lock (_sessionsLock)
{
return _sessions.Values.ToList();
}
}
/// <inheritdoc />
public void CleanupOldSessions(int maxAgeHours = 24)
{
lock (_sessionsLock)
{
var cutoffTime = DateTime.Now.AddHours(-maxAgeHours);
var sessionsToRemove = _sessions.Values
.Where(s => s.LastUpdated < cutoffTime)
.Select(s => s.Id)
.ToList();
foreach (var sessionId in sessionsToRemove)
{
_sessions.Remove(sessionId);
}
}
}
private static string ParseStreamingResponse(string json)
{
try
{
var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
if (!root.TryGetProperty("choices", out var choices) || choices.GetArrayLength() == 0)
{
return string.Empty;
}
var firstChoice = choices[0];
if (!firstChoice.TryGetProperty("delta", out var delta))
{
return string.Empty;
}
if (delta.TryGetProperty("content", out var contentElement))
{
return contentElement.GetString() ?? string.Empty;
}
return string.Empty;
}
catch
{
return string.Empty;
}
}
}
/// <summary>
/// Extension methods for configuring AIChatService in the dependency injection container.
/// </summary>
public static class AIChatServiceExtensions
{
/// <summary>
/// Adds the AIChatService to the service collection with the specified configuration.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="configureOptions">The action to configure the AIChatService options.</param>
/// <returns>The updated service collection.</returns>
public static IServiceCollection AddAIChatService(this IServiceCollection services, Action<AIChatServiceOptions> configureOptions)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (configureOptions == null)
{
throw new ArgumentNullException(nameof(configureOptions));
}
services.Configure(configureOptions);
services.AddScoped<IAIChatService, AIChatService>();
return services;
}
/// <summary>
/// Adds the AIChatService to the service collection with default options.
/// </summary>
/// <param name="services">The service collection.</param>
/// <returns>The updated service collection.</returns>
public static IServiceCollection AddAIChatService(this IServiceCollection services)
{
services.AddOptions<AIChatServiceOptions>();
services.AddScoped<IAIChatService, AIChatService>();
return services;
}
}

View File

@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;
using System.Linq;
using System.Linq.Dynamic.Core;
using Radzen.Blazor.Rendering;
using System.Threading.Tasks;
using System.Net.Mime;
@@ -410,7 +409,7 @@ namespace Radzen.Blazor
if (IsDate(CategoryProperty) || IsNumeric(CategoryProperty))
{
Items = Items.AsQueryable().OrderBy(DynamicLinqCustomTypeProvider.ParsingConfig, CategoryProperty).ToList();
Items = Items.AsQueryable().OrderBy(CategoryProperty).ToList();
}
}

View File

@@ -9,13 +9,13 @@ using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Linq.Dynamic.Core.Parser;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Metadata;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
@@ -293,6 +293,7 @@ namespace Radzen
services.AddScoped<TooltipService>();
services.AddScoped<ContextMenuService>();
services.AddScoped<ThemeService>();
services.AddAIChatService();
return services;
}
@@ -372,6 +373,40 @@ namespace Radzen
December = 11,
}
/// <summary>
/// Specifies the time unit of <see cref="TimeSpan"/>.
/// </summary>
public enum TimeSpanUnit
{
/// <summary>
/// Day.
/// </summary>
Day = 0,
/// <summary>
/// Hour.
/// </summary>
Hour = 1,
/// <summary>
/// Minute.
/// </summary>
Minute = 2,
/// <summary>
/// Second.
/// </summary>
Second = 3,
/// <summary>
/// Millisecond.
/// </summary>
Millisecond = 4
#if NET7_0_OR_GREATER
,
/// <summary>
/// Microsecond.
/// </summary>
Microsecond = 5
#endif
}
/// <summary>
/// Html editor mode (Rendered or Raw). Also used for toolbar buttons to enable/disable according to mode.
/// </summary>
@@ -734,9 +769,14 @@ namespace Radzen
public AppointmentData Appointment { get; set; }
/// <summary>
/// Gets or sets the time span.
/// Gets or sets the time span which represents the difference between slot start and appointment start.
/// </summary>
public TimeSpan TimeSpan { get; set; }
/// <summary>
/// Gets or sets the date of the slot where the appointment is moved.
/// </summary>
public DateTime SlotDate { get; set; }
}
/// <summary>
@@ -1052,6 +1092,21 @@ namespace Radzen
/// </summary>
public class PreviewFileInfo : FileInfo
{
/// <summary>
/// Initializes a new instance of PreviewFileInfo from a browser file.
/// </summary>
/// <param name="files"></param>
public PreviewFileInfo(IBrowserFile files) : base(files)
{
}
/// <summary>
/// Initializes a new, empty instance of PreviewFileInfo.
/// </summary>
public PreviewFileInfo()
{
}
/// <summary>
/// Gets the URL of the previewed file.
/// </summary>
@@ -1063,11 +1118,41 @@ namespace Radzen
/// </summary>
public class Query
{
Func<string> _getFilter;
internal Func<string> GetFilter
{
get
{
return _getFilter;
}
set
{
_filter = null;
_getFilter = value;
}
}
string _filter;
/// <summary>
/// Gets or sets the filter.
/// Gets the filter expression as a string.
/// </summary>
/// <value>The filter.</value>
public string Filter { get; set; }
public string Filter
{
get
{
if (_filter == null && GetFilter != null)
{
_filter = GetFilter();
}
return _filter;
}
set
{
_filter = value;
}
}
/// <summary>
/// Gets the filter expression as a collection of filter descriptors.
/// </summary>
@@ -2288,6 +2373,19 @@ namespace Radzen
/// </summary>
/// <value>The property.</value>
public string Property { get; set; }
/// <summary>
/// Gets or sets the property type.
/// </summary>
/// <value>The property type.</value>
[JsonIgnore]
public Type Type { get; set; }
/// <summary>
/// Gets or sets the name of the filtered property.
/// </summary>
/// <value>The property.</value>
public string FilterProperty { get; set; }
/// <summary>
/// Gets or sets the value to filter by.
/// </summary>
@@ -2326,6 +2424,18 @@ namespace Radzen
/// <value>The property.</value>
public string Property { get; set; }
/// <summary>
/// Gets or sets the property type.
/// </summary>
/// <value>The property type.</value>
public Type Type { get; set; }
/// <summary>
/// Gets or sets the name of the filtered property.
/// </summary>
/// <value>The property.</value>
public string FilterProperty { get; set; }
/// <summary>
/// Gets or sets the value to filter by.
/// </summary>
@@ -2429,6 +2539,43 @@ namespace Radzen
public int Level { get; set; }
}
/// <summary>
/// The result of a call to a <see cref="QueryableExtension"/>.GroupByMany() overload.
/// </summary>
public class GroupResult
{
/// <summary>
/// The key value of the group.
/// </summary>
public dynamic Key { get; internal set; } = null!;
/// <summary>
/// The number of resulting elements in the group.
/// </summary>
public int Count { get; internal set; }
/// <summary>
/// The resulting elements in the group.
/// </summary>
public IEnumerable Items { get; internal set; }
/// <summary>
/// The resulting subgroups in the group.
/// </summary>
public IEnumerable<GroupResult> Subgroups { get; internal set; }
/// <summary>
/// Returns a <see cref="System.String" /> showing the key of the group and the number of items in the group.
/// </summary>
/// <returns>
/// A <see cref="System.String" /> that represents this instance.
/// </returns>
public override string ToString()
{
return string.Format(CultureInfo.CurrentCulture, "{0} ({1})", ((object)Key).ToString(), Count);
}
}
/// <summary>
/// Supplies information about a <see cref="PagedDataBoundComponent{TItem}.LoadData" /> event that is being raised.
/// </summary>
@@ -2447,11 +2594,43 @@ namespace Radzen
/// Gets the sort expression as a string.
/// </summary>
public string OrderBy { get; set; }
Func<string> _getFilter;
internal Func<string> GetFilter
{
get
{
return _getFilter;
}
set
{
_filter = null;
_getFilter = value;
}
}
string _filter;
/// <summary>
/// Gets the filter expression as a string.
/// </summary>
/// <value>The filter.</value>
public string Filter { get; set; }
public string Filter
{
get
{
if (_filter == null && GetFilter != null)
{
_filter = GetFilter();
}
return _filter;
}
set
{
_filter = value;
}
}
/// <summary>
/// Gets the filter expression as a collection of filter descriptors.
/// </summary>
@@ -2521,6 +2700,12 @@ namespace Radzen
/// <value>The filter.</value>
public string Filter { get; internal set; }
/// <summary>
/// Gets or sets filter property used to limit and distinct values, if not set, args.Data are used as values.
/// </summary>
/// <value>The filter property.</value>
public string Property { get; set; }
/// <summary>
/// Gets the column.
/// </summary>
@@ -2959,7 +3144,9 @@ namespace Radzen
{
if (propertyName.Contains("["))
{
return DynamicExpressionParser.ParseLambda<TItem, TValue>(null, false, propertyName).Compile();
var arg = Expression.Parameter(typeof(TItem));
return Expression.Lambda<Func<TItem, TValue>>(QueryableExtension.GetNestedPropertyExpression(arg, propertyName, type), arg).Compile();
}
else
{
@@ -3100,23 +3287,7 @@ namespace Radzen
/// <param name="property">The property.</param>
public static string GetProperty(string property)
{
Type type = null;
try
{
type = Type.GetType($"System.{property}");
}
catch
{
// ignore the exception and assume the property start without a type and do not need the '@' prefix
}
var propertyName = $"{(type != null ? "@" : "")}{property}";
if (propertyName.IndexOf(".") != -1)
{
return $"np({propertyName})";
}
return propertyName;
return property;
}
/// <summary>
@@ -3333,6 +3504,32 @@ namespace Radzen
return null;
}
/// <summary>
/// Gets the property by its name. If the type is an interface, it will search through all interfaces implemented by the type.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="property">The property.</param>
/// <returns>PropertyInfo.</returns>
public static PropertyInfo GetProperty(Type type, string property)
{
if (type.IsInterface)
{
var interfaces = type.GetInterfaces();
foreach (var @interface in interfaces)
{
var propertyInfo = @interface.GetProperty(property);
if (propertyInfo != null)
{
return propertyInfo;
}
}
}
return type.GetProperty(property);
}
/// <summary>
/// Gets the dynamic property expression when binding to IDictionary.
/// </summary>
@@ -3345,7 +3542,7 @@ namespace Radzen
var typeName = isEnum ? "Enum" : (Nullable.GetUnderlyingType(type) ?? type).Name;
var typeFunc = $@"{typeName}{(!isEnum && Nullable.GetUnderlyingType(type) != null ? "?" : "")}";
return $@"{typeFunc}(it[""{name}""])";
return $@"({typeFunc})it[""{name}""]";
}
}
@@ -3405,7 +3602,7 @@ namespace Radzen
/// Gets the field identifier.
/// </summary>
/// <value>The field identifier.</value>
FieldIdentifier FieldIdentifier { get; }
FieldIdentifier FieldIdentifier { get; set; }
/// <summary>
/// Sets the focus.
@@ -3810,4 +4007,42 @@ namespace Radzen
/// </summary>
Right
}
/// <summary>
/// Skeleton shape variants.
/// </summary>
public enum SkeletonVariant
{
/// <summary>
/// Text skeleton shape.
/// </summary>
Text,
/// <summary>
/// Circular skeleton shape.
/// </summary>
Circular,
/// <summary>
/// Rectangular skeleton shape.
/// </summary>
Rectangular
}
/// <summary>
/// Skeleton animation types.
/// </summary>
public enum SkeletonAnimation
{
/// <summary>
/// No animation.
/// </summary>
None,
/// <summary>
/// Wave animation.
/// </summary>
Wave,
/// <summary>
/// Pulse animation.
/// </summary>
Pulse
}
}

View File

@@ -7,6 +7,25 @@ using Microsoft.JSInterop;
namespace Radzen
{
/// <summary>
/// Specifies the SameSite attribute for the cookie.
/// </summary>
public enum CookieSameSiteMode
{
/// <summary>
/// No SameSite attribute.
/// </summary>
None,
/// <summary>
/// Lax SameSite attribute.
/// </summary>
Lax,
/// <summary>
/// Strict SameSite attribute.
/// </summary>
Strict
}
/// <summary>
/// Options for the <see cref="CookieThemeService" />.
/// </summary>
@@ -21,6 +40,16 @@ namespace Radzen
/// Gets or sets the cookie duration.
/// </summary>
public TimeSpan Duration { get; set; } = TimeSpan.FromDays(365);
/// <summary>
/// Gets or sets a value indicating whether to use secure cookies.
/// </summary>
public bool IsSecure { get; set; } = false;
/// <summary>
/// Gets or sets the SameSite attribute for the cookie.
/// </summary>
public CookieSameSiteMode? SameSite { get; set; } = null;
}
/// <summary>
@@ -75,8 +104,19 @@ namespace Radzen
private void OnThemeChanged()
{
var expiration = DateTime.Now.Add(options.Duration);
var cookie = $"{options.Name}={themeService.Theme}; expires={expiration:R}; path=/";
_ = jsRuntime.InvokeVoidAsync("eval", $"document.cookie = \"{options.Name}={themeService.Theme}; expires={expiration:R}; path=/\"");
if (options.SameSite.HasValue)
{
cookie += $"; SameSite={options.SameSite}";
}
if (options.IsSecure)
{
cookie += "; Secure";
}
_ = jsRuntime.InvokeVoidAsync("eval", $"document.cookie = \"{cookie}\"");
}
}

View File

@@ -3,12 +3,9 @@ using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Web;
using Radzen.Blazor;
using Radzen.Blazor.Rendering;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Linq.Expressions;
using System.Threading.Tasks;
@@ -106,13 +103,13 @@ namespace Radzen
/// <summary>
/// The value
/// </summary>
object _value;
private T _value = default;
/// <summary>
/// Gets or sets the value.
/// </summary>
/// <value>The value.</value>
[Parameter]
public object Value
public T Value
{
get
{
@@ -120,10 +117,14 @@ namespace Radzen
}
set
{
if (_value != value)
if (value == null || value.Equals("null"))
{
_value = object.Equals(value, "null") ? null : value;
_value = default;
return;
}
if (!value.Equals(_value))
_value = value;
}
}
@@ -188,7 +189,7 @@ namespace Radzen
if (_data != value)
{
_view = null;
_value = null;
_value = default;
_data = value;
StateHasChanged();
}
@@ -262,23 +263,7 @@ namespace Radzen
{
if (!string.IsNullOrEmpty(searchText))
{
var ignoreCase = FilterCaseSensitivity == FilterCaseSensitivity.CaseInsensitive;
var query = new List<string>();
if (!string.IsNullOrEmpty(TextProperty))
{
query.Add(TextProperty);
}
if (ignoreCase)
{
query.Add("ToLower()");
}
query.Add($"{Enum.GetName(typeof(StringFilterOperator), FilterOperator)}(@0)");
_view = Query.Where(DynamicLinqCustomTypeProvider.ParsingConfig, string.Join(".", query), ignoreCase ? searchText.ToLower() : searchText);
_view = Query.Where(TextProperty, searchText, FilterOperator, FilterCaseSensitivity);
}
else
{
@@ -290,6 +275,8 @@ namespace Radzen
}
}
internal IEnumerable GetView() => View;
/// <summary>
/// Gets or sets the edit context.
/// </summary>
@@ -301,7 +288,8 @@ namespace Radzen
/// Gets the field identifier.
/// </summary>
/// <value>The field identifier.</value>
public FieldIdentifier FieldIdentifier { get; private set; }
[Parameter]
public FieldIdentifier FieldIdentifier { get; set; }
/// <summary>
/// Gets or sets the value expression.
@@ -316,26 +304,25 @@ namespace Radzen
/// <returns>A Task representing the asynchronous operation.</returns>
public override async Task SetParametersAsync(ParameterView parameters)
{
var searchTextChanged = parameters.DidParameterChange(nameof(SearchText), SearchText);
if (searchTextChanged)
{
searchText = parameters.GetValueOrDefault<string>(SearchText);
}
// check for changes before setting the properties through the base call
var dataChanged = parameters.DidParameterChange(nameof(Data), Data);
var disabledChanged = parameters.DidParameterChange(nameof(Disabled), Disabled);
// allow the base class to process parameters and set the properties
// after this call the parameters object should be considered stale
await base.SetParametersAsync(parameters);
// handle changes
if (dataChanged)
{
await OnDataChanged();
}
var disabledChanged = parameters.DidParameterChange(nameof(Disabled), Disabled);
var result = base.SetParametersAsync(parameters);
if (EditContext != null && ValueExpression != null && FieldIdentifier.Model != EditContext.Model)
if (EditContext != null && (ValueExpression != null || ValueChanged.HasDelegate) && FieldIdentifier.Model != EditContext.Model)
{
FieldIdentifier = FieldIdentifier.Create(ValueExpression);
FieldIdentifier = ValueExpression != null
? FieldIdentifier.Create(ValueExpression)
: FieldIdentifier.Create(() => Value);
EditContext.OnValidationStateChanged -= ValidationStateChanged;
EditContext.OnValidationStateChanged += ValidationStateChanged;
}
@@ -344,8 +331,6 @@ namespace Radzen
{
FormFieldContext?.DisabledChanged(Disabled);
}
await result;
}
/// <summary>

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,6 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Threading.Tasks;
namespace Radzen
@@ -213,6 +212,13 @@ namespace Radzen
[Parameter]
public bool Multiple { get; set; }
/// <summary>
/// Gets or sets a value indicating the selected index should reset to the top item when filtering, resulting in a down arrow action will start moving from the top.
/// </summary>
/// <value><c>true</c> to reset selected index to -1 when filtering; otherwise, <c>false</c>.</value>
[Parameter]
public bool ResetSelectedIndexOnFilter { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the user can select all values in multiple selection. Set to <c>true</c> by default.
/// </summary>
@@ -277,6 +283,18 @@ namespace Radzen
/// The selected item
/// </summary>
protected object selectedItem = null;
Type GetItemType(IEnumerable items)
{
var firstType = items.Cast<object>().FirstOrDefault()?.GetType() ?? typeof(object);
var hasNull = items.Cast<object>().Where(i => i == null).Any();
if (hasNull && firstType.IsValueType && Nullable.GetUnderlyingType(firstType) == null)
{
return typeof(Nullable<>).MakeGenericType(firstType);
}
return firstType;
}
/// <summary>
/// Selects all.
@@ -300,37 +318,18 @@ namespace Radzen
if (!string.IsNullOrEmpty(ValueProperty))
{
System.Reflection.PropertyInfo pi = PropertyAccess.GetElementType(Data.GetType()).GetProperty(ValueProperty);
var elementType = PropertyAccess.GetElementType(Data.GetType());
System.Reflection.PropertyInfo pi = PropertyAccess.GetProperty(elementType, ValueProperty);
internalValue = selectedItems.Select(i => GetItemOrValueFromProperty(i, ValueProperty)).AsQueryable().Cast(pi.PropertyType);
}
else
{
var type = typeof(T).IsGenericType ? typeof(T).GetGenericArguments()[0] : typeof(T);
var type = typeof(T).IsGenericType ? typeof(T).GetGenericArguments()[0] :
QueryableExtension.IsEnumerable(typeof(T)) ? GetItemType(selectedItems) : typeof(T);
internalValue = selectedItems.AsQueryable().Cast(type);
}
if (typeof(IList).IsAssignableFrom(typeof(T)))
{
var list = (IList)Activator.CreateInstance(typeof(T));
foreach (var i in (IEnumerable)internalValue)
{
list.Add(i);
}
await ValueChanged.InvokeAsync((T)(object)list);
}
else if (typeof(T).IsGenericType && typeof(ICollection<>).MakeGenericType(typeof(T).GetGenericArguments()[0]).IsAssignableFrom(typeof(T)))
{
var list = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(typeof(T).GetGenericArguments()[0]));
foreach (var i in (IEnumerable)internalValue)
{
list.Add(i);
}
await ValueChanged.InvokeAsync((T)(object)list);
}
else
{
await ValueChanged.InvokeAsync((T)internalValue);
}
await collectionAssignment.MakeAssignment((IEnumerable)internalValue, ValueChanged);
if (FieldIdentifier.FieldName != null) { EditContext?.NotifyFieldChanged(FieldIdentifier); }
await Change.InvokeAsync(internalValue);
@@ -341,15 +340,17 @@ namespace Radzen
internal bool IsAllSelected()
{
List<object> notDisabledItemsInList = View.Cast<object>().ToList()
.Where(i => disabledPropertyGetter == null || disabledPropertyGetter(i) as bool? != true)
.ToList();
if (LoadData.HasDelegate && !string.IsNullOrEmpty(ValueProperty))
{
return View != null && View.Cast<object>().ToList()
.Where(i => disabledPropertyGetter != null ? disabledPropertyGetter(i) as bool? != true : true)
return View != null && notDisabledItemsInList.Count > 0 && notDisabledItemsInList
.All(i => IsItemSelectedByValue(GetItemOrValueFromProperty(i, ValueProperty)));
}
return View != null && selectedItems.Count == View.Cast<object>().ToList()
.Where(i => disabledPropertyGetter != null ? disabledPropertyGetter(i) as bool? != true : true).Count();
return View != null && notDisabledItemsInList.Count > 0 && selectedItems.Count == notDisabledItemsInList.Count;
}
/// <summary>
@@ -373,7 +374,7 @@ namespace Radzen
await SearchTextChanged.InvokeAsync(searchText);
await JSRuntime.InvokeAsync<string>("Radzen.setInputValue", search, "");
internalValue = default(T);
internalValue = collectionAssignment.GetCleared();
selectedItem = null;
selectedItems.Clear();
@@ -434,7 +435,7 @@ namespace Radzen
var type = query.ElementType;
if (type == typeof(object) && typeof(EnumerableQuery).IsAssignableFrom(query.GetType()) && query.Any())
if (type == typeof(object) && typeof(EnumerableQuery).IsAssignableFrom(query.GetType()) && query.Cast<object>().Any())
{
var firstElement = query.Cast<object>().FirstOrDefault(i => i != null);
if (firstElement != null)
@@ -628,7 +629,7 @@ namespace Radzen
/// <param name="shouldSelectOnChange">Should select item on item change with keyboard.</param>
protected virtual async System.Threading.Tasks.Task HandleKeyPress(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs args, bool isFilter = false, bool? shouldSelectOnChange = null)
{
if (Disabled)
if (Disabled || Data == null)
return;
List<object> items = Enumerable.Empty<object>().ToList();
@@ -678,7 +679,7 @@ namespace Radzen
//
}
}
else if (key == "Enter" || key == "NumpadEnter")
else if (key == "Enter" || key == "NumpadEnter" || (key == "Space" && !isFilter))
{
preventKeydown = true;
@@ -698,11 +699,14 @@ namespace Radzen
if (!popupOpened)
{
await OpenPopup(key, isFilter);
if(key != "Space")
{
await OpenPopup(key, isFilter);
}
}
else
{
if (!Multiple)
if (!Multiple && !isFilter)
{
await ClosePopup(key);
}
@@ -716,6 +720,8 @@ namespace Radzen
}
else if (key == "Escape" || key == "Tab")
{
preventKeydown = false;
await ClosePopup(key);
}
else if (key == "Delete" && AllowClear)
@@ -737,15 +743,20 @@ namespace Radzen
{
preventKeydown = true;
if (ResetSelectedIndexOnFilter)
{
selectedIndex = -1;
}
Debounce(DebounceFilter, FilterDelay);
}
else
else if (args.Key.Length == 1 && !args.CtrlKey && !args.AltKey && !args.ShiftKey)
{
// searching for element
var filteredItems = (!string.IsNullOrEmpty(TextProperty) ?
Query.Where(TextProperty, args.Key, StringFilterOperator.StartsWith, FilterCaseSensitivity.CaseInsensitive) :
Query)
.Cast<object>()
.ToList();
.Cast(Query.ElementType).Cast<dynamic>().ToList();
if (previousKey != args.Key)
@@ -757,7 +768,7 @@ namespace Radzen
itemIndex = itemIndex + 1 >= filteredItems.Count() ? 0 : itemIndex + 1;
var itemToSelect = filteredItems.ElementAtOrDefault(itemIndex);
if (itemToSelect != null)
if (itemToSelect is not null)
{
if (!Multiple)
{
@@ -921,33 +932,38 @@ namespace Radzen
/// <returns>A Task representing the asynchronous operation.</returns>
public override async Task SetParametersAsync(ParameterView parameters)
{
// check for changes before setting the properties through the base call
var selectedItemChanged = parameters.DidParameterChange(nameof(SelectedItem), SelectedItem);
var visibleChanged = parameters.DidParameterChange(nameof(Visible), Visible);
var valueChanged = parameters.DidParameterChange(nameof(Value), Value);
// manually set properties that are not set through the base call
if (valueChanged)
{
internalValue = parameters.GetValueOrDefault<object>(nameof(Value));
if (PreserveCollectionOnSelection)
{
collectionAssignment = new ReferenceGenericCollectionAssignment((T)internalValue);
}
}
var pageSize = parameters.GetValueOrDefault<int>(nameof(PageSize));
if (pageSize != default(int))
{
PageSize = pageSize;
}
// allow the base class to process parameters and set the properties
// after this call the parameters object should be considered stale
await base.SetParametersAsync(parameters);
var selectedItemChanged = parameters.DidParameterChange(nameof(SelectedItem), SelectedItem);
// handle changes
if (selectedItemChanged)
{
await SelectItem(selectedItem, false);
}
var shouldClose = false;
if (parameters.DidParameterChange(nameof(Visible), Visible))
{
var visible = parameters.GetValueOrDefault<bool>(nameof(Visible));
shouldClose = !visible;
}
if (parameters.DidParameterChange(nameof(Value), Value))
{
internalValue = parameters.GetValueOrDefault<object>(nameof(Value));
}
await base.SetParametersAsync(parameters);
var shouldClose = visibleChanged && !Visible;
if (shouldClose && !firstRender)
{
await JSRuntime.InvokeVoidAsync("Radzen.destroyPopup", PopupID);
@@ -1097,6 +1113,12 @@ namespace Radzen
}
internal object internalValue;
/// <summary>
/// Will add/remove selected items from a bound ICollection&lt;T&gt;, instead of replacing it.
/// </summary>
protected bool PreserveCollectionOnSelection = false;
private DefaultCollectionAssignment collectionAssignment = new();
/// <summary>
/// Selects the item.
@@ -1135,7 +1157,8 @@ namespace Radzen
if (!string.IsNullOrEmpty(ValueProperty))
{
System.Reflection.PropertyInfo pi = PropertyAccess.GetElementType(Data.GetType()).GetProperty(ValueProperty);
var elementType = PropertyAccess.GetElementType(Data.GetType());
System.Reflection.PropertyInfo pi = PropertyAccess.GetProperty(elementType, ValueProperty);
internalValue = selectedItems.Select(i => GetItemOrValueFromProperty(i, ValueProperty)).AsQueryable().Cast(pi.PropertyType);
}
else
@@ -1143,7 +1166,7 @@ namespace Radzen
var query = Data.AsQueryable();
var elementType = query.ElementType;
if (elementType == typeof(object) && typeof(EnumerableQuery).IsAssignableFrom(query.GetType()) && query.Any())
if (elementType == typeof(object) && typeof(EnumerableQuery).IsAssignableFrom(query.GetType()) && query.Cast<object>().Any())
{
var firstElement = query.Cast<object>().FirstOrDefault(i => i != null);
if (firstElement != null)
@@ -1166,41 +1189,13 @@ namespace Radzen
{
if (ValueChanged.HasDelegate)
{
if (typeof(IList).IsAssignableFrom(typeof(T)))
if (Multiple)
{
if (object.Equals(internalValue, null))
{
await ValueChanged.InvokeAsync(default(T));
}
else
{
var list = (IList)Activator.CreateInstance(typeof(T));
foreach (var i in (IEnumerable)internalValue)
{
list.Add(i);
}
await ValueChanged.InvokeAsync((T)(object)list);
}
}
else if (typeof(T).IsGenericType && typeof(ICollection<>).MakeGenericType(typeof(T).GetGenericArguments()[0]).IsAssignableFrom(typeof(T)))
{
if (object.Equals(internalValue, null))
{
await ValueChanged.InvokeAsync(default(T));
}
else
{
var list = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(typeof(T).GetGenericArguments()[0]));
foreach (var i in (IEnumerable)internalValue)
{
list.Add(i);
}
await ValueChanged.InvokeAsync((T)(object)list);
}
await collectionAssignment.MakeAssignment((IEnumerable)internalValue, ValueChanged);
}
else
{
await ValueChanged.InvokeAsync(object.Equals(internalValue, null) ? default(T) : (T)internalValue);
await ValueChanged.InvokeAsync((T)internalValue);
}
}
@@ -1260,7 +1255,16 @@ namespace Radzen
}
else
{
SelectedItem = view.AsQueryable().Where(DynamicLinqCustomTypeProvider.ParsingConfig, $@"{ValueProperty} == @0", value).FirstOrDefault();
SelectedItem = view.AsQueryable().Where(new FilterDescriptor[]
{
new FilterDescriptor()
{
Property = ValueProperty,
FilterValue = value
}
},
LogicalFilterOperator.And,
FilterCaseSensitivity.Default).FirstOrDefault();
}
}
else
@@ -1277,7 +1281,7 @@ namespace Radzen
{
if (!string.IsNullOrEmpty(ValueProperty))
{
foreach (object v in values.ToDynamicList())
foreach (object v in values.Cast<dynamic>().ToList())
{
dynamic item;
@@ -1287,7 +1291,16 @@ namespace Radzen
}
else
{
item = view.AsQueryable().Where(DynamicLinqCustomTypeProvider.ParsingConfig, $@"{ValueProperty} == @0", v).FirstOrDefault();
item = view.AsQueryable().Where(new FilterDescriptor[]
{
new FilterDescriptor()
{
Property = ValueProperty,
FilterValue = v
}
},
LogicalFilterOperator.And,
FilterCaseSensitivity.Default).FirstOrDefault();
}
if (!object.Equals(item, null) && !selectedItems.AsQueryable().Where(i => object.Equals(GetItemOrValueFromProperty(i, ValueProperty), v)).Any())
@@ -1300,7 +1313,6 @@ namespace Radzen
{
selectedItems = values.Cast<object>().ToHashSet(ItemComparer);
}
}
}
}
@@ -1337,5 +1349,121 @@ namespace Radzen
keys.Clear();
}
private class DefaultCollectionAssignment
{
public virtual async Task MakeAssignment(IEnumerable selectedItems, EventCallback<T> valueChanged)
{
if (typeof(IList).IsAssignableFrom(typeof(T)))
{
if (object.Equals(selectedItems, null))
{
await valueChanged.InvokeAsync(default(T));
}
else
{
var list = (IList)Activator.CreateInstance(typeof(T));
foreach (var i in (IEnumerable)selectedItems)
{
list.Add(i);
}
await valueChanged.InvokeAsync((T)(object)list);
}
}
else if (typeof(T).IsGenericType && typeof(ICollection<>).MakeGenericType(typeof(T).GetGenericArguments()[0]).IsAssignableFrom(typeof(T)))
{
if (object.Equals(selectedItems, null))
{
await valueChanged.InvokeAsync(default(T));
}
else
{
var list = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(typeof(T).GetGenericArguments()[0]));
foreach (var i in (IEnumerable)selectedItems)
{
list.Add(i);
}
await valueChanged.InvokeAsync((T)(object)list);
}
}
else
{
await valueChanged.InvokeAsync(object.Equals(selectedItems, null) ? default(T) : (T)selectedItems);
}
}
public virtual T GetCleared()
{
return default(T);
}
}
private class ReferenceGenericCollectionAssignment : DefaultCollectionAssignment
{
private readonly T originalCollection;
private readonly bool canHandle;
private readonly System.Reflection.MethodInfo clearMethod;
private readonly System.Reflection.MethodInfo addMethod;
private readonly System.Reflection.MethodInfo removeMethod;
public ReferenceGenericCollectionAssignment(T originalCollection)
{
this.originalCollection = originalCollection;
// Pre-calculate if we can handle this instance and get method info
if (originalCollection != null)
{
var actualType = originalCollection.GetType();
if (actualType.IsGenericType && !actualType.IsArray)
{
var elementType = actualType.GetGenericArguments()[0];
var genericCollectionType = typeof(ICollection<>).MakeGenericType(elementType);
if (genericCollectionType.IsAssignableFrom(actualType))
{
clearMethod = actualType.GetMethod("Clear");
addMethod = actualType.GetMethod("Add");
removeMethod = typeof(T).GetMethod("Remove");
canHandle = true;
}
}
}
}
public override async Task MakeAssignment(IEnumerable selectedItems, EventCallback<T> valueChanged)
{
if (!canHandle)
{
// Fallback to default behavior when we can't handle the type
await base.MakeAssignment(selectedItems, valueChanged);
return;
}
var currentItems = selectedItems.Cast<object>().ToHashSet();
var existingItems = ((IEnumerable)originalCollection).Cast<object>().ToHashSet();
foreach (var i in currentItems)
{
if (!existingItems.Contains(i))
addMethod.Invoke(originalCollection, [i]);
}
foreach (var i in existingItems)
{
if (!currentItems.Contains(i))
removeMethod.Invoke(originalCollection, [i]);
}
await valueChanged.InvokeAsync(originalCollection);
}
public override T GetCleared()
{
if (canHandle)
{
clearMethod.Invoke(originalCollection, null);
return originalCollection;
}
return base.GetCleared();
}
}
}
}

View File

@@ -0,0 +1,134 @@
using Radzen;
using System.Linq.Expressions;
using System.Text.RegularExpressions;
namespace System.Linq.Dynamic.Core
{
/// <summary>
/// Class DynamicExtensions used to replace System.Linq.Dynamic.Core library.
/// </summary>
public static class DynamicExtensions
{
static readonly Func<string, Type> typeLocator = type => AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a => a.GetTypes()).FirstOrDefault(t => t.FullName.Replace("+", ".") == type);
/// <summary>
/// Filters using the specified filter descriptors.
/// </summary>
public static IQueryable<T> Where<T>(
this IQueryable<T> source,
string predicate,
object[] parameters = null, object[] otherParameters = null)
{
try
{
if (parameters != null && !string.IsNullOrEmpty(predicate))
{
predicate = Regex.Replace(predicate, @"@(\d+)", match =>
{
int index = int.Parse(match.Groups[1].Value);
if (index >= parameters.Length)
throw new InvalidOperationException($"No parameter provided for {match.Value}");
return ExpressionSerializer.FormatValue(parameters[index]);
});
}
predicate = (predicate == "true" ? "" : predicate)
.Replace("DateTime(", "DateTime.Parse(")
.Replace("DateTimeOffset(", "DateTimeOffset.Parse(")
.Replace("DateOnly(", "DateOnly.Parse(")
.Replace("Guid(", "Guid.Parse(")
.Replace(" = ", " == ");
return !string.IsNullOrEmpty(predicate) ?
source.Where(ExpressionParser.ParsePredicate<T>(predicate, typeLocator)) : source;
}
catch (Exception ex)
{
throw new InvalidOperationException($"Invalid predicate: {predicate}", ex);
}
}
/// <summary>
/// Sorts the elements of a sequence in ascending or descending order according to a key.
/// </summary>
public static IOrderedQueryable<T> OrderBy<T>(
this IQueryable<T> source,
string selector,
object[] parameters = null)
{
try
{
return QueryableExtension.OrderBy(source, selector);
}
catch (Exception ex)
{
throw new InvalidOperationException($"Invalid selector: {selector}.", ex);
}
}
/// <summary>
/// Projects each element of a sequence into a collection of property values.
/// </summary>
public static IQueryable Select<T>(this IQueryable<T> source, string selector, object[] parameters = null)
{
if (source.ElementType == typeof(object))
{
var elementType = source.ElementType;
if (source.Expression is MethodCallExpression methodCall && methodCall.Method.Name == "Cast")
{
elementType = methodCall.Arguments[0].Type.GetGenericArguments().FirstOrDefault() ?? typeof(object);
}
else if (typeof(EnumerableQuery).IsAssignableFrom(source.GetType()))
{
elementType = source.FirstOrDefault()?.GetType() ?? typeof(object);
}
return source.Cast(elementType).Select(selector, expression => ExpressionParser.ParseLambda(expression, elementType));
}
return source.Select(selector, expression => ExpressionParser.ParseLambda<T>(expression));
}
/// <summary>
/// Projects each element of a sequence into a collection of property values.
/// </summary>
public static IQueryable Select(this IQueryable source, string selector, object[] parameters = null)
{
return source.Select(selector, expression => ExpressionParser.ParseLambda(expression, source.ElementType));
}
private static IQueryable Select(this IQueryable source, string selector, Func<string, LambdaExpression> lambdaCreator)
{
try
{
if (string.IsNullOrEmpty(selector))
{
return source;
}
if (!selector.Contains("=>"))
{
var properties = selector
.Replace("new (", "").Replace(")", "").Replace("new {", "").Replace("}", "").Trim()
.Split(",", StringSplitOptions.RemoveEmptyEntries);
selector = string.Join(", ", properties
.Select(s => (s.Contains(" as ") ? s.Split(" as ").LastOrDefault().Trim().Replace(".", "_") : s.Trim().Replace(".", "_")) +
" = " + $"it.{s.Split(" as ").FirstOrDefault().Replace(".", "?.").Trim()}"));
}
var lambda = lambdaCreator(selector.Contains("=>") ? selector : $"it => new {{ {selector} }}");
return source.Provider.CreateQuery(Expression.Call(typeof(Queryable), nameof(Queryable.Select),
[source.ElementType, lambda.Body.Type], source.Expression, Expression.Quote(lambda)));
}
catch (Exception ex)
{
throw new InvalidOperationException($"Invalid selector: {selector}.", ex);
}
}
}
}

View File

@@ -1,18 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq.Dynamic.Core;
using System.Linq.Dynamic.Core.CustomTypeProviders;
using System.Reflection;
namespace Radzen.Blazor
{
class DynamicLinqCustomTypeProvider : IDynamicLinqCustomTypeProvider
{
static readonly HashSet<Type> empty = [];
public HashSet<Type> GetCustomTypes() => empty;
public Dictionary<Type, List<MethodInfo>> GetExtensionMethods() => new Dictionary<Type, List<MethodInfo>>();
public Type ResolveType(string typeName) => throw new NotSupportedException();
public Type ResolveTypeBySimpleName(string simpleTypeName) => throw new NotSupportedException();
public static ParsingConfig ParsingConfig = new() { CustomTypeProvider = new DynamicLinqCustomTypeProvider(), AllowEqualsAndToStringMethodsOnObject = true };
}
}

View File

@@ -0,0 +1,56 @@
using System;
using System.Reflection;
using System.Reflection.Emit;
static class DynamicTypeFactory
{
public static Type CreateType(string typeName, string[] propertyNames, Type[] propertyTypes)
{
if (propertyNames.Length != propertyTypes.Length)
{
throw new ArgumentException("Property names and types count mismatch.");
}
var assemblyName = new AssemblyName("DynamicTypesAssembly");
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule("DynamicTypesModule");
var typeBuilder = moduleBuilder.DefineType(typeName, TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.Sealed);
for (int i = 0; i < propertyNames.Length; i++)
{
var fieldBuilder = typeBuilder.DefineField("_" + propertyNames[i], propertyTypes[i], FieldAttributes.Private);
var propertyBuilder = typeBuilder.DefineProperty(propertyNames[i], PropertyAttributes.None, propertyTypes[i], null);
var getterMethod = typeBuilder.DefineMethod(
"get_" + propertyNames[i],
MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig,
propertyTypes[i],
Type.EmptyTypes);
var getterIl = getterMethod.GetILGenerator();
getterIl.Emit(OpCodes.Ldarg_0);
getterIl.Emit(OpCodes.Ldfld, fieldBuilder);
getterIl.Emit(OpCodes.Ret);
propertyBuilder.SetGetMethod(getterMethod);
var setterMethod = typeBuilder.DefineMethod(
"set_" + propertyNames[i],
MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig,
null,
[propertyTypes[i]]);
var setterIl = setterMethod.GetILGenerator();
setterIl.Emit(OpCodes.Ldarg_0);
setterIl.Emit(OpCodes.Ldarg_1);
setterIl.Emit(OpCodes.Stfld, fieldBuilder);
setterIl.Emit(OpCodes.Ret);
propertyBuilder.SetSetMethod(setterMethod);
}
var dynamicType = typeBuilder.CreateType();
return dynamicType;
}
}

View File

@@ -0,0 +1,873 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq.Expressions;
using System.Text;
namespace Radzen;
class Token
{
public TokenType Type { get; set; }
public string Value { get; set; } = string.Empty;
public ValueKind ValueKind { get; set; } = ValueKind.None;
public int IntValue { get; internal set; }
public uint UintValue { get; internal set; }
public long LongValue { get; internal set; }
public ulong UlongValue { get; internal set; }
public decimal DecimalValue { get; internal set; }
public float FloatValue { get; internal set; }
public double DoubleValue { get; internal set; }
public Token(TokenType type)
{
Type = type;
}
public Token(TokenType type, string value)
{
Type = type;
Value = value;
}
public ConstantExpression ToConstantExpression()
{
return ValueKind switch
{
ValueKind.Null => Expression.Constant(null),
ValueKind.String => Expression.Constant(Value),
ValueKind.Character => Expression.Constant(Value[0]),
ValueKind.Int => Expression.Constant(IntValue),
ValueKind.UInt => Expression.Constant(UintValue),
ValueKind.Long => Expression.Constant(LongValue),
ValueKind.ULong => Expression.Constant(UlongValue),
ValueKind.Float => Expression.Constant(FloatValue),
ValueKind.Double => Expression.Constant(DoubleValue),
ValueKind.Decimal => Expression.Constant(DecimalValue),
ValueKind.True => Expression.Constant(true),
ValueKind.False => Expression.Constant(false),
_ => throw new InvalidOperationException($"Unsupported value kind: {ValueKind}")
};
}
}
enum ValueKind
{
None,
String,
Int,
Float,
Double,
Decimal,
Character,
Null,
True,
False,
Long,
UInt,
ULong,
}
enum TokenType
{
None,
Identifier,
EqualsEquals,
NotEquals,
EqualsGreaterThan,
StringLiteral,
NumericLiteral,
Dot,
OpenParen,
CloseParen,
Comma,
AmpersandAmpersand,
Ampersand,
BarBar,
Bar,
GreaterThan,
LessThan,
LessThanOrEqual,
GreaterThanOrEqual,
Plus,
Minus,
Star,
Slash,
CharacterLiteral,
QuestionMark,
QuestionMarkQuestionMark,
Colon,
QuestionDot,
New,
NullLiteral,
TrueLiteral,
FalseLiteral,
OpenBracket,
CloseBracket,
OpenBrace,
CloseBrace,
ExclamationMark,
Equals,
Caret,
GreaterThanGreaterThan,
LessThanLessThan,
}
static class TokenTypeExtensions
{
public static ExpressionType ToExpressionType(this TokenType tokenType)
{
return tokenType switch
{
TokenType.EqualsEquals => ExpressionType.Equal,
TokenType.NotEquals => ExpressionType.NotEqual,
TokenType.EqualsGreaterThan => ExpressionType.GreaterThanOrEqual,
TokenType.AmpersandAmpersand => ExpressionType.AndAlso,
TokenType.Ampersand => ExpressionType.And,
TokenType.BarBar => ExpressionType.OrElse,
TokenType.Bar => ExpressionType.Or,
TokenType.GreaterThan => ExpressionType.GreaterThan,
TokenType.LessThan => ExpressionType.LessThan,
TokenType.LessThanOrEqual => ExpressionType.LessThanOrEqual,
TokenType.GreaterThanOrEqual => ExpressionType.GreaterThanOrEqual,
TokenType.Plus => ExpressionType.Add,
TokenType.Minus => ExpressionType.Subtract,
TokenType.Star => ExpressionType.Multiply,
TokenType.Slash => ExpressionType.Divide,
TokenType.Caret => ExpressionType.ExclusiveOr,
TokenType.GreaterThanGreaterThan => ExpressionType.RightShift,
TokenType.LessThanLessThan => ExpressionType.LeftShift,
_ => throw new InvalidOperationException($"Unsupported token type: {tokenType}")
};
}
}
class ExpressionLexer(string expression)
{
private int position;
private char Peek(int offset = 0)
{
if (position + offset >= expression.Length)
{
return '\0';
}
return expression[position + offset];
}
private void Advance(int count)
{
position += count;
}
bool TryAdvance(char expected)
{
if (Peek(1) == expected)
{
Advance(1);
return true;
}
return false;
}
private void ScanTrivia()
{
while (char.IsWhiteSpace(Peek()))
{
Advance(1);
}
}
public static List<Token> Scan(string expression)
{
var lexer = new ExpressionLexer(expression);
return [.. lexer.Scan()];
}
public IEnumerable<Token> Scan()
{
while (position < expression.Length)
{
ScanTrivia();
var token = ScanToken();
if (token.Type == TokenType.None)
{
yield break;
}
yield return token;
}
yield return new Token(TokenType.None, string.Empty);
}
private Token ScanToken()
{
var ch = Peek();
switch (ch)
{
case '"':
return ScanStringLiteral();
case '\'':
return ScanCharacterLiteral();
case '=':
if (TryAdvance('='))
{
Advance(1);
return new Token(TokenType.EqualsEquals);
}
if (TryAdvance('>'))
{
Advance(1);
return new Token(TokenType.EqualsGreaterThan);
}
Advance(1);
return new Token(TokenType.Equals);
case '!':
if (TryAdvance('='))
{
Advance(1);
return new Token(TokenType.NotEquals);
}
Advance(1);
return new Token(TokenType.ExclamationMark);
case '>':
if (TryAdvance('='))
{
Advance(1);
return new Token(TokenType.GreaterThanOrEqual);
}
if (TryAdvance('>'))
{
Advance(1);
return new Token(TokenType.GreaterThanGreaterThan);
}
Advance(1);
return new Token(TokenType.GreaterThan);
case '<':
if (TryAdvance('<'))
{
Advance(1);
return new Token(TokenType.LessThanLessThan);
}
if (TryAdvance('='))
{
Advance(1);
return new Token(TokenType.LessThanOrEqual);
}
Advance(1);
return new Token(TokenType.LessThan);
case '+':
Advance(1);
return new Token(TokenType.Plus);
case '-':
Advance(1);
return new Token(TokenType.Minus);
case '*':
Advance(1);
return new Token(TokenType.Star);
case '/':
Advance(1);
return new Token(TokenType.Slash);
case '.':
Advance(1);
return new Token(TokenType.Dot);
case '(':
Advance(1);
return new Token(TokenType.OpenParen);
case ')':
Advance(1);
return new Token(TokenType.CloseParen);
case '[':
Advance(1);
return new Token(TokenType.OpenBracket);
case ']':
Advance(1);
return new Token(TokenType.CloseBracket);
case '{':
Advance(1);
return new Token(TokenType.OpenBrace);
case '}':
Advance(1);
return new Token(TokenType.CloseBrace);
case ',':
Advance(1);
return new Token(TokenType.Comma);
case '&':
if (TryAdvance('&'))
{
Advance(1);
return new Token(TokenType.AmpersandAmpersand);
}
Advance(1);
return new Token(TokenType.Ampersand);
case '|':
if (TryAdvance('|'))
{
Advance(1);
return new Token(TokenType.BarBar);
}
Advance(1);
return new Token(TokenType.Bar);
case '?':
if (TryAdvance('.'))
{
Advance(1);
return new Token(TokenType.QuestionDot);
}
if (TryAdvance('?'))
{
Advance(1);
return new Token(TokenType.QuestionMarkQuestionMark);
}
Advance(1);
return new Token(TokenType.QuestionMark);
case ':':
Advance(1);
return new Token(TokenType.Colon);
case '^':
Advance(1);
return new Token(TokenType.Caret);
case >= '0' and <= '9':
return ScanNumericLiteral();
case '_':
case (>= 'a' and <= 'z') or (>= 'A' and <= 'Z'):
var token = ScanIdentifier();
return token.Value switch
{
"null" => new Token(TokenType.NullLiteral) { ValueKind = ValueKind.Null },
"true" => new Token(TokenType.TrueLiteral) { ValueKind = ValueKind.True },
"false" => new Token(TokenType.FalseLiteral) { ValueKind = ValueKind.False },
"new" => new Token(TokenType.New),
_ => token
};
}
return new Token(TokenType.None, string.Empty);
}
private char ScanEscapeSequence()
{
var ch = Peek();
Advance(1);
switch (ch)
{
case '\'':
case '"':
case '\\':
break;
case '0':
ch = '\u0000';
break;
case 'a':
ch = '\u0007';
break;
case 'b':
ch = '\u0008';
break;
case 'f':
ch = '\u000c';
break;
case 'n':
ch = '\u000a';
break;
case 'r':
ch = '\u000d';
break;
case 't':
ch = '\u0009';
break;
case 'v':
ch = '\u000b';
break;
case 'u':
case 'U':
case 'x':
ch = ScanUnicodeEscapeSequence(ch);
break;
default:
throw new InvalidOperationException($"Invalid escape sequence '\\{ch}' at position {position}.");
}
return ch;
}
private char ScanUnicodeEscapeSequence(char ch)
{
var value = 0;
var count = ch == 'U' ? 8 : 4;
for (var i = 0; i < count; i++)
{
var digit = Peek();
int digitValue;
if (digit >= '0' && digit <= '9')
{
digitValue = digit - '0';
}
else if (digit >= 'a' && digit <= 'f')
{
digitValue = digit - 'a' + 10;
}
else if (digit >= 'A' && digit <= 'F')
{
digitValue = digit - 'A' + 10;
}
else if (ch != 'x')
{
throw new InvalidOperationException($"Invalid unicode escape sequence at position {position}.");
}
else
{
break;
}
value = (value << 4) + digitValue;
Advance(1);
}
return (char)value;
}
private Token ScanCharacterLiteral()
{
Advance(1);
var buffer = new StringBuilder();
while (true)
{
var ch = Peek();
switch (ch)
{
case '\0':
throw new InvalidOperationException($"Unexpected end of character literal at position {position}.");
case '\\':
Advance(1);
buffer.Append(ScanEscapeSequence());
break;
case '\'':
Advance(1);
return new Token(TokenType.CharacterLiteral, buffer.ToString())
{
ValueKind = ValueKind.Character
};
default:
if (buffer.Length > 0)
{
throw new InvalidOperationException($"Too many characters in character literal at position {position}.");
}
buffer.Append(ch);
Advance(1);
break;
}
}
}
private Token ScanStringLiteral()
{
Advance(1);
var buffer = new StringBuilder();
while (true)
{
var ch = Peek();
switch (ch)
{
case '\0':
throw new InvalidOperationException($"Unexpected end of string literal at position {position}.");
case '\\':
Advance(1);
buffer.Append(ScanEscapeSequence());
break;
case '"':
Advance(1);
return new Token(TokenType.StringLiteral, buffer.ToString())
{
ValueKind = ValueKind.String
};
default:
buffer.Append(ch);
Advance(1);
break;
}
}
}
private Token ScanNumericLiteral()
{
var buffer = new StringBuilder();
var hasDecimal = false;
var hasFSuffix = false;
var hasDSuffix = false;
var hasMSuffix = false;
var hasLSuffix = false;
var hasExponent = false;
var hasHex = false;
var hasUSuffix = false;
while (true)
{
var ch = Peek();
if (ch == '0')
{
var next = Peek(1);
if (next == 'x' || next == 'X')
{
hasHex = true;
Advance(2);
continue;
}
}
if (ch >= '0' && ch <= '9')
{
buffer.Append(ch);
Advance(1);
continue;
}
if (ch == '.')
{
if (hasDecimal)
{
throw new InvalidOperationException($"Unexpected character '{ch}' at position {position}.");
}
hasDecimal = true;
buffer.Append(ch);
Advance(1);
continue;
}
if (ch == 'l' || ch == 'L')
{
if (hasLSuffix)
{
throw new InvalidOperationException($"Unexpected character '{ch}' at position {position}.");
}
hasLSuffix = true;
Advance(1);
continue;
}
if (ch == 'u' || ch == 'U')
{
if (hasUSuffix)
{
throw new InvalidOperationException($"Unexpected character '{ch}' at position {position}.");
}
hasUSuffix = true;
Advance(1);
continue;
}
if (hasHex && ((ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F')))
{
buffer.Append(ch);
Advance(1);
continue;
}
if (ch == 'e' || ch == 'E')
{
if (hasExponent)
{
throw new InvalidOperationException($"Unexpected character '{ch}' at position {position}.");
}
hasExponent = true;
buffer.Append(ch);
Advance(1);
// Check for optional + or - after e/E
ch = Peek();
if (ch == '+' || ch == '-')
{
buffer.Append(ch);
Advance(1);
}
// Must have at least one digit after e/E
ch = Peek();
if (ch < '0' || ch > '9')
{
throw new InvalidOperationException($"Expected digit after exponent at position {position}.");
}
continue;
}
if (hasDecimal || hasExponent)
{
switch (ch)
{
case 'F':
case 'f':
hasFSuffix = true;
Advance(1);
break;
case 'D':
case 'd':
hasDSuffix = true;
Advance(1);
break;
case 'M':
case 'm':
hasMSuffix = true;
Advance(1);
break;
}
}
break;
}
var value = new Token(TokenType.NumericLiteral);
var valueKind = ValueKind.None;
if (hasDecimal || hasExponent)
{
valueKind = ValueKind.Double;
}
if (hasFSuffix)
{
valueKind = ValueKind.Float;
}
if (hasDSuffix)
{
valueKind = ValueKind.Double;
}
if (hasMSuffix)
{
valueKind = ValueKind.Decimal;
}
switch (valueKind)
{
case ValueKind.Float:
value.ValueKind = ValueKind.Float;
value.FloatValue = GetValueFloat(buffer.ToString());
break;
case ValueKind.Double:
value.ValueKind = ValueKind.Double;
value.DoubleValue = GetValueDouble(buffer.ToString());
break;
case ValueKind.Decimal:
value.ValueKind = ValueKind.Decimal;
value.DecimalValue = GetValueDecimal(buffer.ToString());
break;
default:
var val = GetValueUInt64(buffer.ToString(), hasHex);
if (!hasUSuffix && !hasLSuffix)
{
if (val <= Int32.MaxValue)
{
value.ValueKind = ValueKind.Int;
value.IntValue = (int)val;
}
else if (val <= UInt32.MaxValue)
{
value.ValueKind = ValueKind.UInt;
value.UintValue = (uint)val;
}
else if (val <= Int64.MaxValue)
{
value.ValueKind = ValueKind.Long;
value.LongValue = (long)val;
}
else
{
value.ValueKind = ValueKind.ULong;
value.UlongValue = val;
}
}
else if (hasUSuffix && !hasLSuffix)
{
if (val <= UInt32.MaxValue)
{
value.ValueKind = ValueKind.UInt;
value.UintValue = (uint)val;
}
else
{
value.ValueKind = ValueKind.ULong;
value.UlongValue = val;
}
}
else if (!hasUSuffix & hasLSuffix)
{
if (val <= Int64.MaxValue)
{
value.ValueKind = ValueKind.Long;
value.LongValue = (long)val;
}
else
{
value.ValueKind = ValueKind.ULong;
value.UlongValue = val;
}
}
else
{
value.ValueKind = ValueKind.ULong;
value.UlongValue = val;
}
break;
}
return value;
}
private static decimal GetValueDecimal(string text)
{
if (!decimal.TryParse(text, NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out var result))
{
throw new InvalidOperationException($"Invalid numeric literal: {text}");
}
return result;
}
private static float GetValueFloat(string text)
{
if (!float.TryParse(text, NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out var result))
{
throw new InvalidOperationException($"Invalid numeric literal: {text}");
}
return result;
}
private static double GetValueDouble(string text)
{
if (!double.TryParse(text, NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out var result))
{
throw new InvalidOperationException($"Invalid numeric literal: {text}");
}
return result;
}
private static ulong GetValueUInt64(string text, bool isHex)
{
if (!UInt64.TryParse(text, isHex ? NumberStyles.AllowHexSpecifier : NumberStyles.None, CultureInfo.InvariantCulture, out var result))
{
throw new InvalidOperationException($"Invalid numeric literal: {text}");
}
return result;
}
private Token ScanIdentifier()
{
var startOffset = position;
while (true)
{
if (position == expression.Length)
{
var length = position - startOffset;
return new Token(TokenType.Identifier, expression.Substring(startOffset, length));
}
switch (Peek())
{
case '\0':
case ' ':
case '\r':
case '\n':
case '\t':
case '!':
case '%':
case '(':
case ')':
case '*':
case '+':
case ',':
case '-':
case '.':
case '/':
case ':':
case ';':
case '<':
case '=':
case '>':
case '?':
case '[':
case ']':
case '^':
case '{':
case '|':
case '}':
case '~':
case '"':
case '\'':
// All of the following characters are not valid in an
// identifier. If we see any of them, then we know we're
// done.
return new Token(TokenType.Identifier, expression[startOffset..position]);
case >= '0' and <= '9':
if (position == startOffset)
{
break;
}
else
{
goto case '_';
}
case (>= 'a' and <= 'z') or (>= 'A' and <= 'Z'):
case '_':
// All of these characters are valid inside an identifier.
// consume it and keep processing.
Advance(1);
continue;
default:
throw new InvalidOperationException($"Unexpected character '{Peek()}' at position {position}.");
}
}
}
}

View File

@@ -0,0 +1,919 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Linq;
namespace Radzen;
#nullable enable
/// <summary>
/// Parse lambda expressions from strings.
/// </summary>
public class ExpressionParser
{
/// <summary>
/// Parses a lambda expression that returns a boolean value.
/// </summary>
public static Expression<Func<T, bool>> ParsePredicate<T>(string expression, Func<string, Type?>? typeResolver = null)
{
return ParseLambda<T, bool>(expression, typeResolver);
}
/// <summary>
/// Parses a lambda expression that returns a typed result.
/// </summary>
public static Expression<Func<T, TResult>> ParseLambda<T, TResult>(string expression, Func<string, Type?>? typeResolver = null)
{
var lambda = ParseLambda(expression, typeof(T), typeResolver);
return Expression.Lambda<Func<T, TResult>>(lambda.Body, lambda.Parameters[0]);
}
/// <summary>
/// Parses a lambda expression that returns untyped result.
/// </summary>
public static LambdaExpression ParseLambda<T>(string expression, Func<string, Type?>? typeLocator = null)
{
return ParseLambda(expression, typeof(T), typeLocator);
}
/// <summary>
/// Parses a lambda expression that returns untyped result.
/// </summary>
public static LambdaExpression ParseLambda(string expression, Type type, Func<string, Type?>? typeResolver = null)
{
var parser = new ExpressionParser(expression, typeResolver);
return parser.ParseLambda(type);
}
private readonly List<Token> tokens;
private int position = 0;
private readonly Func<string, Type?>? typeResolver;
private readonly Stack<ParameterExpression> parameterStack = new();
private ExpressionParser(string expression, Func<string, Type?>? typeResolver = null)
{
this.typeResolver = typeResolver;
tokens = ExpressionLexer.Scan(expression);
}
Token Expect(TokenType type)
{
if (position >= tokens.Count)
{
throw new InvalidOperationException($"Unexpected end of expression. Expected token: {type}");
}
var token = tokens[position];
if (token.Type != type)
{
throw new InvalidOperationException($"Unexpected token: {token.Type}. Expected: {type}");
}
position++;
return token;
}
void Advance(int count)
{
position += count;
}
Token Peek(int offset = 0)
{
if (position + offset >= tokens.Count)
{
return new Token(TokenType.None, string.Empty);
}
return tokens[position + offset];
}
private LambdaExpression ParseLambda(Type paramType)
{
var parameterIdentifier = Expect(TokenType.Identifier);
var parameter = Expression.Parameter(paramType, parameterIdentifier.Value);
parameterStack.Push(parameter);
Expect(TokenType.EqualsGreaterThan);
var body = ParseExpression(parameter);
parameterStack.Pop();
return Expression.Lambda(body, parameter);
}
private Expression ParseExpression(ParameterExpression parameter)
{
var left = ParseBinary(parameter);
var token = Peek();
if (token.Type is TokenType.AmpersandAmpersand)
{
Advance(1);
var right = ParseExpression(parameter) ?? throw new InvalidOperationException($"Expected expression after {token.Value} at position {position}");
left = Expression.AndAlso(left, right);
}
else if (token.Type is TokenType.BarBar)
{
Advance(1);
var right = ParseExpression(parameter) ?? throw new InvalidOperationException($"Expected expression after {token.Value} at position {position}");
left = Expression.OrElse(left, right);
}
return left;
}
private Expression ParseBinary(ParameterExpression parameter)
{
var left = ParseNullCoalescing(parameter);
var token = Peek();
if (token.Type is TokenType.EqualsEquals or TokenType.NotEquals or TokenType.GreaterThan or TokenType.LessThan or TokenType.LessThanOrEqual or TokenType.GreaterThanOrEqual)
{
Advance(1);
var right = ParseBinary(parameter) ?? throw new InvalidOperationException($"Expected expression after {token.Value} at position {position}");
left = Expression.MakeBinary(token.Type.ToExpressionType(), left, ConvertIfNeeded(right, left.Type));
}
return left;
}
private Expression ParseNullCoalescing(ParameterExpression parameter)
{
var left = ParseTernary(parameter);
var token = Peek();
while (token.Type == TokenType.QuestionMarkQuestionMark)
{
Advance(1);
var right = ParseTernary(parameter) ?? throw new InvalidOperationException($"Expected expression after ?? at position {position}");
if (right.Type == typeof(object))
{
right = ConvertIfNeeded(right, left.Type);
}
left = Expression.Coalesce(left, right);
token = Peek();
}
return left;
}
private Expression ParseTernary(ParameterExpression parameter)
{
var condition = ParseOr(parameter);
if (Peek().Type == TokenType.QuestionMark)
{
Advance(1);
var trueExpression = ParseOr(parameter);
Expect(TokenType.Colon);
var falseExpression = ParseOr(parameter);
if (trueExpression is ConstantExpression trueConst && trueConst.Value == null && falseExpression is not ConstantExpression)
{
trueExpression = Expression.Constant(null, falseExpression.Type);
}
else if (falseExpression is ConstantExpression falseConst && falseConst.Value == null && trueExpression is not ConstantExpression)
{
falseExpression = Expression.Constant(null, trueExpression.Type);
}
var ternary = Expression.Condition(condition, trueExpression, falseExpression);
return ParseMemberAccess(ternary, parameter);
}
return ParseMemberAccess(condition, parameter);
}
private Expression ParseMemberAccess(Expression expression, ParameterExpression parameter)
{
var token = Peek();
while (token.Type is TokenType.Dot or TokenType.QuestionDot or TokenType.OpenBracket)
{
if (token.Type == TokenType.Dot)
{
Advance(1);
token = Expect(TokenType.Identifier);
if (Peek().Type == TokenType.OpenParen)
{
expression = ParseInvocation(expression, token.Value, parameter);
}
else
{
expression = Expression.PropertyOrField(expression, token.Value);
}
}
else if (token.Type == TokenType.QuestionDot)
{
Advance(1);
token = Expect(TokenType.Identifier);
var check = Expression.Equal(expression, Expression.Constant(null));
if (Peek().Type == TokenType.OpenParen)
{
var call = ParseInvocation(expression, token.Value, parameter);
expression = Expression.Condition(check, Expression.Constant(null, call.Type), call);
}
else
{
var access = Expression.PropertyOrField(expression, token.Value);
expression = Expression.Condition(check, Expression.Default(access.Type), access);
var nextToken = Peek();
if (nextToken.Type == TokenType.Dot || nextToken.Type == TokenType.QuestionDot)
{
var nextAccess = ParseMemberAccess(access, parameter);
expression = Expression.Condition(check, Expression.Default(nextAccess.Type), nextAccess);
}
}
}
else if (token.Type == TokenType.OpenBracket)
{
Advance(1);
var index = ParseExpression(parameter);
Expect(TokenType.CloseBracket);
if (expression.Type.IsArray)
{
expression = Expression.ArrayIndex(expression, index);
}
else
{
var indexer = expression.Type.GetProperty("Item") ?? throw new InvalidOperationException($"Type {expression.Type} does not have an indexer property");
expression = Expression.Property(expression, indexer, index);
}
}
token = Peek();
}
return expression;
}
private MethodCallExpression ParseInvocation(Expression expression, string methodName, ParameterExpression parameter)
{
Advance(1);
var arguments = new List<Expression>();
if (Peek().Type != TokenType.CloseParen)
{
while (Peek().Type != TokenType.CloseParen)
{
var token = Peek();
if (token.Type == TokenType.Identifier && Peek(1).Type == TokenType.EqualsGreaterThan)
{
var lambdaParameterName = token.Value;
Advance(2);
Type? lambdaParameterType = null;
var extensionMethod = typeof(Enumerable).GetMethods(BindingFlags.Public | BindingFlags.Static)
.FirstOrDefault(m => m.Name == methodName && m.GetParameters().Length == 2);
if (extensionMethod != null)
{
lambdaParameterType = GetItemType(expression.Type);
}
if (lambdaParameterType == null)
{
throw new InvalidOperationException($"Could not infer type for lambda parameter {lambdaParameterName}");
}
var lambdaParameter = Expression.Parameter(lambdaParameterType, lambdaParameterName);
parameterStack.Push(lambdaParameter);
var lambdaBody = ParseExpression(lambdaParameter);
parameterStack.Pop();
arguments.Add(Expression.Lambda(lambdaBody, lambdaParameter));
}
else
{
arguments.Add(ParseExpression(parameter));
}
if (Peek().Type == TokenType.Comma)
{
Advance(1);
}
}
}
Expect(TokenType.CloseParen);
var argumentTypes = arguments.Select(a => a.Type).ToArray();
var method = expression.Type.GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance, null, argumentTypes, null);
if (method != null)
{
return Expression.Call(expression, method, arguments);
}
method = typeof(Enumerable).GetMethods(BindingFlags.Public | BindingFlags.Static)
.FirstOrDefault(m => m.Name == methodName && m.GetParameters().Length == arguments.Count + 1);
if (method != null)
{
var argumentType = GetItemType(expression.Type);
if (argumentType == null)
{
throw new InvalidOperationException($"Cannot determine item type for {expression.Type}");
}
if (method.IsGenericMethodDefinition)
{
method = method.MakeGenericMethod(argumentType);
}
var parameters = method.GetParameters();
var argumentsWithInstance = new[] { expression }.Concat(arguments).ToArray();
return Expression.Call(method, argumentsWithInstance.Select((a, index) => ConvertIfNeeded(a, parameters[index].ParameterType)));
}
throw new InvalidOperationException($"No suitable method '{methodName}' found for type '{expression.Type}'");
}
private static Type? GetItemType(Type enumerableOrArray)
{
return enumerableOrArray.IsArray ? enumerableOrArray.GetElementType() : enumerableOrArray.GetGenericArguments()[0];
}
private Expression? ParseTerm(ParameterExpression parameter)
{
var token = Peek();
if (token.Type == TokenType.None)
{
return null;
}
if (token.Type == TokenType.OpenParen)
{
Advance(1);
if (TryParseCastExpression(parameter, out var expression))
{
return expression;
}
expression = ParseExpression(parameter);
Expect(TokenType.CloseParen);
return expression;
}
if (token.Type == TokenType.Identifier)
{
var matchingParameter = parameterStack.FirstOrDefault(p => p.Name == token.Value);
if (matchingParameter != null)
{
Advance(1);
return ParseMemberAccess(matchingParameter, parameter);
}
var type = GetWellKnownType(token.Value);
if (type != null)
{
Advance(1);
return ParseStaticMemberAccess(type, parameter);
}
if (Peek(1).Type == TokenType.OpenParen)
{
Advance(1);
return ParseInvocation(parameter, token.Value, parameter);
}
throw new InvalidOperationException($"Unexpected identifier: {token.Value}");
}
if (token.Type == TokenType.ExclamationMark)
{
Advance(1);
var operand = ParseTerm(parameter) ?? throw new InvalidOperationException($"Expected expression after ! at position {position}");
operand = ConvertIfNeeded(operand, typeof(bool));
return Expression.Not(operand);
}
if (token.Type == TokenType.Minus)
{
Advance(1);
var operand = ParseTerm(parameter) ?? throw new InvalidOperationException($"Expected expression after - at position {position}");
return Expression.Negate(operand);
}
if (token.Type == TokenType.Plus)
{
Advance(1);
var operand = ParseTerm(parameter) ?? throw new InvalidOperationException($"Expected expression after + at position {position}");
return operand;
}
switch (token.Type)
{
case TokenType.CharacterLiteral:
case TokenType.StringLiteral:
case TokenType.NullLiteral:
case TokenType.NumericLiteral:
case TokenType.TrueLiteral:
case TokenType.FalseLiteral:
Advance(1);
return token.ToConstantExpression();
case TokenType.New:
Advance(1);
token = Peek();
if (token.Type == TokenType.OpenBrace)
{
Advance(1);
var properties = new List<(string Name, Expression Expression)>();
if (Peek().Type != TokenType.CloseBrace)
{
do
{
token = Peek();
string propertyName;
Expression propertyExpression;
if (token.Type == TokenType.Identifier)
{
propertyName = token.Value;
Advance(1);
if (Peek().Type == TokenType.Dot || Peek().Type == TokenType.QuestionDot)
{
// Handle nested property access
Expression expr = propertyName == parameter.Name ? (Expression)parameter : Expression.Property(parameter, propertyName);
propertyExpression = ParseMemberAccess(expr, parameter);
// Get the last identifier token's value
var lastToken = tokens[position - 1];
if (lastToken.Type == TokenType.Identifier)
{
propertyName = lastToken.Value;
}
}
else
{
Expect(TokenType.Equals);
propertyExpression = ParseExpression(parameter);
}
}
else
{
propertyExpression = ParseExpression(parameter);
if (propertyExpression is MemberExpression memberExpression)
{
propertyName = memberExpression.Member.Name;
}
else
{
throw new InvalidOperationException($"Invalid anonymous type member expression at position {position}");
}
}
properties.Add((propertyName, propertyExpression));
if (Peek().Type == TokenType.Comma)
{
Advance(1);
}
else
{
break;
}
} while (Peek().Type != TokenType.CloseBrace);
}
Expect(TokenType.CloseBrace);
var propertyTypes = properties.Select(p => p.Expression.Type).ToArray();
var propertyNames = properties.Select(p => p.Name).ToArray();
var dynamicType = DynamicTypeFactory.CreateType(parameter.Type.Name, propertyNames, propertyTypes);
var bindings = properties.Select(p => Expression.Bind(dynamicType.GetProperty(p.Name)!, p.Expression));
return Expression.MemberInit(Expression.New(dynamicType), bindings);
}
else
{
Type? elementType = null;
var nullable = false;
if (token.Type == TokenType.Identifier)
{
var typeName = token.Value;
elementType = GetWellKnownType(typeName);
Advance(1);
if (Peek().Type == TokenType.QuestionMark)
{
nullable = true;
Advance(1);
}
}
Expect(TokenType.OpenBracket);
Expect(TokenType.CloseBracket);
Expect(TokenType.OpenBrace);
var elements = new List<Expression>();
if (Peek().Type != TokenType.CloseBrace)
{
do
{
elements.Add(ParseExpression(parameter));
if (Peek().Type == TokenType.Comma)
{
Advance(1);
}
else
{
break;
}
} while (Peek().Type != TokenType.CloseBrace);
}
Expect(TokenType.CloseBrace);
if (elementType == null)
{
elementType = elements.Count > 0 ? elements[0].Type : typeof(object);
}
if (nullable)
{
elementType = typeof(Nullable<>).MakeGenericType(elementType);
}
return Expression.NewArrayInit(elementType, elements.Select(e => ConvertIfNeeded(e, elementType)));
}
default:
throw new InvalidOperationException($"Unexpected token: {token.Type} at position {position}");
}
}
private bool TryParseCastExpression(ParameterExpression parameter, out Expression expression)
{
expression = null!;
var token = Peek();
if (token.Type != TokenType.Identifier)
{
return false;
}
var typeName = new StringBuilder(token.Value);
var index = position + 1;
var typeCast = true;
var nullable = false;
while (index < tokens.Count)
{
token = tokens[index];
if (token.Type == TokenType.Dot)
{
index++;
if (index >= tokens.Count || tokens[index].Type != TokenType.Identifier)
{
typeCast = false;
break;
}
typeName.Append('.').Append(tokens[index].Value);
index++;
}
else if (token.Type == TokenType.QuestionMark)
{
nullable = true;
index++;
if (index >= tokens.Count || tokens[index].Type != TokenType.CloseParen)
{
typeCast = false;
break;
}
}
else if (token.Type == TokenType.CloseParen)
{
break;
}
else
{
typeCast = false;
break;
}
}
if (typeCast && index < tokens.Count && tokens[index].Type == TokenType.CloseParen)
{
var name = typeName.ToString();
var type = GetWellKnownType(name) ?? typeResolver?.Invoke(name) ?? throw new InvalidOperationException($"Could not resolve type: {typeName}");
if (nullable && type.IsValueType)
{
type = typeof(Nullable<>).MakeGenericType(type);
}
position = index;
Advance(1);
if (Peek().Type == TokenType.OpenParen && TryParseCastExpression(parameter, out var innerExpression))
{
expression = Expression.Convert(innerExpression, type);
}
else
{
var source = ParseTerm(parameter) ?? throw new InvalidOperationException($"Expected expression to cast at position {position}");
expression = Expression.Convert(source, type);
}
return true;
}
return false;
}
private Expression ParseStaticMemberAccess(Type type, ParameterExpression parameter)
{
Expect(TokenType.Dot);
var token = Expect(TokenType.Identifier);
if (Peek().Type == TokenType.OpenParen)
{
return ParseStaticInvocation(type, token.Value, parameter);
}
else
{
var member = (MemberInfo?)type.GetProperty(token.Value) ?? type.GetField(token.Value);
if (member == null)
{
throw new InvalidOperationException($"Member {token.Value} not found on type {type.Name}");
}
return Expression.MakeMemberAccess(null, member);
}
throw new InvalidOperationException($"Expected method invocation after {token.Value} at position {position}");
}
private Expression ParseStaticInvocation(Type type, string methodName, ParameterExpression parameter)
{
Advance(1);
var arguments = new List<Expression>();
if (Peek().Type != TokenType.CloseParen)
{
arguments.Add(ParseExpression(parameter));
while (Peek().Type == TokenType.Comma)
{
Advance(1);
arguments.Add(ParseExpression(parameter));
}
}
Expect(TokenType.CloseParen);
var method = type.GetMethod(methodName, [.. arguments.Select(a => a.Type)]) ?? throw new InvalidOperationException($"Method {methodName} not found on type {type.Name}");
return Expression.Call(null, method, arguments);
}
private static Type? GetWellKnownType(string typeName)
{
return typeName switch
{
nameof(DateTime) => typeof(DateTime),
nameof(DateOnly) => typeof(DateOnly),
nameof(TimeOnly) => typeof(TimeOnly),
nameof(DateTimeOffset) => typeof(DateTimeOffset),
nameof(Guid) => typeof(Guid),
nameof(CultureInfo) => typeof(CultureInfo),
nameof(DateTimeStyles) => typeof(DateTimeStyles),
nameof(DateTimeKind) => typeof(DateTimeKind),
nameof(Double) or "double" => typeof(double),
nameof(Single) or "float" => typeof(float),
nameof(Int32) or "int" => typeof(int),
nameof(Int64) or "long" => typeof(long),
nameof(Int16) or "short" => typeof(short),
nameof(Byte) or "byte" => typeof(byte),
nameof(SByte) or "sbyte" => typeof(sbyte),
nameof(UInt32) or "uint" => typeof(uint),
nameof(UInt64) or "ulong" => typeof(ulong),
nameof(UInt16) or "ushort" => typeof(ushort),
nameof(Boolean) or "bool" => typeof(bool),
nameof(Char) or "char" => typeof(char),
nameof(Decimal) or "decimal" => typeof(decimal),
nameof(String) or "string" => typeof(string),
nameof(Math) => typeof(Math),
nameof(Convert) => typeof(Convert),
_ => null
};
}
private Expression ParseOr(ParameterExpression parameter)
{
var left = ParseMemberAccess(ParseAnd(parameter), parameter);
var token = Peek();
while (token.Type == TokenType.BarBar)
{
Advance(1);
var right = ParseMemberAccess(ParseAnd(parameter) ?? throw new InvalidOperationException($"Expected expression after || at position {position}"), parameter);
left = Expression.OrElse(left, right);
token = Peek();
}
return left;
}
private Expression ParseAnd(ParameterExpression parameter)
{
var left = ParseMemberAccess(ParseComparison(parameter), parameter);
var token = Peek();
while (token.Type == TokenType.AmpersandAmpersand)
{
Advance(1);
var right = ParseMemberAccess(ParseComparison(parameter) ?? throw new InvalidOperationException($"Expected expression after && at position {position}"), parameter);
left = Expression.AndAlso(left, right);
token = Peek();
}
return left;
}
private Expression ParseComparison(ParameterExpression parameter)
{
var left = ParseShift(parameter);
var token = Peek();
if (token.Type is TokenType.EqualsEquals or TokenType.NotEquals or TokenType.GreaterThan or TokenType.LessThan or TokenType.LessThanOrEqual or TokenType.GreaterThanOrEqual)
{
Advance(1);
var right = ParseShift(parameter) ?? throw new InvalidOperationException($"Expected expression after {token.Value} at position {position}");
left = Expression.MakeBinary(token.Type.ToExpressionType(), left, ConvertIfNeeded(right, left.Type));
}
return ParseBinaryAnd(left, parameter);
}
private Expression ParseBinaryAnd(Expression left, ParameterExpression parameter)
{
var token = Peek();
while (token.Type == TokenType.Ampersand)
{
Advance(1);
var right = ParseShift(parameter) ?? throw new InvalidOperationException($"Expected expression after & at position {position}");
left = Expression.MakeBinary(ExpressionType.And, left, ConvertIfNeeded(right, left.Type));
token = Peek();
}
return ParseBinaryXor(left, parameter);
}
private Expression ParseBinaryXor(Expression left, ParameterExpression parameter)
{
var token = Peek();
while (token.Type == TokenType.Caret)
{
Advance(1);
var right = ParseBinaryAnd(ParseShift(parameter), parameter) ?? throw new InvalidOperationException($"Expected expression after ^ at position {position}");
left = Expression.MakeBinary(ExpressionType.ExclusiveOr, left, ConvertIfNeeded(right, left.Type));
token = Peek();
}
return ParseBinaryOr(left, parameter);
}
private Expression ParseBinaryOr(Expression left, ParameterExpression parameter)
{
var token = Peek();
while (token.Type == TokenType.Bar)
{
Advance(1);
var right = ParseBinaryXor(ParseShift(parameter), parameter) ?? throw new InvalidOperationException($"Expected expression after | at position {position}");
left = Expression.MakeBinary(ExpressionType.Or, left, ConvertIfNeeded(right, left.Type));
token = Peek();
}
return left;
}
private Expression ParseShift(ParameterExpression parameter)
{
var left = ParseAdditive(parameter);
var token = Peek();
while (token.Type is TokenType.LessThanLessThan or TokenType.GreaterThanGreaterThan)
{
Advance(1);
var right = ParseAdditive(parameter) ?? throw new InvalidOperationException($"Expected expression after {token.Value} at position {position}");
left = Expression.MakeBinary(token.Type.ToExpressionType(), left, ConvertIfNeeded(right, left.Type));
token = Peek();
}
return left;
}
private Expression ParseAdditive(ParameterExpression parameter)
{
var left = ParseMultiplicative(parameter);
var token = Peek();
while (token.Type is TokenType.Plus or TokenType.Minus)
{
Advance(1);
var right = ParseMultiplicative(parameter) ?? throw new InvalidOperationException($"Expected expression after {token.Value} at position {position}");
if (token.Type == TokenType.Plus && left.Type == typeof(string))
{
left = Expression.Call(null, typeof(string).GetMethod(nameof(string.Concat), [typeof(string), typeof(string)])!, left, ConvertIfNeeded(right, typeof(string)));
}
else
{
left = Expression.MakeBinary(token.Type.ToExpressionType(), left, ConvertIfNeeded(right, left.Type));
}
token = Peek();
}
return left;
}
private Expression ParseMultiplicative(ParameterExpression parameter)
{
var left = ParseTerm(parameter) ?? throw new InvalidOperationException($"Expected expression at position {position}");
var token = Peek();
while (token.Type is TokenType.Star or TokenType.Slash)
{
Advance(1);
var right = ParseTerm(parameter) ?? throw new InvalidOperationException($"Expected expression after {token.Value} at position {position}");
left = Expression.MakeBinary(token.Type.ToExpressionType(), left, ConvertIfNeeded(right, left.Type));
token = Peek();
}
return left;
}
private static Expression ConvertIfNeeded(Expression expression, Type targetType)
{
if (expression is not LambdaExpression)
{
return expression.Type == targetType ? expression : Expression.Convert(expression, targetType);
}
return expression;
}
}

View File

@@ -0,0 +1,473 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
/// <summary>
/// Serializes LINQ Expression Trees into C# string representations.
/// </summary>
public class ExpressionSerializer : ExpressionVisitor
{
private readonly StringBuilder _sb = new StringBuilder();
/// <summary>
/// Serializes a given LINQ Expression into a C# string.
/// </summary>
/// <param name="expression">The expression to serialize.</param>
/// <returns>A string representation of the expression.</returns>
public string Serialize(Expression expression)
{
_sb.Clear();
Visit(expression);
return _sb.ToString();
}
/// <inheritdoc/>
protected override Expression VisitLambda<T>(Expression<T> node)
{
if (node.Parameters.Count > 1)
{
_sb.Append("(");
for (int i = 0; i < node.Parameters.Count; i++)
{
if (i > 0) _sb.Append(", ");
_sb.Append(node.Parameters[i].Name);
}
_sb.Append(") => ");
}
else
{
_sb.Append(node.Parameters[0].Name);
_sb.Append(" => ");
}
Visit(node.Body);
return node;
}
/// <inheritdoc/>
protected override Expression VisitParameter(ParameterExpression node)
{
_sb.Append(node.Name);
return node;
}
/// <inheritdoc/>
protected override Expression VisitMember(MemberExpression node)
{
if (node.Expression != null)
{
Visit(node.Expression);
_sb.Append($".{node.Member.Name}");
}
else
{
_sb.Append(node.Member.Name);
}
return node;
}
/// <inheritdoc/>
protected override Expression VisitMethodCall(MethodCallExpression node)
{
if (node.Method.IsStatic && node.Arguments.Count > 0 &&
(node.Method.DeclaringType == typeof(Enumerable) ||
node.Method.DeclaringType == typeof(Queryable)))
{
Visit(node.Arguments[0]);
_sb.Append($".{node.Method.Name}(");
for (int i = 1; i < node.Arguments.Count; i++)
{
if (i > 1) _sb.Append(", ");
if (node.Arguments[i] is NewArrayExpression arrayExpr)
{
VisitNewArray(arrayExpr);
}
else
{
Visit(node.Arguments[i]);
}
}
_sb.Append(")");
}
else if (node.Method.IsStatic)
{
_sb.Append($"{node.Method.DeclaringType.Name}.{node.Method.Name}(");
for (int i = 0; i < node.Arguments.Count; i++)
{
if (i > 0) _sb.Append(", ");
Visit(node.Arguments[i]);
}
_sb.Append(")");
}
else
{
if (node.Object != null)
{
Visit(node.Object);
_sb.Append($".{node.Method.Name}(");
}
else
{
_sb.Append($"{node.Method.Name}(");
}
for (int i = 0; i < node.Arguments.Count; i++)
{
if (i > 0) _sb.Append(", ");
Visit(node.Arguments[i]);
}
_sb.Append(")");
}
return node;
}
/// <inheritdoc/>
protected override Expression VisitUnary(UnaryExpression node)
{
if (node.NodeType == ExpressionType.Not)
{
_sb.Append("(!(");
Visit(node.Operand);
_sb.Append("))");
}
else if (node.NodeType == ExpressionType.Convert)
{
if (node.Operand is IndexExpression indexExpr)
{
_sb.Append($"({node.Type.DisplayName(true).Replace("+",".")})");
Visit(indexExpr.Object);
_sb.Append("[");
Visit(indexExpr.Arguments[0]);
_sb.Append("]");
return node;
}
Visit(node.Operand);
}
else
{
_sb.Append(node.NodeType switch
{
ExpressionType.Negate => "-",
ExpressionType.UnaryPlus => "+",
_ => throw new NotSupportedException($"Unsupported unary operator: {node.NodeType}")
});
Visit(node.Operand);
}
return node;
}
/// <inheritdoc/>
protected override Expression VisitConstant(ConstantExpression node)
{
_sb.Append(FormatValue(node.Value));
return node;
}
internal static string FormatValue(object value)
{
if (value == null)
return "null";
return value switch
{
string s when s == string.Empty => @"""""",
null => "null",
string s => @$"""{s.Replace("\"", "\\\"")}""",
char c => $"'{c}'",
bool b => b.ToString().ToLowerInvariant(),
DateTime dt => FormatDateTime(dt),
DateTimeOffset dto => $"DateTime.Parse(\"{dto.UtcDateTime:yyyy-MM-ddTHH:mm:ss.fffZ}\")",
DateOnly dateOnly => $"DateOnly.Parse(\"{dateOnly.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}\", CultureInfo.InvariantCulture)",
TimeOnly timeOnly => $"TimeOnly.Parse(\"{timeOnly.ToString("HH:mm:ss", CultureInfo.InvariantCulture)}\", CultureInfo.InvariantCulture)",
Guid guid => $"Guid.Parse(\"{guid.ToString("D", CultureInfo.InvariantCulture)}\")",
IEnumerable enumerable when value is not string => FormatEnumerable(enumerable),
_ => value.GetType().IsEnum
? $"({value.GetType().FullName.Replace("+", ".")})" + Convert.ChangeType(value, Enum.GetUnderlyingType(value.GetType()), CultureInfo.InvariantCulture).ToString()
: Convert.ToString(value, CultureInfo.InvariantCulture)
};
}
private static string FormatDateTime(DateTime dateTime)
{
var finalDate = dateTime.TimeOfDay == TimeSpan.Zero ? dateTime.Date : dateTime;
var dateFormat = dateTime.TimeOfDay == TimeSpan.Zero ? "yyyy-MM-dd" : "yyyy-MM-ddTHH:mm:ss.fffZ";
return $"DateTime.SpecifyKind(DateTime.Parse(\"{finalDate.ToString(dateFormat, CultureInfo.InvariantCulture)}\", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind), DateTimeKind.{Enum.GetName(finalDate.Kind)})";
}
private static string FormatEnumerable(IEnumerable enumerable)
{
var arrayType = enumerable.AsQueryable().ElementType;
var items = enumerable.Cast<object>().Select(FormatValue);
return $"new {(Nullable.GetUnderlyingType(arrayType) != null ? arrayType.DisplayName(true).Replace("+", ".") : "")}[] {{ {string.Join(", ", items)} }}";
}
/// <inheritdoc/>
protected override Expression VisitNewArray(NewArrayExpression node)
{
bool needsParentheses = node.NodeType == ExpressionType.NewArrayInit &&
(node.Expressions.Count > 1 || node.Expressions[0].NodeType != ExpressionType.Constant);
if (needsParentheses) _sb.Append("(");
_sb.Append("new [] { ");
bool first = true;
foreach (var expr in node.Expressions)
{
if (!first) _sb.Append(", ");
first = false;
Visit(expr);
}
_sb.Append(" }");
if (needsParentheses) _sb.Append(")");
return node;
}
/// <inheritdoc/>
protected override Expression VisitBinary(BinaryExpression node)
{
_sb.Append("(");
Visit(node.Left);
_sb.Append($" {GetOperator(node.NodeType)} ");
Visit(node.Right);
_sb.Append(")");
return node;
}
/// <inheritdoc/>
protected override Expression VisitConditional(ConditionalExpression node)
{
_sb.Append("(");
Visit(node.Test);
_sb.Append(" ? ");
Visit(node.IfTrue);
_sb.Append(" : ");
Visit(node.IfFalse);
_sb.Append(")");
return node;
}
/// <summary>
/// Maps an ExpressionType to its corresponding C# operator.
/// </summary>
/// <param name="type">The ExpressionType to map.</param>
/// <returns>A string representation of the corresponding C# operator.</returns>
private static string GetOperator(ExpressionType type)
{
return type switch
{
ExpressionType.Add => "+",
ExpressionType.Subtract => "-",
ExpressionType.Multiply => "*",
ExpressionType.Divide => "/",
ExpressionType.AndAlso => "&&",
ExpressionType.OrElse => "||",
ExpressionType.Equal => "==",
ExpressionType.NotEqual => "!=",
ExpressionType.LessThan => "<",
ExpressionType.LessThanOrEqual => "<=",
ExpressionType.GreaterThan => ">",
ExpressionType.GreaterThanOrEqual => ">=",
ExpressionType.Coalesce => "??",
_ => throw new NotSupportedException($"Unsupported operator: {type}")
};
}
}
/// <summary>
/// Provides an extension method for displaying type names.
/// </summary>
public static class SharedTypeExtensions
{
private static readonly Dictionary<Type, string> BuiltInTypeNames = new()
{
{ typeof(bool), "bool" },
{ typeof(byte), "byte" },
{ typeof(char), "char" },
{ typeof(decimal), "decimal" },
{ typeof(double), "double" },
{ typeof(float), "float" },
{ typeof(int), "int" },
{ typeof(long), "long" },
{ typeof(object), "object" },
{ typeof(sbyte), "sbyte" },
{ typeof(short), "short" },
{ typeof(string), "string" },
{ typeof(uint), "uint" },
{ typeof(ulong), "ulong" },
{ typeof(ushort), "ushort" },
{ typeof(void), "void" }
};
/// <summary>
/// Unwraps nullable type.
/// </summary>
public static Type UnwrapNullableType(this Type type)
=> Nullable.GetUnderlyingType(type) ?? type;
/// <summary>
/// Returns a display name for the given type.
/// </summary>
/// <param name="type">The type to display.</param>
/// <param name="fullName">Indicates whether to use the full name.</param>
/// <param name="compilable">Indicates whether to use a compilable format.</param>
/// <returns>A string representing the type name.</returns>
public static string DisplayName(this Type type, bool fullName = true, bool compilable = false)
{
var stringBuilder = new StringBuilder();
ProcessType(stringBuilder, type, fullName, compilable);
return stringBuilder.ToString();
}
private static void ProcessType(StringBuilder builder, Type type, bool fullName, bool compilable)
{
if (type.IsGenericType)
{
var genericArguments = type.GetGenericArguments();
ProcessGenericType(builder, type, genericArguments, genericArguments.Length, fullName, compilable);
}
else if (type.IsArray)
{
ProcessArrayType(builder, type, fullName, compilable);
}
else if (BuiltInTypeNames.TryGetValue(type, out var builtInName))
{
builder.Append(builtInName);
}
else if (!type.IsGenericParameter)
{
if (compilable)
{
if (type.IsNested)
{
ProcessType(builder, type.DeclaringType!, fullName, compilable);
builder.Append('.');
}
else if (fullName)
{
builder.Append(type.Namespace).Append('.');
}
builder.Append(type.Name);
}
else
{
builder.Append(fullName ? type.FullName : type.Name);
}
}
}
private static void ProcessArrayType(StringBuilder builder, Type type, bool fullName, bool compilable)
{
var innerType = type;
while (innerType.IsArray)
{
innerType = innerType.GetElementType()!;
}
ProcessType(builder, innerType, fullName, compilable);
while (type.IsArray)
{
builder.Append('[');
builder.Append(',', type.GetArrayRank() - 1);
builder.Append(']');
type = type.GetElementType()!;
}
}
private static void ProcessGenericType(
StringBuilder builder,
Type type,
Type[] genericArguments,
int length,
bool fullName,
bool compilable)
{
if (type.IsConstructedGenericType
&& type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
ProcessType(builder, type.UnwrapNullableType(), fullName, compilable);
builder.Append('?');
return;
}
var offset = type.IsNested ? type.DeclaringType!.GetGenericArguments().Length : 0;
if (compilable)
{
if (type.IsNested)
{
ProcessType(builder, type.DeclaringType!, fullName, compilable);
builder.Append('.');
}
else if (fullName)
{
builder.Append(type.Namespace);
builder.Append('.');
}
}
else
{
if (fullName)
{
if (type.IsNested)
{
ProcessGenericType(builder, type.DeclaringType!, genericArguments, offset, fullName, compilable);
builder.Append('+');
}
else
{
builder.Append(type.Namespace);
builder.Append('.');
}
}
}
var genericPartIndex = type.Name.IndexOf('`');
if (genericPartIndex <= 0)
{
builder.Append(type.Name);
return;
}
builder.Append(type.Name, 0, genericPartIndex);
builder.Append('<');
for (var i = offset; i < length; i++)
{
ProcessType(builder, genericArguments[i], fullName, compilable);
if (i + 1 == length)
{
continue;
}
builder.Append(',');
if (!genericArguments[i + 1].IsGenericParameter)
{
builder.Append(' ');
}
}
builder.Append('>');
}
}

View File

@@ -214,7 +214,9 @@ namespace Radzen
/// Gets the field identifier.
/// </summary>
/// <value>The field identifier.</value>
public FieldIdentifier FieldIdentifier { get; private set; }
[Parameter]
public FieldIdentifier FieldIdentifier { get; set; }
/// <summary>
/// Gets or sets the value expression.
/// </summary>

View File

@@ -187,7 +187,7 @@ namespace Radzen.Blazor
if (Visible)
{
JSRuntime.InvokeVoidAsync("Radzen.destroyGauge", Element);
JSRuntime.InvokeVoid("Radzen.destroyGauge", Element);
}
}

View File

@@ -77,6 +77,12 @@ namespace Radzen.Blazor
/// <param name="appointments">The appointments for this range.</param>
Task SelectMonth(DateTime monthStart, IEnumerable<AppointmentData> appointments);
/// <summary>
/// Selects the specified day.
/// </summary>
/// <param name="day">The selected day.</param>
/// <param name="appointments">The appointments for this range.</param>
Task SelectDay(DateTime day, IEnumerable<AppointmentData> appointments);
/// <summary>
/// Selects the specified more link.
/// </summary>
/// <param name="start">The start.</param>
@@ -94,8 +100,9 @@ namespace Radzen.Blazor
/// </summary>
/// <param name="start">The start of the slot.</param>
/// <param name="end">The end of the slot.</param>
/// <param name="getAppointments">Function to return appointments for this range.</param>
/// <returns>A dictionary containing the HTML attributes for the specified slot.</returns>
IDictionary<string, object> GetSlotAttributes(DateTime start, DateTime end);
IDictionary<string, object> GetSlotAttributes(DateTime start, DateTime end, Func<IEnumerable<AppointmentData>> getAppointments);
/// <summary>
/// Renders the appointment.
/// </summary>

View File

@@ -0,0 +1,24 @@
using System;
using System.Threading.Tasks;
using Microsoft.JSInterop;
namespace Radzen;
static class JSRuntimeExtensions
{
public static void InvokeVoid(this IJSRuntime jsRuntime, string identifier, params object[] args)
{
_ = jsRuntime.InvokeVoidAsync(identifier, args).FireAndForget();
}
private static async ValueTask FireAndForget(this ValueTask task)
{
try
{
await task;
}
catch (Exception)
{
}
}
}

View File

@@ -62,10 +62,19 @@ namespace Radzen.Blazor
var start = Input.Start;
var end = Input.End;
if (start == end)
{
start = 0;
end += NiceNumber(end / ticks, false);
if (end < 0)
{
start += NiceNumber(end / ticks, false);
end = 0;
}
else
{
start = 0;
end += NiceNumber(end / ticks, false);
}
}
if (Round && end < 0)
@@ -113,12 +122,12 @@ namespace Radzen.Blazor
Round = false;
}
if (step <= 0)
if (step == 0)
{
throw new ArgumentOutOfRangeException("Step must be greater than zero");
throw new ArgumentOutOfRangeException("Step must be non-zero");
}
return (start, end, step);
return (start, end, Math.Abs(step));
}
}
}

View File

@@ -3,5 +3,4 @@
<assembly fullname="System.Core">
<type fullname="System.Linq.Queryable" preserve="all" />
</assembly>
<assembly fullname="System.Linq.Dynamic.Core" />
</linker>

View File

@@ -0,0 +1,47 @@
using System.Text.RegularExpressions;
namespace Radzen.Blazor.Markdown;
/// <summary>
/// Represents a markdown ATX heading: <c># Heading</c>.
/// </summary>
public class AtxHeading : Heading
{
private static readonly Regex MarkerRegex = new(@"^#{1,6}(?:[ \t]+|$)");
private static readonly Regex StartRegex = new(@"^[ \t]*#+[ \t]*$");
private static readonly Regex EndRegex = new(@"[ \t]+#+[ \t]*$");
internal static BlockStart Start(BlockParser parser, Block block)
{
if (parser.Indented)
{
return BlockStart.Skip;
}
var line = parser.CurrentLine[parser.NextNonSpace..];
var match = MarkerRegex.Match(line);
if (match.Success)
{
parser.AdvanceNextNonSpace();
parser.AdvanceOffset(match.Length, false);
parser.CloseUnmatchedBlocks();
var container = parser.AddChild<AtxHeading>(parser.NextNonSpace);
container.Level = match.Value.Trim().Length;
// remove trailing ###s:
line = parser.CurrentLine[parser.Offset..];
container.Value = EndRegex.Replace(StartRegex.Replace(line, ""), "");
parser.AdvanceOffset(parser.CurrentLine.Length - parser.Offset, false);
return BlockStart.Leaf;
}
return BlockStart.Skip;
}
}

View File

@@ -0,0 +1,380 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace Radzen.Blazor.Markdown;
#nullable enable
class BlazorMarkdownRendererOptions
{
public int AutoLinkHeadingDepth { get; set; }
public bool AllowHtml { get; set; }
public IEnumerable<string>? AllowedHtmlTags { get; set; }
public IEnumerable<string>? AllowedHtmlAttributes { get; set; }
}
class BlazorMarkdownRenderer(BlazorMarkdownRendererOptions options, RenderTreeBuilder builder, Action<RenderTreeBuilder, int> outlet) : NodeVisitorBase
{
public const string Outlet = "<!--rz-outlet-{0}-->";
private static readonly Regex OutletRegex = new (@"<!--rz-outlet-(\d+)-->");
private static readonly Regex HtmlTagRegex = new(@"<(\w+)((?:\s+[^>]*)?)\/?>");
private static readonly Regex HtmlClosingTagRegex = new(@"</(\w+)>");
private static readonly Regex AttributeRegex = new(@"(\w+)(?:\s*=\s*(?:([""'])(.*?)\2|([^\s>]+)))?");
private readonly HtmlSanitizer sanitizer = new (options.AllowedHtmlTags, options.AllowedHtmlAttributes);
public override void VisitHeading(Heading heading)
{
builder.OpenComponent<RadzenText>(0);
builder.AddAttribute(1, nameof(RadzenText.ChildContent), RenderChildren(heading.Children));
switch (heading.Level)
{
case 1:
builder.AddAttribute(2, nameof(RadzenText.TextStyle), TextStyle.H1);
break;
case 2:
builder.AddAttribute(3, nameof(RadzenText.TextStyle), TextStyle.H2);
break;
case 3:
builder.AddAttribute(4, nameof(RadzenText.TextStyle), TextStyle.H3);
break;
case 4:
builder.AddAttribute(5, nameof(RadzenText.TextStyle), TextStyle.H4);
break;
case 5:
builder.AddAttribute(6, nameof(RadzenText.TextStyle), TextStyle.H5);
break;
case 6:
builder.AddAttribute(7, nameof(RadzenText.TextStyle), TextStyle.H6);
break;
}
if (heading.Level <= options.AutoLinkHeadingDepth)
{
var anchor = Regex.Replace(heading.Value, @"[^\w\s-]", string.Empty).Replace(' ', '-').ToLowerInvariant().Trim();
builder.AddAttribute(8, nameof(RadzenText.Anchor), anchor);
}
else
{
builder.AddAttribute(9, nameof(RadzenText.Anchor), (string?)null);
}
builder.CloseComponent();
}
public override void VisitTable(Table table)
{
builder.OpenComponent<RadzenTable>(0);
builder.AddAttribute(1, nameof(RadzenTable.ChildContent), RenderChildren(table.Rows));
builder.CloseComponent();
}
public override void VisitTableRow(TableRow row)
{
builder.OpenComponent<RadzenTableRow>(0);
builder.AddAttribute(1, nameof(RadzenTableRow.ChildContent), RenderChildren(row.Cells));
builder.CloseComponent();
}
public override void VisitTableCell(TableCell cell)
{
builder.OpenComponent<RadzenTableCell>(0);
builder.AddAttribute(1, nameof(RadzenTableCell.ChildContent), RenderChildren(cell.Children));
RenderCellAlignment(builder, cell.Alignment);
builder.CloseComponent();
}
private static void RenderCellAlignment(RenderTreeBuilder builder, TableCellAlignment alignment)
{
switch (alignment)
{
case TableCellAlignment.Center:
builder.AddAttribute(2, nameof(RadzenTableCell.Style), "text-align: center");
break;
case TableCellAlignment.Right:
builder.AddAttribute(3, nameof(RadzenTableCell.Style), "text-align: right");
break;
}
}
public override void VisitTableHeaderRow(TableHeaderRow header)
{
builder.OpenComponent<RadzenTableHeader>(0);
builder.AddAttribute(1, nameof(RadzenTableHeader.ChildContent), new RenderFragment(headerBuilder =>
{
headerBuilder.OpenComponent<RadzenTableHeaderRow>(0);
headerBuilder.AddAttribute(1, nameof(RadzenTableHeaderRow.ChildContent), new RenderFragment(headerRowBuilder =>
{
foreach (var cell in header.Cells)
{
headerRowBuilder.OpenComponent<RadzenTableHeaderCell>(0);
headerRowBuilder.AddAttribute(1, nameof(RadzenTableHeaderCell.ChildContent), RenderChildren(cell.Children));
RenderCellAlignment(headerRowBuilder, cell.Alignment);
headerRowBuilder.CloseComponent();
}
}));
headerBuilder.CloseComponent();
}));
builder.CloseComponent();
}
public override void VisitIndentedCodeBlock(IndentedCodeBlock code)
{
builder.OpenElement(0, "pre");
builder.OpenElement(1, "code");
builder.AddContent(2, code.Value);
builder.CloseElement();
builder.CloseElement();
}
public override void VisitParagraph(Paragraph paragraph)
{
if (paragraph.Parent is ListItem item && item.Parent is List list && list.Tight)
{
VisitChildren(paragraph.Children);
}
else
{
builder.OpenComponent<RadzenText>(0);
builder.AddAttribute(1, nameof(RadzenText.ChildContent), RenderChildren(paragraph.Children));
builder.CloseComponent();
}
}
private RenderFragment RenderChildren(IEnumerable<INode> children)
{
return innerBuilder =>
{
var inner = new BlazorMarkdownRenderer(options, innerBuilder, outlet);
inner.VisitChildren(children);
};
}
public override void VisitBlockQuote(BlockQuote blockQuote)
{
builder.OpenElement(0, "blockquote");
VisitChildren(blockQuote.Children);
builder.CloseElement();
}
public override void VisitCode(Code code)
{
builder.OpenElement(0, "code");
builder.AddContent(1, code.Value);
builder.CloseElement();
}
public override void VisitStrong(Strong strong)
{
builder.OpenElement(0, "strong");
VisitChildren(strong.Children);
builder.CloseElement();
}
public override void VisitEmphasis(Emphasis emphasis)
{
builder.OpenElement(0, "em");
VisitChildren(emphasis.Children);
builder.CloseElement();
}
public override void VisitLink(Link link)
{
builder.OpenComponent<RadzenLink>(0);
if (!HtmlSanitizer.IsDangerousUrl(link.Destination))
{
builder.AddAttribute(1, nameof(RadzenLink.Path), link.Destination);
}
builder.AddAttribute(2, nameof(RadzenLink.ChildContent), RenderChildren(link.Children));
if (!string.IsNullOrEmpty(link.Title))
{
builder.AddAttribute(3, "title", link.Title);
}
builder.CloseComponent();
}
public override void VisitImage(Image image)
{
builder.OpenComponent<RadzenImage>(0);
if (!HtmlSanitizer.IsDangerousUrl(image.Destination))
{
builder.AddAttribute(1, nameof(RadzenImage.Path), image.Destination);
}
if (!string.IsNullOrEmpty(image.Title))
{
builder.AddAttribute(2, nameof(RadzenImage.AlternateText), image.Title);
}
builder.CloseElement();
}
public override void VisitOrderedList(OrderedList orderedList)
{
builder.OpenElement(0, "ol");
VisitChildren(orderedList.Children);
builder.CloseElement();
}
public override void VisitUnorderedList(UnorderedList unorderedList)
{
builder.OpenElement(0, "ul");
VisitChildren(unorderedList.Children);
builder.CloseElement();
}
public override void VisitListItem(ListItem listItem)
{
builder.OpenElement(0, "li");
VisitChildren(listItem.Children);
builder.CloseElement();
}
public override void VisitFencedCodeBlock(FencedCodeBlock fencedCodeBlock)
{
builder.OpenElement(0, "pre");
builder.OpenElement(1, "code");
builder.AddContent(2, fencedCodeBlock.Value);
builder.CloseElement();
builder.CloseElement();
}
public override void VisitThematicBreak(ThematicBreak thematicBreak)
{
builder.OpenElement(0, "hr");
builder.CloseElement();
}
public override void VisitHtmlBlock(HtmlBlock htmlBlock)
{
var match = OutletRegex.Match(htmlBlock.Value);
if (match.Success)
{
var markerId = Convert.ToInt32(match.Groups[1].Value);
outlet(builder, markerId);
}
else if (options.AllowHtml)
{
var html = sanitizer.Sanitize(htmlBlock.Value);
builder.AddMarkupContent(0, html);
}
else
{
builder.AddContent(0, htmlBlock.Value);
}
}
public override void VisitLineBreak(LineBreak lineBreak)
{
builder.OpenElement(0, "br");
builder.CloseElement();
}
public override void VisitText(Text text)
{
builder.AddContent(0, text.Value);
}
private static bool IsVoidElement(string tagName)
{
return tagName.ToLowerInvariant() switch
{
"area" => true,
"base" => true,
"br" => true,
"col" => true,
"embed" => true,
"hr" => true,
"img" => true,
"input" => true,
"link" => true,
"meta" => true,
"param" => true,
"source" => true,
"track" => true,
"wbr" => true,
_ => false
};
}
public override void VisitSoftLineBreak(SoftLineBreak softBreak)
{
builder.AddContent(0, "\n");
}
public override void VisitHtmlInline(HtmlInline htmlInline)
{
var match = OutletRegex.Match(htmlInline.Value);
if (match.Success)
{
var markerId = Convert.ToInt32(match.Groups[1].Value);
outlet(builder, markerId);
return;
}
if (!options.AllowHtml)
{
builder.AddContent(0, htmlInline.Value);
return;
}
var html = sanitizer.Sanitize(htmlInline.Value);
var closingMatch = HtmlClosingTagRegex.Match(html);
if (closingMatch.Success)
{
builder.CloseElement();
return;
}
var openingMatch = HtmlTagRegex.Match(html);
if (openingMatch.Success)
{
var tagName = openingMatch.Groups[1].Value;
builder.OpenElement(0, tagName);
var attributes = openingMatch.Groups[2].Value;
if (!string.IsNullOrEmpty(attributes))
{
var matches = AttributeRegex.Matches(attributes);
foreach (Match attribute in matches)
{
var name = attribute.Groups[1].Value;
var value = name;
if (attribute.Groups[2].Success) // Quoted value (either single or double)
{
value = attribute.Groups[3].Value;
}
else if (attribute.Groups[4].Success) // Unquoted value
{
value = attribute.Groups[4].Value;
}
builder.AddAttribute(1, name, value);
}
}
if (html.EndsWith("/>") || IsVoidElement(tagName))
{
builder.CloseElement();
}
}
}
}

View File

@@ -0,0 +1,73 @@
namespace Radzen.Blazor.Markdown;
#nullable enable
/// <summary>
/// Base class for a markdown block nodes.
/// </summary>
public abstract class Block : INode
{
/// <summary>
/// Accepts a visitor.
/// </summary>
/// <param name="visitor"></param>
public abstract void Accept(INodeVisitor visitor);
/// <summary>
/// Returns the last child of the block.
/// </summary>
public virtual Block? LastChild => null;
/// <summary>
/// Returns the first child of the block.
/// </summary>
public virtual Block? FirstChild => null;
/// <summary>
/// Returns the next sibling of the block.
/// </summary>
public virtual Block? Next => Parent.NextSibling(this);
/// <summary>
/// Returns the parent node of the block.
/// </summary>
public BlockContainer Parent { get; set; } = null!;
/// <summary>
/// Removes the block from its parent.
/// </summary>
public void Remove()
{
Parent.Remove(this);
}
internal virtual BlockMatch Matches(BlockParser parser) => 0;
internal bool Open { get; set; } = true;
internal Range Range;
internal virtual void Close(BlockParser parser)
{
Open = false;
}
}
enum BlockMatch
{
Match,
Skip,
Break
}
struct Position
{
public int Line { get; set; }
public int Column { get; set; }
}
struct Range
{
public Position Start;
public Position End;
}

View File

@@ -0,0 +1,87 @@
using System.Collections.Generic;
namespace Radzen.Blazor.Markdown;
#nullable enable
/// <summary>
/// Base class for markdown block nodes that can contain other blocks.
/// </summary>
public abstract class BlockContainer : Block
{
/// <summary>
/// Returns the children of the block.
/// </summary>
public IReadOnlyList<Block> Children => children;
private readonly List<Block> children = [];
/// <summary>
/// Determines if the block can contain the specified node.
/// </summary>
public virtual bool CanContain(Block node) => false;
/// <summary>
/// Appends a block to the children of the block.
/// </summary>
/// <typeparam name="T">The type of the block.</typeparam>
/// <param name="block">The block to add.</param>
/// <returns>The added block.</returns>
public virtual T Add<T>(T block) where T : Block
{
children.Add(block);
block.Parent = this;
return block;
}
/// <summary>
/// Replaces a block with another block.
/// </summary>
/// <param name="source">The block to replace.</param>
/// <param name="target">The block to replace with.</param>
public void Replace(Block source, Block target)
{
var index = children.IndexOf(source);
if (index >= 0)
{
children[index] = target;
target.Parent = this;
target.Range = source.Range;
}
}
/// <summary>
/// Removes a block from the children of the block.
/// </summary>
/// <param name="block">The block to remove.</param>
public void Remove(Block block)
{
children.Remove(block);
}
/// <summary>
/// Returns the next sibling of the block.
/// </summary>
/// <param name="block">The block to get the next sibling of.</param>
/// <returns>The next sibling of the block.</returns>
public Block? NextSibling(Block block)
{
var index = children.IndexOf(block);
if (index >= 0 && index < children.Count - 1)
{
return children[index + 1];
}
return null;
}
/// <inheritdoc/>
public override Block? LastChild => children.Count > 0 ? children[^1] : null;
/// <inheritdoc/>
public override Block? FirstChild => children.Count > 0 ? children[0] : null;
}

View File

@@ -0,0 +1,491 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
namespace Radzen.Blazor.Markdown;
#nullable enable
class BlockParser
{
private static readonly string tagName = @"[A-Za-z][A-Za-z0-9-]*";
private static readonly string attributeName = @"[a-zA-Z_:][a-zA-Z0-9:._-]*";
private static readonly string unquotedValue = @"[^""'=<>`\x00-\x20]+";
private static readonly string singleQuotedValue = @"'[^']*'";
private static readonly string doubleQuotedValue = @"""[^""]*""";
private static readonly string attributeValue = @$"(?:{unquotedValue}|{singleQuotedValue}|{doubleQuotedValue})";
private static readonly string attributeValueSpec = @$"(?:\s*=\s*{attributeValue})";
private static readonly string attribute = @$"(?:\s+{attributeName}{attributeValueSpec}?)";
private static readonly string OpenTag = @$"<{tagName}{attribute}*\s*/?>";
private static readonly string CloseTag = @$"</{tagName}\s*[>]";
private static readonly string htmlComment = @"<!-->|<!--->|<!--[\s\S]*?-->";
private static readonly string processingInstruction = @"<\?[ \s\S]*?\?>";
private static readonly string declaration = @$"<![A-Za-z]+[^>]*>";
private static readonly string cdata = @"<!\[CDATA\[[\s\S]*?\]\]>";
public static readonly Regex HtmlRegex = new(@$"^(?:{OpenTag}|{CloseTag}|{htmlComment}|{processingInstruction}|{declaration}|{cdata})");
private BlockParser()
{
Tip = document;
OldTip = document;
lastMatchedContainer = document;
}
public static Document Parse(string markdown)
{
var parser = new BlockParser();
var document = parser.ParseBlocks(markdown);
parser.ParseInlines(document);
return document;
}
public static readonly Regex NewLineRegex = new(@"\r\n|\r|\n");
private readonly Document document = new();
private void ParseInlines(Document document)
{
var visitor = new InlineVisitor(linkReferences);
document.Accept(visitor);
}
public char Peek()
{
return CurrentLine.Peek(Offset);
}
public char PeekNonSpace(int offset = 0)
{
return CurrentLine.Peek(NextNonSpace + offset);
}
public void AdvanceOffset(int count, bool columns)
{
var currentLine = CurrentLine;
char c;
while (count > 0 && Offset < currentLine.Length != default)
{
c = currentLine[Offset];
if (c == '\t')
{
var charsToTab = 4 - (Column % 4);
if (columns)
{
PartiallyConsumedTab = charsToTab > count;
var charsToAdvance = charsToTab > count ? count : charsToTab;
Column += charsToAdvance;
Offset += PartiallyConsumedTab ? 0 : 1;
count -= charsToAdvance;
}
else
{
PartiallyConsumedTab = false;
Column += charsToTab;
Offset += 1;
count -= 1;
}
}
else
{
PartiallyConsumedTab = false;
Offset += 1;
Column += 1;
count -= 1;
}
}
}
private Document ParseBlocks(string markdown)
{
LineNumber = 0;
lastMatchedContainer = document;
var lines = NewLineRegex.Split(markdown);
var length = lines.Length;
if (markdown.EndsWith(InlineParser.LineFeed))
{
length--;
}
for (var index = 0; index < length; index++)
{
IncorporateLine(lines[index]);
}
while (Tip != null)
{
Close(Tip, length);
}
return document;
}
private void IncorporateLine(string line)
{
Offset = 0;
Column = 0;
Blank = false;
PartiallyConsumedTab = false;
LineNumber++;
Block container = document;
OldTip = Tip;
Block? tail;
var allMatched = true;
CurrentLine = line;
while ((tail = container.LastChild) != null && tail.Open)
{
container = tail;
FindNextNonSpace();
switch (container.Matches(this))
{
case BlockMatch.Match: // we've matched, keep going
break;
case BlockMatch.Skip: // we've failed to match a block
allMatched = false;
break;
case BlockMatch.Break: // we've hit end of line for fenced code close and can return
return;
default:
throw new InvalidOperationException("Invalid continue result");
}
if (!allMatched)
{
container = container.Parent;
break;
}
}
AllClosed = container == OldTip;
lastMatchedContainer = container;
var matchedLeaf = container is not (Paragraph or Table) && container is Leaf;
while (!matchedLeaf)
{
FindNextNonSpace();
int blockIndex;
for (blockIndex = 0; blockIndex < blockStarts.Length; blockIndex++)
{
var blockStart = blockStarts[blockIndex];
var result = blockStart(this, container);
if (result == BlockStart.Container)
{
container = Tip;
break;
}
else if (result == BlockStart.Leaf)
{
container = Tip;
matchedLeaf = true;
break;
}
}
if (blockIndex == blockStarts.Length)
{
AdvanceNextNonSpace();
break;
}
}
// What remains at the offset is a text line. Add the text to the
// appropriate container.
if (!AllClosed && !Blank && this.Tip is Paragraph or Table)
{
// lazy paragraph continuation
if (Tip is Paragraph paragraph)
{
paragraph.AddLine(this);
}
else if (Tip is Table table)
{
table.AddLine(this);
}
}
else
{
// not a lazy continuation
// finalize any blocks not matched
CloseUnmatchedBlocks();
if (container is Leaf leaf)
{
leaf.AddLine(this);
if (container is HtmlBlock block && block.Type >= 1 && block.Type <= 5 && HtmlBlockCloseRegex[block.Type].IsMatch(line[Offset..]))
{
LastLineLength = line.Length;
Close(container, LineNumber);
}
}
else if (Offset < line.Length && !Blank)
{
var paragraph = AddChild<Paragraph>(Offset);
AdvanceNextNonSpace();
paragraph.AddLine(this);
}
}
LastLineLength = line.Length;
}
public int LastLineLength { get; set; }
public void Close(Block block, int lineNumber)
{
var above = block.Parent;
block.Range.End.Line = lineNumber;
block.Range.End.Column = LastLineLength;
block.Close(this);
Tip = above;
}
public T AddChild<T>(int offset) where T : Block, new()
{
var node = new T();
AddChild(node, offset);
return node;
}
public void AddChild(Block node, int offset)
{
while (Tip is not BlockContainer container || !container.CanContain(node))
{
Close(Tip, LineNumber - 1);
}
if (Tip is BlockContainer parent)
{
parent.Add(node);
}
var columnNumber = offset + 1; // offset 0 = column 1
node.Range.Start.Line = LineNumber;
node.Range.Start.Column = columnNumber;
Tip = node;
}
public void CloseUnmatchedBlocks()
{
if (!AllClosed)
{
while (OldTip != lastMatchedContainer)
{
var parent = OldTip.Parent;
Close(OldTip, LineNumber - 1);
OldTip = parent;
}
AllClosed = true;
}
}
public void AdvanceNextNonSpace()
{
Offset = NextNonSpace;
Column = NextNonSpaceColumn;
PartiallyConsumedTab = false;
}
private static readonly Func<BlockParser, Block, BlockStart>[] blockStarts =
[
BlockQuote.Start,
AtxHeading.Start,
FencedCodeBlock.Start,
HtmlBlock.Start,
SetExtHeading.Start,
ThematicBreak.Start,
ListItem.Start,
IndentedCodeBlock.Start,
Table.Start,
];
public bool AllClosed { get; private set; }
private Block lastMatchedContainer;
public void FindNextNonSpace()
{
var currentLine = CurrentLine;
var i = Offset;
var cols = Column;
char c = default;
while (i < currentLine.Length)
{
c = currentLine[i];
if (c == ' ')
{
i++;
cols++;
}
else if (c == '\t')
{
i++;
cols += 4 - (cols % 4);
}
else
{
break;
}
}
Blank = c == '\n' || c == '\r' || i == currentLine.Length;
NextNonSpace = i;
NextNonSpaceColumn = cols;
Indent = NextNonSpaceColumn - Column;
Indented = Indent >= CodeIndent;
}
public const int CodeIndent = 4;
public int Indent { get; private set; }
public bool Indented { get; private set; }
public int NextNonSpaceColumn { get; private set; }
public int NextNonSpace { get; private set; }
public bool Blank { get; private set; }
public bool PartiallyConsumedTab { get; private set; }
public Block Tip { get; set; }
public Block OldTip { get; private set; }
public string CurrentLine { get; private set; } = string.Empty;
public int Offset { get; set; }
public int Column { get; set; }
public int LineNumber { get; private set; }
private static readonly Regex LinkReferenceRegex = new(@"^[ \t]{0,3}\[");
private readonly Dictionary<string, LinkReference> linkReferences = [];
// https://spec.commonmark.org/0.31.2/#html-blocks
internal static readonly Regex[] HtmlBlockOpenRegex = [
new (@"."), // dummy for 1 based indexing
new (@"^<(?:script|pre|textarea|style)(?:\s|>|$)", RegexOptions.IgnoreCase),
new (@"^<!--"),
new (@"^<[?]"),
new (@"^<![A-Za-z]"),
new (@"^<!\[CDATA\["),
new (@"^<[/]?(?:address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[123456]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|section|search|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)(?:\s|[/]?[>]|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled),
new (@$"^(?:{OpenTag}|{CloseTag})\s*$", RegexOptions.IgnoreCase)
];
private static readonly Regex[] HtmlBlockCloseRegex = [
new (@"."), // dummy for 1 based indexing
new (@"</(?:script|pre|textarea|style)>", RegexOptions.IgnoreCase),
new (@"-->"),
new (@"\?>"),
new (@">"),
new (@"\]\]>")
];
public bool TryParseLinkReference(string markdown, out int newIndex)
{
newIndex = 0;
if (!LinkReferenceRegex.IsMatch(markdown))
{
return false;
}
var position = 0;
while (position < markdown.Length - 1 && (markdown[position] is not InlineParser.CloseBracket || (position > 0 && markdown[position - 1] is InlineParser.Backslash)))
{
position++;
}
if (position >= markdown.Length - 1)
{
return false;
}
position++;
if (position >= markdown.Length || markdown[position] is not InlineParser.Colon)
{
return false;
}
var colonIndex = position;
var closeIndex = colonIndex - 1;
var openIndex = 0;
while (openIndex < closeIndex && markdown[openIndex] is not InlineParser.OpenBracket)
{
openIndex++;
}
if (openIndex == closeIndex)
{
return false;
}
var id = new StringBuilder();
for (var index = openIndex + 1; index < closeIndex; index++)
{
var next = index < closeIndex - 1 ? markdown[index + 1] : default;
if (markdown[index] is not InlineParser.Backslash || !next.IsPunctuation())
{
id.Append(markdown[index]);
}
}
if (!InlineParser.TryParseDestinationAndTitle(markdown, colonIndex + 1, out var destination, out var title, out position))
{
return false;
}
var link = new LinkReference { Destination = destination, Title = title };
var key = id.ToString().ToLowerInvariant();
if (!linkReferences.ContainsKey(key))
{
linkReferences[key] = link;
}
newIndex = position;
return true;
}
}
enum BlockStart
{
Skip,
Container,
Leaf
}

View File

@@ -0,0 +1,64 @@
namespace Radzen.Blazor.Markdown;
/// <summary>
/// Represents a markdown block quote: <c>&gt; Quote</c>.
/// </summary>
public class BlockQuote : BlockContainer
{
/// <inheritdoc />
public override void Accept(INodeVisitor visitor)
{
visitor.VisitBlockQuote(this);
}
/// <inheritdoc />
public override bool CanContain(Block node)
{
return node is not ListItem;
}
internal override BlockMatch Matches(BlockParser parser)
{
if (!parser.Indented && parser.PeekNonSpace() == '>')
{
parser.AdvanceNextNonSpace();
parser.AdvanceOffset(1, false);
// optional following space
if (parser.Peek().IsSpaceOrTab())
{
parser.AdvanceOffset(1, true);
}
}
else
{
return BlockMatch.Skip;
}
return BlockMatch.Match;
}
internal static BlockStart Start(BlockParser parser, Block block)
{
if (!parser.Indented && parser.PeekNonSpace() == '>')
{
parser.AdvanceNextNonSpace();
parser.AdvanceOffset(1, false);
// optional following space
if (parser.Peek().IsSpaceOrTab())
{
parser.AdvanceOffset(1, true);
}
parser.CloseUnmatchedBlocks();
parser.AddChild<BlockQuote>(parser.NextNonSpace);
return BlockStart.Container;
}
return BlockStart.Skip;
}
}

View File

@@ -0,0 +1,10 @@
namespace Radzen.Blazor.Markdown;
static class CharExtensions
{
public static bool IsNullOrWhiteSpace(this char ch) => ch == '\0' || char.IsWhiteSpace(ch);
public static bool IsPunctuation(this char ch) => char.IsPunctuation(ch);
public static bool IsSpaceOrTab(this char ch) => ch == ' ' || ch == '\t';
}

View File

@@ -0,0 +1,19 @@
namespace Radzen.Blazor.Markdown;
/// <summary>
/// Represents a markdown inline code block: <c>`code`</c>.
/// </summary>
/// <param name="value"></param>
public class Code(string value) : Inline
{
/// <summary>
/// Gets or sets the code value.
/// </summary>
public string Value { get; set; } = value;
/// <inheritdoc />
public override void Accept(INodeVisitor visitor)
{
visitor.VisitCode(this);
}
}

View File

@@ -0,0 +1,35 @@
namespace Radzen.Blazor.Markdown;
/// <summary>
/// Represents a markdown document.
/// </summary>
public class Document : BlockContainer
{
/// <summary>
/// Initializes a new instance of the <see cref="Document"/> class.
/// </summary>
public Document()
{
Range.Start.Line = 1;
Range.Start.Column = 1;
}
/// <inheritdoc />
public override void Accept(INodeVisitor visitor)
{
visitor.VisitDocument(this);
}
/// <inheritdoc />
public override bool CanContain(Block node)
{
return node is not ListItem;
}
internal override void Close(BlockParser parser)
{
base.Close(parser);
LinkReferenceParser.Parse(parser, this);
}
}

View File

@@ -0,0 +1,13 @@
namespace Radzen.Blazor.Markdown;
/// <summary>
/// Represents an emphasis element in a markdown document: <c>_emphasis_</c> or <c>*emphasis*</c>.
/// </summary>
public class Emphasis : InlineContainer
{
/// <inheritdoc />
public override void Accept(INodeVisitor visitor)
{
visitor.VisitEmphasis(this);
}
}

View File

@@ -0,0 +1,98 @@
using System.Text.RegularExpressions;
namespace Radzen.Blazor.Markdown;
/// <summary>
/// Represents a fenced code block in a markdown document: <c>```</c> or <c>~~~</c>.
/// </summary>
public class FencedCodeBlock : Leaf
{
/// <summary>
/// The delimiter used to start and end the code block.
/// </summary>
public string Delimiter { get; private set; }
internal int Indent { get; private set; }
/// <summary>
/// The info string of the code block. This is the first line of the code block and is used to specify the language of the code block.
/// </summary>
public string Info { get; private set; }
/// <inheritdoc />
public override void Accept(INodeVisitor visitor)
{
visitor.VisitFencedCodeBlock(this);
}
internal override void Close(BlockParser parser)
{
base.Close(parser);
// first line becomes info string
var newlinePos = Value.IndexOf('\n');
var firstLine = Value[..newlinePos];
Info = firstLine.Trim();
Value = Value[(newlinePos + 1)..];
}
internal override BlockMatch Matches(BlockParser parser)
{
var line = parser.CurrentLine[parser.NextNonSpace..];
var indent = parser.Indent;
var match = ClosingFenceRegex.Match(line);
if (indent <= 3 && parser.PeekNonSpace() == Delimiter[0] && match.Success && match.Length >= Delimiter.Length)
{
// closing fence - we're at end of line, so we can return
parser.LastLineLength = parser.Offset + indent + match.Length;
parser.Close(this, parser.LineNumber);
return BlockMatch.Break;
}
else
{
// skip optional spaces of fence offset
var i = Indent;
while (i > 0 && parser.Peek().IsSpaceOrTab())
{
parser.AdvanceOffset(1, true);
i--;
}
}
return BlockMatch.Match;
}
private static readonly Regex ClosingFenceRegex = new(@"^(?:`{3,}|~{3,})(?=[ \t]*$)");
private static readonly Regex OpeningFenceRegex = new(@"^`{3,}(?!.*`)|^~{3,}");
internal static BlockStart Start(BlockParser parser, Block node)
{
if (parser.Indented)
{
return BlockStart.Skip;
}
var line = parser.CurrentLine[parser.NextNonSpace..];
var match = OpeningFenceRegex.Match(line);
if (match.Success)
{
parser.CloseUnmatchedBlocks();
var container = parser.AddChild<FencedCodeBlock>(parser.NextNonSpace);
container.Delimiter = match.Value;
container.Indent = parser.Indent;
parser.AdvanceNextNonSpace();
parser.AdvanceOffset(match.Value.Length, false);
return BlockStart.Leaf;
}
return BlockStart.Skip;
}
}

View File

@@ -0,0 +1,25 @@
namespace Radzen.Blazor.Markdown;
/// <summary>
/// A base class for all heading elements.
/// </summary>
public abstract class Heading : Leaf
{
/// <summary>
/// The level of the heading. The value is between 1 and 6.
/// </summary>
public int Level { get; set; }
/// <inheritdoc />
public override void Accept(INodeVisitor visitor)
{
visitor.VisitHeading(this);
}
internal override BlockMatch Matches(BlockParser parser)
{
// a heading can never container another line
return BlockMatch.Skip;
}
}

View File

@@ -0,0 +1,56 @@
using System.Text.RegularExpressions;
namespace Radzen.Blazor.Markdown;
/// <summary>
/// Represents an HTML block.
/// </summary>
public class HtmlBlock : Leaf
{
internal int Type { get; private set; }
/// <inheritdoc />
public override void Accept(INodeVisitor visitor)
{
visitor.VisitHtmlBlock(this);
}
internal override BlockMatch Matches(BlockParser parser)
{
return parser.Blank && (Type == 6 || Type == 7) ? BlockMatch.Skip : BlockMatch.Match;
}
private static readonly Regex TrailinNewLineRegex = new(@"\n$");
internal override void Close(BlockParser parser)
{
base.Close(parser);
Value = TrailinNewLineRegex.Replace(Value, "");
}
internal static BlockStart Start(BlockParser parser, Block node)
{
if (!parser.Indented && parser.PeekNonSpace() == '<')
{
var line = parser.CurrentLine[parser.NextNonSpace..];
for (var blockType = 1; blockType <= 7; blockType++) {
if (BlockParser.HtmlBlockOpenRegex[blockType].IsMatch(line) &&
(blockType < 7 || (node is not Paragraph &&
!(!parser.AllClosed && !parser.Blank && parser.Tip is Paragraph) // maybe lazy
))) {
parser.CloseUnmatchedBlocks();
// We don't adjust parser.offset;
// spaces are part of the HTML block:
var block = parser.AddChild<HtmlBlock>(parser.Offset);
block.Type = blockType;
return BlockStart.Leaf;
}
}
}
return BlockStart.Skip;
}
}

View File

@@ -0,0 +1,19 @@
namespace Radzen.Blazor.Markdown;
/// <summary>
/// Represents an inline HTML element.
/// </summary>
public class HtmlInline : Inline
{
/// <summary>
/// Gets or sets the HTML element value.
/// </summary>
public string Value { get; set; }
/// <inheritdoc />
public override void Accept(INodeVisitor visitor)
{
visitor.VisitHtmlInline(this);
}
}

View File

@@ -0,0 +1,136 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
#nullable enable
class HtmlSanitizer
{
private readonly ISet<string> allowedTags;
private readonly ISet<string> allowedAttributes;
public HtmlSanitizer(IEnumerable<string>? allowedHtmlTags, IEnumerable<string>? allowedHtmlAttributes)
{
allowedTags = allowedHtmlTags != null ? new HashSet<string>(allowedHtmlTags) : AllowedTags;
allowedAttributes = allowedHtmlAttributes != null ? new HashSet<string>(allowedHtmlAttributes) : AllowedAttributes;
}
private static ISet<string> AllowedTags { get; } = new HashSet<string>()
{
// https://developer.mozilla.org/en/docs/Web/Guide/HTML/HTML5/HTML5_element_list
"a", "abbr", "acronym", "address", "area", "b",
"big", "blockquote", "br", "button", "caption", "center", "cite",
"code", "col", "colgroup", "dd", "del", "dfn", "dir", "div", "dl", "dt",
"em", "fieldset", "font", "form", "h1", "h2", "h3", "h4", "h5", "h6",
"hr", "i", "img", "input", "ins", "kbd", "label", "legend", "li", "map",
"menu", "ol", "optgroup", "option", "p", "pre", "q", "s", "samp",
"select", "small", "span", "strike", "strong", "sub", "sup", "table",
"tbody", "td", "textarea", "tfoot", "th", "thead", "tr", "tt", "u",
"ul", "var", "section", "nav", "article", "aside", "header", "footer", "main",
"figure", "figcaption", "data", "time", "mark", "ruby", "rt", "rp", "bdi", "wbr",
"datalist", "keygen", "output", "progress", "meter", "details", "summary", "menuitem",
"html", "head", "body"
};
public static ISet<string> AllowedAttributes { get; } = new HashSet<string>()
{
// https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
"abbr", "accept", "accept-charset", "accesskey",
"action", "align", "alt", "axis", "bgcolor", "border", "cellpadding",
"cellspacing", "char", "charoff", "charset", "checked", "cite", "class",
"clear", "cols", "colspan", "color", "compact", "coords", "datetime",
"dir", "disabled", "enctype", "for", "frame", "headers", "height",
"href", "hreflang", "hspace", "id", "ismap", "label", "lang",
"longdesc", "maxlength", "media", "method", "multiple", "name",
"nohref", "noshade", "nowrap", "prompt", "readonly", "rel", "rev",
"rows", "rowspan", "rules", "scope", "selected", "shape", "size",
"span", "src", "start", "style", "summary", "tabindex", "target", "title",
"type", "usemap", "valign", "value", "vspace", "width",
"high", "keytype", "list", "low", "max", "min", "novalidate", "open", "optimum",
"pattern", "placeholder", "pubdate", "radiogroup", "required", "reversed", "spellcheck", "step",
"wrap", "challenge", "contenteditable", "draggable", "dropzone", "autocomplete", "autosave",
};
public static ISet<string> UriAttributes { get; } = new HashSet<string>()
{
"action", "background", "dynsrc", "href", "lowsrc", "src"
};
public string Sanitize(string input)
{
if (string.IsNullOrWhiteSpace(input))
{
return string.Empty;
}
return Regex.Replace(input, @"</?([a-zA-Z0-9]+)(\s[^>]*)?>", SanitizeTag);
}
private string SanitizeTag(Match match)
{
var tag = match.Groups[1].Value.ToLowerInvariant();
if (!allowedTags.Contains(tag))
{
return string.Empty;
}
var attributes = match.Groups[2].Value;
var safeAttributes = Regex.Replace(attributes, @"(\w+)\s*=\s*(""[^""]*""|'[^']*'|[^\s>]+)", SanitizeAttribute);
return $"<{(match.Value.StartsWith("</") ? "/" : "")}{tag}{safeAttributes}>";
}
private string SanitizeAttribute(Match match)
{
var name = match.Groups[1].Value.ToLowerInvariant();
var value = match.Groups[2].Value;
if (!allowedAttributes.Contains(name))
{
return string.Empty;
}
if (name == "style")
{
var decoded = HtmlDecode(value).ToLowerInvariant();
if (Regex.IsMatch(decoded, @"expression|javascript:|vbscript:|url\s*\(\s*(['""])?\s*javascript", RegexOptions.IgnoreCase))
{
return string.Empty;
}
}
if (UriAttributes.Contains(name) && IsDangerousUrl(value))
{
return string.Empty;
}
if ((value.StartsWith('\'') && value.EndsWith('\'')) || (value.StartsWith('"') && value.EndsWith('"')))
{
value = value[1..^1];
}
return $" {name}=\"{value}\"";
}
private static string HtmlDecode(string input)
{
return System.Web.HttpUtility.HtmlDecode(input);
}
public static bool IsDangerousUrl(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return false;
}
var decoded = HtmlDecode(value).Trim().ToLowerInvariant();
return decoded.StartsWith("javascript:") ||
decoded.StartsWith("vbscript:") ||
decoded.StartsWith("data:text/html") ||
decoded.Contains("expression(");
}
}

View File

@@ -0,0 +1,25 @@
using System.Collections.Generic;
namespace Radzen.Blazor.Markdown;
/// <summary>
/// Represents a block node that has inline children.
/// </summary>
public interface IBlockInlineContainer
{
/// <summary>
/// Gets the inline children of the block.
/// </summary>
IReadOnlyList<Inline> Children { get; }
/// <summary>
/// Adds an inline child to the block.
/// </summary>
/// <param name="child"></param>
void Add(Inline child);
/// <summary>
/// Gets string value of the block.
/// </summary>
string Value { get; }
}

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
namespace Radzen.Blazor.Markdown;
/// <summary>
/// Represents a markdown node that can be visited by a <see cref="INodeVisitor"/>.
/// </summary>
public interface INode
{
/// <summary>
/// Accepts a <see cref="INodeVisitor"/>.
/// </summary>
/// <param name="visitor"></param>
public void Accept(INodeVisitor visitor);
}

View File

@@ -0,0 +1,128 @@
namespace Radzen.Blazor.Markdown;
/// <summary>
/// Represents a visitor for Markdown AST nodes.
/// </summary>
public interface INodeVisitor
{
/// <summary>
/// Visits a heading node.
/// </summary>
void VisitHeading(Heading heading);
/// <summary>
/// Visits a paragraph node.
/// </summary>
void VisitParagraph(Paragraph paragraph);
/// <summary>
/// Visits a block quote node.
/// </summary>
void VisitBlockQuote(BlockQuote blockQuote);
/// <summary>
/// Visits a document node.
/// </summary>
void VisitDocument(Document document);
/// <summary>
/// Visits an unordered list node.
/// </summary>
void VisitUnorderedList(UnorderedList unorderedList);
/// <summary>
/// Visits a list item node.
/// </summary>
void VisitListItem(ListItem listItem);
/// <summary>
/// Visits a text node.
/// </summary>
void VisitText(Text text);
/// <summary>
/// Visits an ordered list node.
/// </summary>
void VisitOrderedList(OrderedList orderedList);
/// <summary>
/// Visits an emphasis node.
/// </summary>
void VisitEmphasis(Emphasis emphasis);
/// <summary>
/// Visits a strong node.
/// </summary>
void VisitStrong(Strong strong);
/// <summary>
/// Visits a code node.
/// </summary>
void VisitCode(Code code);
/// <summary>
/// Visits a link node.
/// </summary>
void VisitLink(Link link);
/// <summary>
/// Visits an image node.
/// </summary>
void VisitImage(Image image);
/// <summary>
/// Visits an HTML inline node.
/// </summary>
void VisitHtmlInline(HtmlInline html);
/// <summary>
/// Visits a line break node.
/// </summary>
void VisitLineBreak(LineBreak lineBreak);
/// <summary>
/// Visits a soft line break node.
/// </summary>
void VisitSoftLineBreak(SoftLineBreak softLineBreak);
/// <summary>
/// Visits a thematic break node.
/// </summary>
void VisitThematicBreak(ThematicBreak thematicBreak);
/// <summary>
/// Visits an indented code block node.
/// </summary>
void VisitIndentedCodeBlock(IndentedCodeBlock codeBlock);
/// <summary>
/// Visits a fenced code block node.
/// </summary>
void VisitFencedCodeBlock(FencedCodeBlock fencedCodeBlock);
/// <summary>
/// Visits an HTML block node.
/// </summary>
void VisitHtmlBlock(HtmlBlock htmlBlock);
/// <summary>
/// Visits a table node.
/// </summary>
void VisitTable(Table table);
/// <summary>
/// Visits a table header row node.
/// </summary>
void VisitTableHeaderRow(TableHeaderRow header);
/// <summary>
/// Visits a table row node.
/// </summary>
void VisitTableRow(TableRow row);
/// <summary>
/// Visits a table cell node.
/// </summary>
void VisitTableCell(TableCell cell);
}

View File

@@ -0,0 +1,23 @@
namespace Radzen.Blazor.Markdown;
/// <summary>
/// Represents an inline image element: <c>![Alt text](/path/to/img.jpg "Optional title")</c>
/// </summary>
public class Image : InlineContainer
{
/// <summary>
/// Gets or sets the destination (URL) of the image.
/// </summary>
public string Destination { get; set; }
/// <summary>
/// Gets or sets the alternative text of the image.
/// </summary>
public string Title { get; set; }
/// <inheritdoc />
public override void Accept(INodeVisitor visitor)
{
visitor.VisitImage(this);
}
}

View File

@@ -0,0 +1,69 @@
using System.Linq;
using System.Text.RegularExpressions;
namespace Radzen.Blazor.Markdown;
/// <summary>
/// Represents a markdown indented code block.
/// </summary>
public class IndentedCodeBlock : Leaf
{
/// <inheritdoc />
public override void Accept(INodeVisitor visitor)
{
visitor.VisitIndentedCodeBlock(this);
}
internal override BlockMatch Matches(BlockParser parser)
{
if (parser.Indent >= BlockParser.CodeIndent)
{
parser.AdvanceOffset(BlockParser.CodeIndent, true);
}
else if (parser.Blank)
{
parser.AdvanceNextNonSpace();
}
else
{
return BlockMatch.Skip;
}
return BlockMatch.Match;
}
private static readonly Regex TrailingWhiteSpaceRegex = new(@"^[ \t]*$");
internal override void Close(BlockParser parser)
{
base.Close(parser);
var lines = Value.Split('\n').ToList();;
// Note that indented code block cannot be empty, so
// lines.length cannot be zero.
while (TrailingWhiteSpaceRegex.IsMatch(lines[^1]))
{
lines.RemoveAt(lines.Count - 1);
}
Value = string.Join('\n', lines) + '\n';
Range.End.Line = Range.Start.Line + lines.Count - 1;
Range.End.Column = Range.Start.Column + lines[^1].Length - 1;
}
internal static BlockStart Start(BlockParser parser, Block container)
{
if (parser.Indented && parser.Tip is not Paragraph && !parser.Blank)
{
// indented code
parser.AdvanceOffset(BlockParser.CodeIndent, true);
parser.CloseUnmatchedBlocks();
parser.AddChild<IndentedCodeBlock>(parser.Offset);
return BlockStart.Leaf;
}
return BlockStart.Skip;
}
}

View File

@@ -0,0 +1,12 @@
namespace Radzen.Blazor.Markdown;
/// <summary>
/// Base class for markdown inline nodes.
/// </summary>
public abstract class Inline : INode
{
/// <summary>
/// Accepts a visitor.
/// </summary>
public abstract void Accept(INodeVisitor visitor);
}

View File

@@ -0,0 +1,25 @@
using System.Collections.Generic;
namespace Radzen.Blazor.Markdown;
/// <summary>
/// Base class for inline elements that contain other inline elements.
/// </summary>
public abstract class InlineContainer : Inline
{
private readonly List<Inline> children = [];
/// <summary>
/// Gets the children of the container.
/// </summary>
public IReadOnlyList<Inline> Children => children;
/// <summary>
/// Appends a child to the container.
/// </summary>
/// <param name="node">The child to add.</param>
public void Add(Inline node)
{
children.Add(node);
}
}

View File

@@ -0,0 +1,959 @@
using System.Text.RegularExpressions;
using System.Text;
using System.Collections.Generic;
using System;
namespace Radzen.Blazor.Markdown;
class InlineParser
{
class Delimiter
{
public char Char { get; set; }
public int Length { get; set; }
public int Position { get; set; }
public Text Node { get; set; }
public bool CanOpen { get; set; }
public bool CanClose { get; set; }
public bool Active { get; set; } = true;
}
private const char Asterisk = '*';
private const char Underscore = '_';
internal const char Backslash = '\\';
private const char Null = '\0';
private const char Backtick = '`';
internal const char Space = ' ';
internal const char LineFeed = '\n';
private const char CarrigeReturn = '\r';
internal const char OpenBracket = '[';
internal const char CloseBracket = ']';
private const char OpenParenthesis = '(';
private const char CloseParenthesis = ')';
internal const char Quote = '"';
internal const char OpenAngleBracket = '<';
internal const char CloseAngleBracket = '>';
private const char SingleQuote = '\'';
private const char Exclamation = '!';
internal const char Colon = ':';
private readonly List<Inline> inlines = [];
private readonly List<Delimiter> delimiters = [];
private readonly StringBuilder buffer = new();
enum LinkState
{
Text,
Destination,
Title
}
private void AddTextNode(bool trim = false)
{
if (buffer.Length > 0)
{
var output = new StringBuilder();
for (var index = 0; index < buffer.Length; index++)
{
var ch = buffer[index];
if (ch is Backslash && index < buffer.Length - 1 && !buffer[index + 1].IsPunctuation())
{
continue;
}
else
{
output.Append(ch);
}
}
var value = output.ToString();
if (trim)
{
value = value.TrimEnd();
}
inlines.Add(new Text(value));
buffer.Clear();
}
}
private bool TryParseCode(string text, int index, out int newIndex)
{
if (text[index] is not Backtick)
{
newIndex = index;
return false;
}
AddTextNode();
// Count opening backticks
var openingCount = 0;
var position = index;
while (position < text.Length && text[position] is Backtick)
{
openingCount++;
position++;
}
// Find matching closing backticks
var searchStart = position;
var bestMatch = -1;
while (position < text.Length)
{
// Count consecutive backticks
var count = 0;
var closingStart = position;
while (position < text.Length && text[position] is Backtick)
{
count++;
position++;
}
if (count == openingCount)
{
bestMatch = closingStart;
break;
}
if (position < text.Length)
{
position++;
}
}
if (bestMatch >= 0)
{
var content = text[searchStart..bestMatch];
content = BlockParser.NewLineRegex.Replace(content, $"{Space}");
if (content.StartsWith(Space) && content.EndsWith(Space) && !string.IsNullOrWhiteSpace(content))
{
content = content[1..^1];
}
inlines.Add(new Code(content));
newIndex = bestMatch + openingCount;
return true;
}
inlines.Add(new Text($"{new string(Backtick, openingCount)}"));
newIndex = index + openingCount;
return true;
}
private bool TryParseBackslash(string text, int index, char next, out int newIndex)
{
if (text[index] is not Backslash)
{
newIndex = index;
return false;
}
if (next.IsPunctuation())
{
AddTextNode();
inlines.Add(new Text(text[index + 1].ToString()));
newIndex = index + 2;
return true;
}
buffer.Append(text[index]);
newIndex = index + 1;
return true;
}
private bool TryParseDelimiter(string text, int index, char next, char prev, out int newIndex)
{
var ch = text[index];
if (ch is not (Asterisk or Underscore or OpenBracket) && (ch is not Exclamation || next is not OpenBracket))
{
newIndex = index;
return false;
}
AddTextNode();
var position = index;
while (position < text.Length && text[position] == ch)
{
buffer.Append(ch);
position++;
}
if (ch is Exclamation)
{
buffer.Append(OpenBracket);
position++;
}
next = position < text.Length ? text[position] : Null;
if (buffer.Length > 0)
{
var node = new Text(buffer.ToString());
var leftFlanking = LeftFlanking(prev, next);
var rightFlanking = RightFlanking(prev, next);
var canOpen = false;
var canClose = false;
if (ch is Asterisk)
{
canOpen = leftFlanking;
canClose = rightFlanking;
}
if (ch is Underscore)
{
canClose = rightFlanking && (!leftFlanking || next.IsPunctuation());
canOpen = leftFlanking && (!rightFlanking || prev.IsPunctuation());
}
var delimiter = new Delimiter
{
Node = node,
Char = ch,
Length = buffer.Length,
Position = index,
CanClose = canClose,
CanOpen = canOpen
};
delimiters.Add(delimiter);
inlines.Add(node);
buffer.Clear();
}
newIndex = position;
return true;
}
private static bool RightFlanking(char prev, char next)
{
/*
that is (1) not preceded by Unicode whitespace, and either (2a) not preceded by a Unicode punctuation character,
or (2b) preceded by a Unicode punctuation character and followed by Unicode whitespace or a Unicode punctuation character.
*/
return !prev.IsNullOrWhiteSpace() && (!prev.IsPunctuation() || next.IsNullOrWhiteSpace() || next.IsPunctuation());
}
private static bool LeftFlanking(char prev, char next)
{
/*
that is (1) not followed by Unicode whitespace, and either (2a) not followed by a Unicode punctuation character,
or (2b) followed by a Unicode punctuation character and preceded by Unicode whitespace or a Unicode punctuation character.
*/
return !next.IsNullOrWhiteSpace() && (!next.IsPunctuation() || prev.IsNullOrWhiteSpace() || prev.IsPunctuation());
}
public static List<Inline> Parse(string text, Dictionary<string, LinkReference> linkReferences)
{
var parser = new InlineParser();
return parser.ParseInlines(text.Trim(), linkReferences);
}
private static readonly Regex EmailRegex = new(@"^([a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)");
private bool TryParseAutoLink(string text, int index, out int newIndex)
{
newIndex = index;
if (text[index] is not OpenAngleBracket)
{
return false;
}
var destination = new StringBuilder();
var position = index + 1;
while (position < text.Length && text[position] is not CloseAngleBracket)
{
destination.Append(text[position]);
position++;
}
if (position >= text.Length || text[position] is not CloseAngleBracket)
{
return false;
}
var url = destination.ToString();
if (url.Contains(Space))
{
return false;
}
var content = url;
if (EmailRegex.IsMatch(url))
{
url = $"mailto:{url}";
}
else if (!Uri.TryCreate(url, UriKind.Absolute, out _))
{
return false;
}
var link = new Link { Destination = url };
link.Add(new Text(content));
inlines.Add(link);
newIndex = position + 1;
return true;
}
private List<Inline> ParseInlines(string text, Dictionary<string, LinkReference> references)
{
var index = 0;
while (index < text.Length)
{
if (TryParseHtml(text, index, out index))
{
continue;
}
if (TryParseAutoLink(text, index, out index))
{
continue;
}
if (TryParseCode(text, index, out index))
{
continue;
}
if (TryParseLineBreak(text, index, out index))
{
continue;
}
if (TryParseSoftLineBreak(text, index, out index))
{
continue;
}
char next = index < text.Length - 1 ? text[index + 1] : Null;
if (TryParseBackslash(text, index, next, out index))
{
continue;
}
char prev = index > 0 ? text[index - 1] : Null;
if (TryParseDelimiter(text, index, next, prev, out index))
{
continue;
}
if (TryParseLinkFromReference(text, index, references, out index))
{
continue;
}
if (TryParseLinkOrImage(text, index, out index))
{
continue;
}
buffer.Append(text[index]);
index++;
}
AddTextNode();
ParseEmphasisAndStrong();
NormalizeText();
return inlines;
}
private void NormalizeText()
{
if (inlines.Count > 0)
{
if (inlines[0] is Text first)
{
first.Value = first.Value.TrimStart();
}
if (inlines[^1] is Text last)
{
last.Value = last.Value.TrimEnd();
}
}
}
private bool TryParseSoftLineBreak(string text, int index, out int newIndex)
{
newIndex = index;
if (TryParseNewLine(text, index, out var position))
{
AddTextNode(trim: true);
inlines.Add(new SoftLineBreak());
newIndex = position;
return true;
}
return false;
}
private bool TryParseLineBreak(string text, int index, out int newIndex)
{
newIndex = index;
if (text[index] is not Space && text[index] is not Backslash)
{
return false;
}
var position = index + 1;
if (position < text.Length && text[position] is Space)
{
while (position < text.Length && text[position] is Space)
{
position++;
}
}
if (text[index] is Space && position == index + 1)
{
return false;
}
if (position < text.Length && TryParseNewLine(text, position, out position))
{
AddTextNode(trim: true);
inlines.Add(new LineBreak());
newIndex = position;
return true;
}
return false;
}
private static bool TryParseNewLine(string text, int position, out int newIndex)
{
newIndex = position;
if (position >= text.Length)
{
return false;
}
if (text[position] is LineFeed)
{
newIndex = position + 1;
return true;
}
if (text[position] is CarrigeReturn && position < text.Length - 1 && text[position + 1] is LineFeed)
{
newIndex = position + 2;
return true;
}
return false;
}
private bool TryParseHtml(string text, int index, out int newIndex)
{
newIndex = index;
var match = BlockParser.HtmlRegex.Match(text[index..]);
if (match.Success)
{
AddTextNode();
var value = text[index..(index + match.Length)];
inlines.Add(new HtmlInline { Value = value });
newIndex = index + match.Length;
return true;
}
return false;
}
internal static bool TryParseDestinationAndTitle(string text, int position, out string destination, out string title, out int newPosition)
{
newPosition = position;
destination = string.Empty;
title = string.Empty;
// Skip whitespace
while (position < text.Length && text[position] is Space or LineFeed)
{
position++;
}
if (position >= text.Length)
{
return false;
}
// Parse destination
var destinationBuilder = new StringBuilder();
var angleBrackets = position < text.Length && text[position] is OpenAngleBracket;
if (angleBrackets)
{
position++;
}
var parentheses = 0;
while (position < text.Length)
{
var ch = text[position];
var prev = position > 0 ? text[position - 1] : Null;
var next = position < text.Length - 1 ? text[position + 1] : Null;
if (angleBrackets && ch is CloseAngleBracket && prev is not Backslash)
{
position++;
break;
}
if (!angleBrackets)
{
if (ch is OpenParenthesis && prev is not Backslash)
{
parentheses++;
}
else if (ch is CloseParenthesis && prev is not Backslash)
{
if (parentheses == 0)
{
break;
}
parentheses--;
}
if (ch is Space or LineFeed)
{
break;
}
}
if (ch is Backslash && next.IsPunctuation())
{
position++;
continue;
}
destinationBuilder.Append(ch);
position++;
}
if (angleBrackets)
{
// Skip whitespace after angle brackets
while (position < text.Length && text[position] is Space)
{
position++;
}
}
// Parse title if present
var titleBuilder = new StringBuilder();
if (position < text.Length && text[position].IsNullOrWhiteSpace())
{
var lines = 0;
while (position < text.Length && text[position].IsNullOrWhiteSpace())
{
if (text[position] is LineFeed)
{
lines++;
}
position++;
}
var titleStart = position;
if (position < text.Length)
{
var titleDelimiter = text[position];
if (titleDelimiter is Quote or SingleQuote or OpenParenthesis)
{
position++; // Skip opening delimiter
char closingDelimiter = titleDelimiter is OpenParenthesis ? CloseParenthesis : titleDelimiter;
while (position < text.Length && (text[position] != closingDelimiter || text[position - 1] is Backslash))
{
if (text[position] is LineFeed && titleBuilder.Length > 0 && titleBuilder[^1] is LineFeed)
{
return false;
}
titleBuilder.Append(text[position]);
position++;
}
if (position < text.Length && text[position] == closingDelimiter)
{
position++; // Skip closing delimiter
// Skip whitespace after title
while (position < text.Length && text[position] is Space)
{
position++;
}
if (position < text.Length)
{
if (position < text.Length && text[position] is not (CloseParenthesis or LineFeed))
{
if (lines > 0)
{
// non-white space characters after title
newPosition = titleStart;
destination = destinationBuilder.ToString();
title = string.Empty;
return true;
}
else
{
return false;
}
}
}
}
else
{
return false;
}
}
}
}
if (position < text.Length)
{
if (text[position] is LineFeed)
{
position++;
}
else if (text[position] is Quote or SingleQuote or OpenParenthesis)
{
return false;
}
}
destination = destinationBuilder.ToString();
title = titleBuilder.ToString();
newPosition = position;
return true;
}
private bool TryParseLinkOrImage(string text, int index, out int newIndex)
{
newIndex = index;
if (!TryGetOpenerIndex(text, index, out var openerIndex, out var position))
{
return false;
}
if (!TryParseDestinationAndTitle(text, position, out var destination, out var title, out position))
{
return false;
}
if (position >= text.Length || text[position] is not CloseParenthesis)
{
return false;
}
var opener = delimiters[openerIndex];
InlineContainer container = opener.Char == Exclamation ? new Image { Destination = destination, Title = title } : new Link { Destination = destination, Title = title };
ReplaceOpener(openerIndex, container);
newIndex = position + 1;
if (container is Link)
{
for (var delimiterIndex = 0; delimiterIndex < openerIndex; delimiterIndex++)
{
if (delimiters[delimiterIndex].Char == OpenBracket)
{
delimiters[delimiterIndex].Active = false;
}
}
}
delimiters.Remove(opener);
return true;
}
private bool TryGetOpenerIndex(string text, int index, out int openerIndex, out int position)
{
position = index;
openerIndex = -1;
if (text[index] is not CloseBracket)
{
return false;
}
var di = delimiters.Count - 1;
while (di >= 0)
{
var delimiter = delimiters[di];
if ((delimiter.Active && delimiter.Char is OpenBracket) || delimiter.Char is Exclamation)
{
openerIndex = di;
break;
}
di--;
}
if (di < 0)
{
return false;
}
AddTextNode();
position = index + 1;
// Skip if not followed by opening parenthesis
if (position >= text.Length || text[position] is not OpenParenthesis)
{
delimiters.RemoveAt(openerIndex);
return false;
}
position++; // Skip opening parenthesis
return true;
}
private void ReplaceOpener(int openerIndex, InlineContainer parent)
{
var startIndex = inlines.FindIndex(delimiters[openerIndex].Node.Equals);
ParseEmphasisAndStrong(openerIndex);
var endIndex = inlines.Count - startIndex;
var children = inlines.GetRange(startIndex + 1, endIndex - 1);
inlines.RemoveRange(startIndex, endIndex);
foreach (var child in children)
{
parent.Add(child);
}
inlines.Insert(startIndex, parent);
}
private bool TryParseLinkFromReference(string text, int index, Dictionary<string, LinkReference> references, out int newIndex)
{
newIndex = index;
if (references.Count == 0 || text[index] is not CloseBracket)
{
return false;
}
var openerIndex = FindOpenBracketIndex(OpenBracket);
if (openerIndex < 0)
{
return false;
}
AddTextNode();
var startIndex = inlines.FindIndex(delimiters[openerIndex].Node.Equals);
var endIndex = inlines.Count - startIndex;
var children = inlines.GetRange(startIndex + 1, endIndex - 1);
var id = new StringBuilder();
foreach (var child in children)
{
if (child is Text textNode)
{
id.Append(textNode.Value);
}
}
if (!references.TryGetValue(id.ToString().ToLowerInvariant(), out var reference))
{
return false;
}
var link = new Link { Destination = reference.Destination, Title = reference.Title };
foreach (var child in children)
{
link.Add(child);
}
inlines.RemoveRange(startIndex, endIndex);
inlines.Insert(startIndex, link);
link.Destination = reference.Destination;
link.Title = reference.Title;
newIndex = index + 1;
return true;
}
private int FindOpenBracketIndex(char ch)
{
for (var index = delimiters.Count - 1; index >= 0; index--)
{
if (delimiters[index].Char == ch)
{
return index;
}
}
return -1;
}
private void ParseEmphasisAndStrong(int index = -1)
{
var closerIndex = 0;
while ((closerIndex = FindCloserIndex()) > 0)
{
var openerIndex = FindOpenerIndex(closerIndex, index);
if (openerIndex >= 0)
{
var closer = delimiters[closerIndex];
var opener = delimiters[openerIndex];
var startIndex = inlines.FindIndex(opener.Node.Equals);
var endIndex = inlines.FindIndex(closer.Node.Equals);
if (startIndex >= 0 && endIndex >= 0)
{
var innerInlines = inlines.GetRange(startIndex + 1, endIndex - startIndex - 1);
var charsToConsume = closer.Length == opener.Length && closer.Length > 1 ? 2 : 1;
InlineContainer parent = charsToConsume == 1 ? new Emphasis() : new Strong();
foreach (var child in innerInlines)
{
parent.Add(child);
}
opener.Length -= charsToConsume;
if (opener.Length > 0)
{
opener.Node.Value = opener.Node.Value[..^charsToConsume];
startIndex += charsToConsume;
}
closer.Length -= charsToConsume;
if (closer.Length > 0)
{
closer.Node.Value = closer.Node.Value[..^charsToConsume];
endIndex -= charsToConsume;
}
inlines.RemoveRange(startIndex, endIndex - startIndex + 1);
inlines.Insert(startIndex, parent);
}
delimiters.RemoveAt(closerIndex);
delimiters.RemoveAt(openerIndex);
}
else
{
break;
}
}
}
private int FindCloserIndex()
{
for (var index = 1; index < delimiters.Count; index++)
{
var delimiter = delimiters[index];
if (delimiter.CanClose && (delimiter.Char is Asterisk or Underscore))
{
return index;
}
}
return -1;
}
private int FindOpenerIndex(int startIndex, int endIndex)
{
var closer = delimiters[startIndex];
for (var index = startIndex - 1; index > endIndex; index--)
{
var delimiter = delimiters[index];
if (delimiter.CanOpen && delimiter.Char == closer.Char)
{
return index;
}
}
return -1;
}
}

View File

@@ -0,0 +1,22 @@
using System.Collections.Generic;
namespace Radzen.Blazor.Markdown;
class InlineVisitor(Dictionary<string, LinkReference> references) : NodeVisitorBase
{
public override void VisitHeading(Heading heading) => ParseChildren(heading, references);
private static void ParseChildren(IBlockInlineContainer node, Dictionary<string, LinkReference> references)
{
var inlines = InlineParser.Parse(node.Value, references);
foreach (var inline in inlines)
{
node.Add(inline);
}
}
public override void VisitParagraph(Paragraph paragraph) => ParseChildren(paragraph, references);
public override void VisitTableCell(TableCell cell) => ParseChildren(cell, references);
}

View File

@@ -0,0 +1,42 @@
using System.Collections.Generic;
namespace Radzen.Blazor.Markdown;
/// <summary>
/// Base class for markdown leaf block nodes.
/// </summary>
public abstract class Leaf : Block, IBlockInlineContainer
{
/// <summary>
/// Gets or sets the value of the leaf node.
/// </summary>
public string Value { get; set; } = string.Empty;
private readonly List<Inline> children = [];
/// <summary>
/// Gets the children of the leaf node.
/// </summary>
public IReadOnlyList<Inline> Children => children;
/// <summary>
/// Appends a child to the leaf node.
/// </summary>
public void Add(Inline node)
{
children.Add(node);
}
internal void AddLine(BlockParser blockParser)
{
if (blockParser.PartiallyConsumedTab)
{
blockParser.Offset += 1;
var charsToTab = 4 - (blockParser.Column % 4);
Value += new string(' ', charsToTab);
}
Value += blockParser.CurrentLine[blockParser.Offset..] + "\n";
}
}

View File

@@ -0,0 +1,13 @@
namespace Radzen.Blazor.Markdown;
/// <summary>
/// Represents a line break node. Line breaks are usually empty lines and are used to separate paragraphs.
/// </summary>
public class LineBreak : Inline
{
/// <inheritdoc />
public override void Accept(INodeVisitor visitor)
{
visitor.VisitLineBreak(this);
}
}

View File

@@ -0,0 +1,23 @@
namespace Radzen.Blazor.Markdown;
/// <summary>
/// Represents a link element: <c>[Link text](/path/to/page "Optional title")</c>
/// </summary>
public class Link : InlineContainer
{
/// <summary>
/// Gets or sets the destination (URL) of the link.
/// </summary>
public string Destination { get; set; }
/// <summary>
/// Gets or sets the link title.
/// </summary>
public string Title { get; set; }
/// <inheritdoc />
public override void Accept(INodeVisitor visitor)
{
visitor.VisitLink(this);
}
}

View File

@@ -0,0 +1,7 @@
namespace Radzen.Blazor.Markdown;
class LinkReference
{
public string Destination { get; set; }
public string Title { get; set; }
}

View File

@@ -0,0 +1,46 @@
using System.Collections.Generic;
namespace Radzen.Blazor.Markdown;
class LinkReferenceParser(BlockParser parser) : NodeVisitorBase
{
private readonly List<Block> emptyNodes = [];
public override void VisitParagraph(Paragraph paragraph)
{
var hasReferenceDefs = false;
// Try parsing the beginning as link reference definitions;
// Note that link reference definitions must be the beginning of a
// paragraph node since link reference definitions cannot interrupt
// paragraphs.
while (paragraph.Value.Peek() == '[' && parser.TryParseLinkReference(paragraph.Value, out var position))
{
var removedText = paragraph.Value[..position];
paragraph.Value = paragraph.Value[position..];
hasReferenceDefs = true;
var lines = removedText.Split('\n');
// -1 for final newline.
paragraph.Range.Start.Line += lines.Length - 1;
}
if (hasReferenceDefs && string.IsNullOrWhiteSpace(paragraph.Value))
{
emptyNodes.Add(paragraph);
}
}
public static void Parse(BlockParser parser, Document document)
{
var visitor = new LinkReferenceParser(parser);
document.Accept(visitor);
foreach (var node in visitor.emptyNodes)
{
node.Remove();
}
}
}

View File

@@ -0,0 +1,74 @@
namespace Radzen.Blazor.Markdown;
/// <summary>
/// Base class for list elements (ordered and unordered).
/// </summary>
public abstract class List : BlockContainer
{
/// <summary>
/// Gets or sets the list marker.
/// </summary>
public char Marker { get; set; }
internal int MarkerOffset { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the list is tight. Tight lists have no space between their items.
/// </summary>
public bool Tight { get; set; } = true;
internal int Padding { get; set; }
internal string Delimiter { get; set; }
internal override BlockMatch Matches(BlockParser parser)
{
return BlockMatch.Match;
}
/// <inheritdoc />
public override bool CanContain(Block node)
{
return node is ListItem;
}
private static bool EndsWithBlankLine(Block block)
{
return block.Next != null && block.Range.End.Line != block.Next.Range.Start.Line - 1;
}
internal override void Close(BlockParser parser)
{
base.Close(parser);
var item = FirstChild;
while (item != null)
{
// check for non-final list item ending with blank line:
if (item.Next != null && EndsWithBlankLine(item))
{
Tight = false;
break;
}
// recurse into children of list item, to see if there are
// spaces between any of them:
var subitem = item.FirstChild;
while (subitem != null)
{
if (subitem.Next != null && EndsWithBlankLine(subitem))
{
Tight = false;
break;
}
subitem = subitem.Next;
}
item = item.Next;
}
if (LastChild != null)
{
Range.End = LastChild.Range.End;
}
}
}

View File

@@ -0,0 +1,199 @@
using System.Text.RegularExpressions;
namespace Radzen.Blazor.Markdown;
#nullable enable
/// <summary>
/// Represents a list item node.
/// </summary>
public class ListItem : BlockContainer
{
/// <inheritdoc />
public override void Accept(INodeVisitor visitor)
{
visitor.VisitListItem(this);
}
/// <inheritdoc />
public override bool CanContain(Block node)
{
return node is not ListItem;
}
internal override BlockMatch Matches(BlockParser parser)
{
if (parser.Blank)
{
if (Children.Count == 0)
{
// Blank line after empty list item
return BlockMatch.Skip;
}
else
{
parser.AdvanceNextNonSpace();
}
}
else if (parser.Indent >= data.MarkerOffset + data.Padding)
{
parser.AdvanceOffset(data.MarkerOffset + data.Padding, true);
}
else
{
return BlockMatch.Skip;
}
return BlockMatch.Match;
}
internal override void Close(BlockParser parser)
{
base.Close(parser);
if (LastChild != null)
{
Range.End = LastChild.Range.End;
}
else
{
// Empty list item
Range.End.Line = Range.Start.Line;
if (Parent is List list)
{
Range.End.Column = list.MarkerOffset + list.Padding;
}
}
}
internal static BlockStart Start(BlockParser parser, Block container)
{
if ((!parser.Indented || container is List) && TryParseListMarker(parser, container, out var data))
{
parser.CloseUnmatchedBlocks();
var list = container as List ?? (container.Parent is ListItem item ? item.data : null);
// add the list if needed
if (parser.Tip is not List || !ListsMatch(list, data))
{
parser.AddChild(data, parser.NextNonSpace);
}
var node = parser.AddChild<ListItem>(parser.NextNonSpace);
node.data = data;
return BlockStart.Container;
}
return BlockStart.Skip;
}
private List data = null!;
private static readonly Regex UnorderedMarkerRegex = new(@"^[*+-]");
private static readonly Regex OrderedMarkerRegex = new(@"^(\d{1,9})([.)])");
private static bool TryParseListMarker(BlockParser parser, Block container, out List data)
{
data = null!;
if (parser.Indent >= 4)
{
return false;
}
var rest = parser.CurrentLine[parser.NextNonSpace..];
var match = UnorderedMarkerRegex.Match(rest);
if (match.Success)
{
data = new UnorderedList
{
Marker = match.Value[0],
MarkerOffset = parser.Indent
};
}
else
{
match = OrderedMarkerRegex.Match(rest);
if (match.Success && (container is not Paragraph || match.Groups[1].Value == "1"))
{
var list = new OrderedList
{
MarkerOffset = parser.Indent,
Start = int.Parse(match.Groups[1].Value),
Delimiter = match.Groups[2].Value
};
data = list;
}
else
{
return false;
}
}
// make sure we have spaces after
var ch = parser.PeekNonSpace(match.Length);
if (ch != default && !ch.IsSpaceOrTab())
{
return false;
}
// if it interrupts paragraph, make sure first line isn't blank
if (container is Paragraph && string.IsNullOrWhiteSpace(parser.CurrentLine[(parser.NextNonSpace + match.Length)..]))
{
return false;
}
// we've got a match! advance offset and calculate padding
parser.AdvanceNextNonSpace(); // to start of marker
parser.AdvanceOffset(match.Length, true); // to end of marker
var startColumn = parser.Column;
var startOffset = parser.Offset;
do
{
parser.AdvanceOffset(1, true);
ch = parser.Peek();
} while (parser.Column - startColumn < 5 && ch.IsSpaceOrTab());
var blank = parser.Peek() == default;
var spacesAfterMarker = parser.Column - startColumn;
if (spacesAfterMarker >= 5 || spacesAfterMarker < 1 || blank)
{
data.Padding = match.Length + 1;
parser.Column = startColumn;
parser.Offset = startOffset;
if (parser.Peek().IsSpaceOrTab())
{
parser.AdvanceOffset(1, true);
}
}
else
{
data.Padding = match.Length + spacesAfterMarker;
}
return true;
}
private static bool ListsMatch(List? x, List y)
{
if (x == null)
{
return false;
}
return x.GetType() == y.GetType() && x.Marker == y.Marker && x.Delimiter == y.Delimiter;
}
}

View File

@@ -0,0 +1,17 @@
namespace Radzen.Blazor.Markdown;
/// <summary>
/// Parses a Markdown document.
/// </summary>
public static class MarkdownParser
{
/// <summary>
/// Parses a string containing Markdown into a document.
/// </summary>
/// <param name="markdown">The Markdown content to parse.</param>
/// <returns>The parsed document.</returns>
public static Document Parse(string markdown)
{
return BlockParser.Parse(markdown);
}
}

View File

@@ -0,0 +1,159 @@
using System.Collections.Generic;
namespace Radzen.Blazor.Markdown;
/// <summary>
/// Base class for visitors that traverse a Markdown document.
/// </summary>
public abstract class NodeVisitorBase : INodeVisitor
{
/// <summary>
/// Visits a block quote by visiting its children.
/// </summary>
public virtual void VisitBlockQuote(BlockQuote blockQuote) => VisitChildren(blockQuote.Children);
/// <summary>
/// Visits a document by visiting its children.
/// </summary>
public virtual void VisitDocument(Document document) => VisitChildren(document.Children);
/// <summary>
/// Visits a heading by visiting its children.
/// </summary>
public virtual void VisitHeading(Heading heading) => VisitChildren(heading.Children);
/// <summary>
/// Visits a list item by visiting its children.
/// </summary>
public virtual void VisitListItem(ListItem listItem) => VisitChildren(listItem.Children);
/// <summary>
/// Visits an ordered list by visiting its children.
/// </summary>
public virtual void VisitOrderedList(OrderedList orderedList) => VisitChildren(orderedList.Children);
/// <summary>
/// Visits a paragraph by visiting its children.
/// </summary>
public virtual void VisitParagraph(Paragraph paragraph) => VisitChildren(paragraph.Children);
/// <summary>
/// Visits a thematic break.
/// </summary>
public virtual void VisitThematicBreak(ThematicBreak thematicBreak)
{
}
/// <summary>
/// Visits a text node.
/// </summary>
public virtual void VisitText(Text text)
{
}
/// <summary>
/// Visits a code node.
/// </summary>
public virtual void VisitCode(Code code)
{
}
/// <summary>
/// Visits an HTML block.
/// </summary>
public virtual void VisitHtmlInline(HtmlInline html)
{
}
/// <summary>
/// Visits a line break.
/// </summary>
public virtual void VisitLineBreak(LineBreak lineBreak)
{
}
/// <summary>
/// Visits a soft line break.
/// </summary>
public virtual void VisitSoftLineBreak(SoftLineBreak softLineBreak)
{
}
/// <summary>
/// Visits an ordered list by visiting its children.
/// </summary>
public virtual void VisitUnorderedList(UnorderedList unorderedList) => VisitChildren(unorderedList.Children);
/// <summary>
/// Visits an emphasis by visiting its children.
/// </summary>
public virtual void VisitEmphasis(Emphasis emphasis) => VisitChildren(emphasis.Children);
/// <summary>
/// Visits a strong by visiting its children.
/// </summary>
public virtual void VisitStrong(Strong strong) => VisitChildren(strong.Children);
/// <summary>
/// Visits a link by visiting its children.
/// </summary>
public virtual void VisitLink(Link link) => VisitChildren(link.Children);
/// <summary>
/// Visits an image by visiting its children.
/// </summary>
public virtual void VisitImage(Image image) => VisitChildren(image.Children);
/// <summary>
/// Visits a code block.
/// </summary>
public virtual void VisitIndentedCodeBlock(IndentedCodeBlock codeBlock)
{
}
/// <summary>
/// Visits a fenced code block.
/// </summary>
public virtual void VisitFencedCodeBlock(FencedCodeBlock fencedCodeBlock)
{
}
/// <summary>
/// Visits an HTML block.
/// </summary>
public virtual void VisitHtmlBlock(HtmlBlock htmlBlock)
{
}
/// <summary>
/// Visits a table.
/// </summary>
public virtual void VisitTable(Table table) => VisitChildren(table.Rows);
/// <summary>
/// Visits a table header row by visiting its children.
/// </summary>
public virtual void VisitTableHeaderRow(TableHeaderRow header) => VisitChildren(header.Cells);
/// <summary>
/// Visits a table row by visiting its children.
/// </summary>
public virtual void VisitTableRow(TableRow row) => VisitChildren(row.Cells);
/// <summary>
/// Visits a table cell by visiting its children.
/// </summary>
public virtual void VisitTableCell(TableCell cell) => VisitChildren(cell.Children);
/// <summary>
/// Visits a collection of nodes.
/// </summary>
protected void VisitChildren(IEnumerable<INode> children)
{
foreach (var node in children)
{
node.Accept(this);
}
}
}

View File

@@ -0,0 +1,18 @@
namespace Radzen.Blazor.Markdown;
/// <summary>
/// Represents an ordered list: <c>1. item</c>.
/// </summary>
public class OrderedList : List
{
/// <inheritdoc />
public override void Accept(INodeVisitor visitor)
{
visitor.VisitOrderedList(this);
}
/// <summary>
/// Gets or sets the start number of the ordered list.
/// </summary>
public int Start { get; set; }
}

View File

@@ -0,0 +1,19 @@
namespace Radzen.Blazor.Markdown;
/// <summary>
/// Represents a paragraph node.
/// </summary>
public class Paragraph : Leaf
{
/// <inheritdoc />
public override void Accept(INodeVisitor visitor)
{
visitor.VisitParagraph(this);
}
internal override BlockMatch Matches(BlockParser parser)
{
return parser.Blank ? BlockMatch.Skip : BlockMatch.Match;
}
}

View File

@@ -0,0 +1,50 @@
using System.Text.RegularExpressions;
namespace Radzen.Blazor.Markdown;
/// <summary>
/// Represents a setext heading node. Setext headings are headings that are underlined with equal signs for level 1 headings and dashes for level 2 headings.
/// </summary>
public class SetExtHeading : Heading
{
private static readonly Regex HeadingRegex = new (@"^(?:=+|-+)[ \t]*$");
internal static BlockStart Start(BlockParser parser, Block block)
{
if (parser.Indented || block is not Paragraph paragraph)
{
return BlockStart.Skip;
}
var line = parser.CurrentLine[parser.NextNonSpace..];
var match = HeadingRegex.Match(line);
if (match.Success)
{
parser.CloseUnmatchedBlocks();
// resolve reference links
while (paragraph.Value.Peek() == '[' && parser.TryParseLinkReference(paragraph.Value, out var position))
{
paragraph.Value = paragraph.Value[position..];
}
if (paragraph.Value.Length > 0)
{
var heading = new SetExtHeading
{
Level = match.Value[0] == '=' ? 1 : 2,
Value = paragraph.Value
};
paragraph.Parent.Replace(paragraph, heading);
parser.Tip = heading;
parser.AdvanceOffset(parser.CurrentLine.Length - parser.Offset, false);
return BlockStart.Leaf;
}
}
return BlockStart.Skip;
}
}

View File

@@ -0,0 +1,13 @@
namespace Radzen.Blazor.Markdown;
/// <summary>
/// Represents a soft line break node. Soft line breaks are usually used to separate lines in a paragraph.
/// </summary>
public class SoftLineBreak : Inline
{
/// <inheritdoc />
public override void Accept(INodeVisitor visitor)
{
visitor.VisitSoftLineBreak(this);
}
}

View File

@@ -0,0 +1,9 @@
namespace Radzen.Blazor.Markdown;
static class StringExtensions
{
public static char Peek(this string line, int offset = 0)
{
return offset < line.Length ? line[offset] : default;
}
}

View File

@@ -0,0 +1,13 @@
namespace Radzen.Blazor.Markdown;
/// <summary>
/// Represents a strong node: <c>**strong**</c>.
/// </summary>
public class Strong : InlineContainer
{
/// <inheritdoc />
public override void Accept(INodeVisitor visitor)
{
visitor.VisitStrong(this);
}
}

View File

@@ -0,0 +1,348 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace Radzen.Blazor.Markdown;
/// <summary>
/// The alignment of a table cell.
/// </summary>
public enum TableCellAlignment
{
/// <summary>
/// No alignment specified. Default alignment is left.
/// </summary>
None,
/// <summary>
/// Left alignment.
/// </summary>
Left,
/// <summary>
/// Center alignment.
/// </summary>
Center,
/// <summary>
/// Right alignment.
/// </summary>
Right
}
/// <summary>
/// Represents a table in a Markdown document.
/// </summary>
public class Table : Leaf
{
private readonly List<TableRow> rows = [];
/// <summary>
/// Gets the rows of the table.
/// </summary>
public IReadOnlyList<TableRow> Rows => rows;
/// <inheritdoc />
public override void Accept(INodeVisitor visitor)
{
visitor.VisitTable(this);
}
private static readonly Regex DelimiterRegex = new(@"^\s*(\|?\s*:?-{1,}:?\s*)+(\|+\s*:?-{1,}:?\s*)*\|?\s*$");
internal override void Close(BlockParser parser)
{
base.Close(parser);
var header = rows[0];
var headerCells = header.Cells;
var dataLines = Value.Split('\n');
foreach (var line in dataLines)
{
if (!string.IsNullOrWhiteSpace(line))
{
var row = new TableRow();
var cells = ParseRow(line);
// Trim excess cells
var count = Math.Min(cells.Count, headerCells.Count);
for (int cellIndex = 0; cellIndex < count; cellIndex++)
{
var alignment = cellIndex < headerCells.Count ? headerCells[cellIndex].Alignment : TableCellAlignment.None;
row.Add(cells[cellIndex], alignment);
}
for (int missingCellIndex = 0; missingCellIndex < header.Cells.Count - cells.Count; missingCellIndex++)
{
row.Add("", TableCellAlignment.None);
}
rows.Add(row);
}
}
}
private static List<string> ParseRow(string line)
{
// Remove leading and trailing pipes if present
line = line.Trim();
if (line.StartsWith('|'))
{
line = line[1..];
}
if (line.EndsWith('|'))
{
line = line[..^1];
}
// Split by pipe character, but not by escaped pipes
var cells = new List<string>();
var currentCell = "";
var escaped = false;
for (int i = 0; i < line.Length; i++)
{
var c = line[i];
if (escaped)
{
// Add the escaped character (including escaped pipes)
if (c == '|')
{
currentCell += '|'; // Replace \| with |
}
else
{
currentCell += $"\\{c}"; // Keep the escape character for other escaped chars
}
escaped = false;
}
else if (c == '\\')
{
escaped = true;
}
else if (c == '|')
{
// End of cell
cells.Add(currentCell.Trim());
currentCell = "";
}
else
{
currentCell += c;
}
}
// Add the last cell
if (!string.IsNullOrEmpty(currentCell) || cells.Count > 0)
{
cells.Add(currentCell.Trim());
}
return cells;
}
internal override BlockMatch Matches(BlockParser parser)
{
return parser.Blank ? BlockMatch.Skip : BlockMatch.Match;
}
internal static BlockStart Start(BlockParser parser, Block block)
{
if (parser.Indented || block is not Paragraph paragraph)
{
return BlockStart.Skip;
}
var line = parser.CurrentLine[parser.NextNonSpace..];
// Check if the line contains a pipe character to be more specific about table delimiters
// This helps avoid misinterpreting heading delimiters as table delimiters
if (!line.Contains('|') && !line.Contains(':'))
{
return BlockStart.Skip;
}
var match = DelimiterRegex.Match(line);
if (match.Success)
{
// Parse the delimiter row to determine alignments
var delimiterRow = line.Trim();
// Parse the header row from the paragraph text
var headerLine = paragraph.Value.Trim();
// Parse header cells and delimiter cells
var headerCells = ParseRow(headerLine);
// Parse delimiter cells to determine alignments
var cleanDelimiterRow = delimiterRow;
if (cleanDelimiterRow.StartsWith('|'))
{
cleanDelimiterRow = cleanDelimiterRow[1..];
}
if (cleanDelimiterRow.EndsWith('|'))
{
cleanDelimiterRow = cleanDelimiterRow[..^1];
}
// Split by pipe character
var delimiters = cleanDelimiterRow.Split('|');
// If the number of header cells and delimiter cells don't match, don't create a table
if (headerCells.Count != delimiters.Length)
{
return BlockStart.Skip;
}
// Initialize alignments list
var alignments = new List<TableCellAlignment>(headerCells.Count);
for (int i = 0; i < headerCells.Count; i++)
{
alignments.Add(TableCellAlignment.None);
}
// Determine alignments from delimiters
for (var i = 0; i < delimiters.Length && i < alignments.Count; i++)
{
var trimmed = delimiters[i].Trim();
if (trimmed.StartsWith(':') && trimmed.EndsWith(':'))
{
alignments[i] = TableCellAlignment.Center;
}
else if (trimmed.EndsWith(':'))
{
alignments[i] = TableCellAlignment.Right;
}
else if (trimmed.StartsWith(':'))
{
alignments[i] = TableCellAlignment.Left;
}
}
parser.CloseUnmatchedBlocks();
// resolve reference links
while (paragraph.Value.Peek() == '[' && parser.TryParseLinkReference(paragraph.Value, out var position))
{
paragraph.Value = paragraph.Value[position..];
}
if (paragraph.Value.Length > 0)
{
var table = new Table();
// Create header row
var header = new TableHeaderRow();
table.rows.Add(header);
// Add header cells with alignments
for (int cellindex = 0; cellindex < headerCells.Count; cellindex++)
{
var alignment = cellindex < alignments.Count ? alignments[cellindex] : TableCellAlignment.None;
header.Add(headerCells[cellindex], alignment);
}
paragraph.Parent.Replace(paragraph, table);
parser.Tip = table;
parser.AdvanceOffset(parser.CurrentLine.Length - parser.Offset, false);
return BlockStart.Leaf;
}
}
return BlockStart.Skip;
}
}
/// <summary>
/// Represents a table header row in a Markdown table.
/// </summary>
public class TableHeaderRow : TableRow
{
/// <inheritdoc />
public override void Accept(INodeVisitor visitor)
{
visitor.VisitTableHeaderRow(this);
}
}
/// <summary>
/// Represents a table row in a Markdown table.
/// </summary>
public class TableRow : INode
{
private readonly List<TableCell> children = [];
/// <summary>
/// /// Gets the cells of the table row.
/// </summary>
public IReadOnlyList<TableCell> Cells => children;
/// <summary>
/// Adds a cell to the table row.
/// </summary>
public void Add(string value, TableCellAlignment alignment = TableCellAlignment.None)
{
var cell = new TableCell(value, alignment);
children.Add(cell);
}
/// <inheritdoc />
public virtual void Accept(INodeVisitor visitor)
{
visitor.VisitTableRow(this);
}
}
/// <summary>
/// Represents a table cell in a Markdown table.
/// </summary>
public class TableCell : INode, IBlockInlineContainer
{
/// <summary>
/// Gets the alignment of the table cell.
/// </summary>
public TableCellAlignment Alignment { get; }
/// <summary>
/// Initializes a new instance of the <see cref="TableCell"/> class.
/// </summary>
public TableCell(string value, TableCellAlignment alignment = TableCellAlignment.None)
{
Value = value;
Alignment = alignment;
}
/// <summary>
/// Gets or sets the inline content of the cell
/// </summary>
public string Value { get; set; }
/// <inheritdoc />
public void Accept(INodeVisitor visitor)
{
visitor.VisitTableCell(this);
}
private readonly List<Inline> children = [];
/// <summary>
/// Gets the children of the table cell.
/// </summary>
public IReadOnlyList<Inline> Children => children;
/// <summary>
/// Appends a child to table cell.
/// </summary>
public void Add(Inline node)
{
children.Add(node);
}
}

Some files were not shown because too many files have changed in this diff Show More