├── .editorconfig
├── .github
└── workflows
│ └── deploy.yml
├── .gitignore
├── Directory.Build.props
├── LICENSE
├── Mjml.Net.Benchmark
├── Mjml.Net.Benchmark.csproj
├── Program.cs
├── TemplateBenchmarks.cs
├── Templates
│ ├── Amario.mjml
│ ├── Arturia.mjml
│ ├── Austin.mjml
│ ├── BlackFriday.mjml
│ ├── Card.mjml
│ ├── Christmas.mjml
│ ├── HappyNewYear.mjml
│ ├── ManyHeroes.mjml
│ ├── OnePage.mjml
│ ├── Proof.mjml
│ ├── Racoon.mjml
│ ├── Reactivation.mjml
│ ├── RealEstate.mjml
│ ├── Recast.mjml
│ ├── Receipt.mjml
│ ├── Referral.mjml
│ ├── SpheroDroids.mjml
│ ├── SpheroMini.mjml
│ ├── UGGRoyale.mjml
│ ├── Welcome.mjml
│ └── Worldly.mjml
└── TestRunner.cs
├── Mjml.Net.Generator
├── BindGenerator.cs
├── Constants.cs
├── FieldSource.cs
├── IsExternalInit.cs
├── Mjml.Net.Generator.csproj
├── Template.handlebar
├── TemplateField.cs
├── TemplateModel.cs
└── icon.png
├── Mjml.Net.PostProcessors
├── AngleSharpExtensions.cs
├── AngleSharpPostProcessor.cs
├── AttributesPostProcessor.cs
├── Components
│ ├── AttributeSelectorComponent.cs
│ ├── HtmlAttributeComponent.cs
│ └── HtmlAttributesComponent.cs
├── Declarations
│ ├── FallbackConverter.cs
│ ├── FallbackCssValue.cs
│ ├── FallbackCssValueConverter.cs
│ ├── FallbackDeclarationFactory.cs
│ └── ValueConverterExtensions.cs
├── IAngleSharpPostProcessor.cs
├── INestingPostProcessor.cs
├── InlineCssPostProcessor.cs
├── Mjml.Net.PostProcessors.csproj
├── PostProcessorExtensions.cs
└── icon.png
├── Mjml.Net.sln
├── Mjml.Net
├── AllowedAttributes.cs
├── AllowedParents.cs
├── AttributeTypes.cs
├── Attributes.cs
├── BindAttribute.cs
├── BindTextAttribute.cs
├── BindType.cs
├── BindingHelper.cs
├── Component.cs
├── Components
│ ├── Body
│ │ ├── AccordionComponent.cs
│ │ ├── AccordionElementComponent.cs
│ │ ├── AccordionTextComponent.cs
│ │ ├── AccordionTitleComponent.cs
│ │ ├── BodyComponent.cs
│ │ ├── BodyComponentBase.cs
│ │ ├── ButtonComponent.cs
│ │ ├── CarouselComponent.cs
│ │ ├── CarouselImageComponent.cs
│ │ ├── ColumnComponent.cs
│ │ ├── DividerComponent.cs
│ │ ├── GroupComponent.cs
│ │ ├── HeroComponent.cs
│ │ ├── ImageComponent.cs
│ │ ├── MsoButtonComponent.cs
│ │ ├── NavbarComponent.cs
│ │ ├── NavbarLinkComponent.cs
│ │ ├── RawComponent.cs
│ │ ├── SectionComponent.cs
│ │ ├── SocialComponent.cs
│ │ ├── SocialElementComponent.cs
│ │ ├── SocialNetwork.cs
│ │ ├── SpacerComponent.cs
│ │ ├── TableComponent.cs
│ │ ├── TextComponent.cs
│ │ └── WrapperComponent.cs
│ ├── CommentComponent.cs
│ ├── Extensions
│ │ └── List
│ │ │ ├── ListComponent.cs
│ │ │ ├── ListExtensions.cs
│ │ │ └── ListItemComponent.cs
│ ├── Head
│ │ ├── AttributesComponent.cs
│ │ ├── BreakpointComponent.cs
│ │ ├── FontComponent.cs
│ │ ├── HeadComponent.cs
│ │ ├── HeadComponentBase.cs
│ │ ├── PreviewComponent.cs
│ │ ├── StyleComponent.cs
│ │ └── TitleComponent.cs
│ ├── IncludeComponent.cs
│ ├── IncludeType.cs
│ ├── RootComponent.cs
│ └── RootData.cs
├── ContentType.cs
├── DefaultIDGenerator.cs
├── DefaultPools.cs
├── Extensions
│ ├── StringExtensions.cs
│ └── WriterExtensions.cs
├── GlobalContext.cs
├── GlobalData.cs
├── HelperTarget.cs
├── Helpers
│ ├── Font.cs
│ ├── Preview.cs
│ ├── Style.cs
│ └── Title.cs
├── HtmlError.cs
├── IBinder.cs
├── IComponent.cs
├── IFileLoader.cs
├── IHelper.cs
├── IHtmlAttrRenderer.cs
├── IHtmlClassRenderer.cs
├── IHtmlReader.cs
├── IHtmlRenderer.cs
├── IHtmlStyleRenderer.cs
├── IIdGenerator.cs
├── IMjmlReader.cs
├── IMjmlRenderer.cs
├── IPostProcessor.cs
├── IType.cs
├── IValidator.cs
├── InMemoryFileLoader.cs
├── InnerTextOrHtml.cs
├── Internal
│ ├── Binder.cs
│ ├── Constants.cs
│ ├── HtmlReaderWrapper.cs
│ ├── ReflectionHelper.cs
│ ├── RenderStack.cs
│ └── SubtreeReader.cs
├── Mjml.Net.csproj
├── MjmlOptions.cs
├── MjmlRenderContext.Rendering.cs
├── MjmlRenderContext.cs
├── MjmlRenderer.cs
├── Properties
│ ├── Assembly.cs
│ ├── Resources.Designer.cs
│ └── Resources.resx
├── RenderBuffer.cs
├── RenderResult.cs
├── SourcePosition.cs
├── Types
│ ├── ColorType.cs
│ ├── EnumType.cs
│ ├── ManyType.cs
│ ├── NumberType.cs
│ ├── OneOfType.cs
│ └── StringType.cs
├── Unit.cs
├── UnitParser.cs
├── ValidationContext.cs
├── ValidationErrors.cs
├── Validators
│ ├── SoftValidator.cs
│ ├── StrictValidator.cs
│ └── ValidatorBase.cs
└── icon.png
├── README.md
├── Tests
├── BugReportTests.cs
├── ComplexTests.cs
├── Components
│ ├── AccordionTests.cs
│ ├── AttributesTests.cs
│ ├── BodyTests.cs
│ ├── ButtonTests.cs
│ ├── CarouselTests.cs
│ ├── ColumnTests.cs
│ ├── CommentTests.cs
│ ├── DividerTests.cs
│ ├── FontTests.cs
│ ├── GroupTests.cs
│ ├── HeroTests.cs
│ ├── HtmlAttributesTests.cs
│ ├── ImageTests.cs
│ ├── IncludeTests.cs
│ ├── ListTests.cs
│ ├── MsoButtonTests.cs
│ ├── NavbarTests.cs
│ ├── Outputs
│ │ ├── Accordion.html
│ │ ├── AccordionEmptyElements.html
│ │ ├── Breakpoint.html
│ │ ├── Button.html
│ │ ├── ButtonLink.html
│ │ ├── ButtonLinkWithRel.html
│ │ ├── ButtonMixedContent.html
│ │ ├── ButtonMixedContent2.html
│ │ ├── ButtonWithoutWidthUnit.html
│ │ ├── Carousel.html
│ │ ├── CarouselHeadStyles.html
│ │ ├── CarouselIconWidth.html
│ │ ├── CarouselImageWithHref.html
│ │ ├── CarouselImagesFive.html
│ │ ├── CarouselImagesOne.html
│ │ ├── CarouselImagesTwo.html
│ │ ├── CarouselThumbnailWidth.html
│ │ ├── ChildClasses.html
│ │ ├── ColumnClass.html
│ │ ├── ColumnFour.html
│ │ ├── ColumnOne.html
│ │ ├── ColumnOneWithInnerBorder.html
│ │ ├── ColumnOneWithPadding.html
│ │ ├── ColumnThree.html
│ │ ├── ColumnTwo.html
│ │ ├── Comments.html
│ │ ├── Divider.html
│ │ ├── DividerWithoutWidthUnit.html
│ │ ├── Font.html
│ │ ├── FontUbuntu.html
│ │ ├── FontUbuntu2.html
│ │ ├── Group.html
│ │ ├── GroupWithColumns.html
│ │ ├── Hero.html
│ │ ├── HeroDivider.html
│ │ ├── HeroDividers.html
│ │ ├── HtmlAttributeInvalid.html
│ │ ├── HtmlAttributes.html
│ │ ├── HtmlAttributesNoProcessor.html
│ │ ├── Image.html
│ │ ├── ImageWithLink.html
│ │ ├── List.html
│ │ ├── MsoButton.html
│ │ ├── MsoButtonWithBorder.html
│ │ ├── Navbar.html
│ │ ├── NavbarWithLinks.html
│ │ ├── NavbarWithoutHamburger.html
│ │ ├── Preview.html
│ │ ├── Section.html
│ │ ├── SectionWithBackgroundColor.html
│ │ ├── SectionWithBackgroundImage.html
│ │ ├── SectionWithColumns.html
│ │ ├── SectionWithGroups.html
│ │ ├── Social.html
│ │ ├── SocialEmpty.html
│ │ ├── SocialRaw.html
│ │ ├── Spacer.html
│ │ ├── SpacerWithHeight.html
│ │ ├── Style.html
│ │ ├── StyleInclude.html
│ │ ├── StyleInline.html
│ │ ├── StyleInline2.html
│ │ ├── StyleInline3.html
│ │ ├── StyleInline4.html
│ │ ├── StyleInlineFallback.html
│ │ ├── Table.html
│ │ ├── TablePercent.html
│ │ ├── TablePixels.html
│ │ ├── Text.html
│ │ ├── TextInclude.html
│ │ ├── TextRawWhitespace.html
│ │ ├── TextWhitespace.html
│ │ ├── TextWithEntity.html
│ │ ├── TextWithHtml.html
│ │ ├── TextWithHtml2.html
│ │ ├── TextWithHtmlAndWhitespace.html
│ │ ├── Title.html
│ │ └── Wrapper.html
│ ├── PreviewTests.cs
│ ├── RawTests.cs
│ ├── SectionTests.cs
│ ├── SocialTests.cs
│ ├── SpacerTests.cs
│ ├── StyleTests.cs
│ ├── TableTests.cs
│ ├── TextTests.cs
│ ├── TitleTests.cs
│ └── WrapperTests.cs
├── HtmlExtensionsTests.cs
├── HtmlReaderTests.cs
├── HtmlRenderTests.cs
├── HtmlSpecialCaseTests.cs
├── IncludeTests.cs
├── InnerTextOrHtmlTests.cs
├── Internal
│ ├── AssertHelpers.cs
│ ├── CustomFilters.cs
│ ├── StaticIdGenerator.cs
│ ├── TestComponent.cs
│ └── TestHelper.cs
├── Templates
│ ├── amario.mjml
│ ├── arturia.mjml
│ ├── austin.mjml
│ ├── basic.mjml
│ ├── black-friday.mjml
│ ├── bug.mjml
│ ├── card.mjml
│ ├── christmas.mjml
│ ├── happy-new-year.mjml
│ ├── include
│ │ ├── about.mjml
│ │ ├── footer.mjml
│ │ ├── header.mjml
│ │ └── styling.mjml
│ ├── newsletter.mjml
│ ├── onepage.mjml
│ ├── proof.mjml
│ ├── racoon.mjml
│ ├── reactivation-email.mjml
│ ├── real-estate.mjml
│ ├── recast.mjml
│ ├── receipt-email.mjml
│ ├── referral-email.mjml
│ ├── sphero-droids.mjml
│ ├── sphero-mini.mjml
│ ├── ticketshop.mjml
│ ├── welcome-email.mjml
│ └── worldly.mjml
├── Tests.csproj
├── Types
│ ├── ColorTypeTests.cs
│ ├── EnumTypeTests.cs
│ ├── ManyTypeTests.cs
│ ├── NumberTypeTests.cs
│ └── UnitParserTests.cs
└── ValidationTests.cs
├── Tools
├── ConvertJS.cs
├── MigrateCS.cs
├── Program.cs
└── Tools.csproj
└── stylecop.json
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Build and Push to Nuget
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 |
9 | steps:
10 | - uses: actions/checkout@v3
11 |
12 | - name: setup dotnet
13 | uses: actions/setup-dotnet@v3
14 | with:
15 | dotnet-version: 9.0.x
16 |
17 | - name: setup node
18 | uses: actions/setup-node@v3
19 | with:
20 | node-version: '14'
21 |
22 | - name: install mjml
23 | run: |
24 | npm i mjml -g
25 |
26 | - name: test
27 | run: |
28 | dotnet test --filter Category!=Dependencies
29 |
30 | - name: pack
31 | run: |
32 | cd Mjml.Net & dotnet pack -c Release
33 |
34 | - name: publish
35 | if: github.event_name != 'pull_request' && github.ref_name == 'main'
36 | run: |
37 | dotnet nuget push **/*.nupkg --source 'https://api.nuget.org/v3/index.json' --skip-duplicate -k ${{ secrets.nuget }}
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # User-specific files
2 | *.log
3 | *.suo
4 | *.user
5 | *.vs
6 |
7 | .angular
8 | .awCache
9 | .idea
10 | .vscode
11 |
12 | # Build results
13 | bin/
14 | build/
15 | obj/
16 | object/
17 | out/
18 | publish/
19 |
20 | # Test Output
21 | _test-output/
22 |
23 | # NodeJS
24 | node_modules/
25 |
26 | /backend/src/Squidex/Assets
27 |
28 | appsettings.Development.json
29 | appsettings.Production.json
30 | launchSettings.json
31 |
32 | /frontend/app-config/localhost-key.pem
33 | /frontend/app-config/localhost.pem
34 |
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | Liam Riddell, Sebastian Stehle
4 | A blazingly-fast unofficial port of MJML (by MailJet) to .NET 9.
5 | MIT
6 | true
7 | true
8 | icon.png
9 | email,mjml,templates
10 | MIT
11 | https://github.com/SebastianStehle/mjml-net
12 | https://github.com/SebastianStehle/mjml-net.git
13 | git
14 | snupkg
15 | 4.9.0
16 |
17 |
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Sebastian Stehle & LiamRiddell
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Mjml.Net.Benchmark/Program.cs:
--------------------------------------------------------------------------------
1 | using BenchmarkDotNet.Running;
2 | using CommandLine;
3 |
4 | namespace Mjml.Net.Benchmarking;
5 |
6 | public static class Program
7 | {
8 | private sealed class Options
9 | {
10 | [Option('p', "profiler", Required = false, HelpText = "Runs the test runner logic.")]
11 | public bool TestRunner { get; set; }
12 |
13 | [Option('i', "interations", Required = false, HelpText = "The number of iterations when using profiler mode.", Default = 20)]
14 | public int TestRunnerIterations { get; set; }
15 | }
16 |
17 | public static void Main(string[] args)
18 | {
19 | Parser.Default.ParseArguments(args)
20 | .WithParsed(o =>
21 | {
22 | if (o.TestRunner)
23 | {
24 | TestRunner.Run(o.TestRunnerIterations);
25 | } else
26 | {
27 | BenchmarkRunner.Run();
28 | }
29 | });
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Mjml.Net.Benchmark/TemplateBenchmarks.cs:
--------------------------------------------------------------------------------
1 | using BenchmarkDotNet.Attributes;
2 | using BenchmarkDotNet.Configs;
3 | using BenchmarkDotNet.Exporters;
4 | using BenchmarkDotNet.Jobs;
5 |
6 | namespace Mjml.Net.Benchmarking;
7 |
8 | [Config(typeof(Config))]
9 | [MemoryDiagnoser]
10 | public class TemplateBenchmarks
11 | {
12 | private static readonly MjmlOptions WithBeautify = new MjmlOptions { Beautify = true };
13 | private readonly MjmlRenderer MjmlRenderer;
14 |
15 | [ParamsSource(nameof(MjmlTemplates))]
16 | public string MjmlTemplateFilePath { get; set; }
17 |
18 | public static IEnumerable MjmlTemplates => Directory.GetFiles("./Templates/", "*.mjml");
19 |
20 | public string MjmlTemplate { get; set; }
21 |
22 | public class Config : ManualConfig
23 | {
24 | public Config()
25 | {
26 | var baseJob = Job.ShortRun;
27 |
28 | AddJob(baseJob
29 | .WithId("Dev").WithBaseline(true));
30 |
31 | AddJob(baseJob.WithCustomBuildConfiguration("V1_24")
32 | .WithId("1.24.0"));
33 |
34 | AddJob(baseJob.WithCustomBuildConfiguration("V2_0")
35 | .WithId("2.0.0"));
36 |
37 | AddJob(baseJob.WithCustomBuildConfiguration("V2_1")
38 | .WithId("2.1.0"));
39 |
40 | AddJob(baseJob.WithCustomBuildConfiguration("V3_8")
41 | .WithId("3.8.0"));
42 |
43 | AddExporter(MarkdownExporter.GitHub);
44 | }
45 | }
46 |
47 | public TemplateBenchmarks()
48 | {
49 | MjmlRenderer = new();
50 | }
51 |
52 | [GlobalSetup]
53 | public void GlobalSetup()
54 | {
55 | MjmlTemplate = File.ReadAllText(MjmlTemplateFilePath);
56 | }
57 |
58 | [Benchmark()]
59 | public string Render_Template_Beautify()
60 | {
61 | return MjmlRenderer.Render(MjmlTemplate, WithBeautify).Html;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Mjml.Net.Benchmark/Templates/HappyNewYear.mjml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | New dreams, new hopes, new experiences and new joys, we wish you all the best for this New Year to come in 2018!
17 |
18 |
19 |
20 |
21 |
22 | Simply created on Mailjet Passport
23 |
24 |
25 |
26 |
27 | [[DELIVERY_INFO]]
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/Mjml.Net.Benchmark/Templates/Proof.mjml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Article Title
14 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sit amet ipsum consequat.
15 | READ MORE
16 |
17 |
18 |
19 |
20 |
21 | Article Title
22 | Lorem ipsum dolor sit amet, consectetur adipiscing elit.
23 | READ MORE
24 |
25 |
26 |
27 | Article Title
28 | Lorem ipsum dolor sit amet, consectetur adipiscing elit.
29 | READ MORE
30 |
31 |
32 |
33 | Article Title
34 | Lorem ipsum dolor sit amet, consectetur adipiscing elit.
35 | READ MORE
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/Mjml.Net.Benchmark/Templates/Referral.mjml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | a { text-decoration: none!important; color: inherit!important; }
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | Hey {{FirstName}}!
20 | Are you enjoying our weekly newsletter?
Then why not share it with your friends?
21 | You'll get a 15% discount
22 | on your next order when a friend uses the code {{ReferalCode}}!
23 |
24 | Refer a friend now
25 | Best,
The {{CompanyName}} Team
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/Mjml.Net.Benchmark/Templates/Welcome.mjml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Welcome aboard
11 |
12 |
13 |
14 |
15 | Dear [[FirstName]]
Welcome to [[CompanyName]].
16 | We're really excited you've decided to give us a try. In case you have any questions, feel free to reach out to us at [[ContactEmail]]. You can login to your account with your username [[UserName]]
17 | Login
18 | Thanks,
The [[CompanyName]] Team
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/Mjml.Net.Benchmark/TestRunner.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 |
3 | namespace Mjml.Net.Benchmarking;
4 |
5 | public static class TestRunner
6 | {
7 | private static readonly MjmlOptions Options = new MjmlOptions { Beautify = true };
8 |
9 | public static void Run(int numberOfIterations)
10 | {
11 | var mjmlRenderer = new MjmlRenderer();
12 | var mjmlTemplates = Directory.GetFiles("./Templates/", "*.mjml");
13 |
14 | foreach (var mjmlTemplatePath in mjmlTemplates)
15 | {
16 | try
17 | {
18 | var fileName = Path.GetFileName(mjmlTemplatePath);
19 |
20 | Console.WriteLine($"\n=============================");
21 | Console.WriteLine($" {fileName}");
22 | Console.WriteLine($" {mjmlTemplatePath}");
23 | Console.WriteLine($"=============================");
24 |
25 | var input = File.ReadAllText(mjmlTemplatePath);
26 |
27 | for (var i = 0; i < numberOfIterations; i++)
28 | {
29 | Run(input, mjmlRenderer);
30 | }
31 | }
32 | catch (Exception ex)
33 | {
34 | Console.WriteLine(ex.ToString());
35 | }
36 | }
37 | }
38 |
39 | private static void Run( string input, MjmlRenderer mjmlRenderer)
40 | {
41 | var watch = Stopwatch.StartNew();
42 |
43 | var html = mjmlRenderer.Render(input, Options).Html;
44 |
45 | watch.Stop();
46 |
47 | Console.WriteLine("* Elapsed after {0}ms. Length {1}", watch.Elapsed.TotalMilliseconds, html.Length);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Mjml.Net.Generator/Constants.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net.Generator;
2 |
3 | public static class Constants
4 | {
5 | public const string BindAttributeName = "Mjml.Net.BindAttribute";
6 |
7 | public const string BindTextAttributeName = "Mjml.Net.BindTextAttribute";
8 | }
9 |
--------------------------------------------------------------------------------
/Mjml.Net.Generator/FieldSource.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 |
3 | #pragma warning disable RECS0082 // Parameter has the same name as a member and hides it
4 |
5 | namespace Mjml.Net.Generator;
6 |
7 | internal record FieldSource(IFieldSymbol Field, string Value, bool AsText);
8 |
--------------------------------------------------------------------------------
/Mjml.Net.Generator/IsExternalInit.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel;
2 |
3 | namespace System.Runtime.CompilerServices
4 | {
5 | [EditorBrowsable(EditorBrowsableState.Never)]
6 | internal static class IsExternalInit
7 | {
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Mjml.Net.Generator/Mjml.Net.Generator.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | latest
6 | enable
7 | true
8 | enable
9 | latest
10 | true
11 | README.md
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | all
22 | runtime; build; native; contentfiles; analyzers; buildtransitive
23 |
24 |
25 |
26 | all
27 | runtime; build; native; contentfiles; analyzers; buildtransitive
28 |
29 |
30 | all
31 | runtime; build; native; contentfiles; analyzers; buildtransitive
32 |
33 |
34 |
35 |
36 |
37 |
38 | $(GetTargetPathDependsOn);GetDependencyTargetPaths
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | True
58 |
59 |
60 |
61 |
62 | True
63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/Mjml.Net.Generator/Template.handlebar:
--------------------------------------------------------------------------------
1 | #pragma warning disable
2 | // Auto-generated code
3 | using Mjml.Net;
4 |
5 | namespace {{classNamespace}};
6 |
7 | public partial class {{className}}
8 | {
9 | private static readonly AllowedAttributes AllowedAttributesStatic = new AllowedAttributes();
10 | {{#each customTypes}}
11 | private static readonly IType {{customName}} = new {{customType}}();
12 | {{/each}}
13 |
14 | static {{ClassName}}()
15 | {
16 | {{#each defaultTypes}}
17 | AllowedAttributesStatic["{{attribute}}"] = AttributeTypes.{{defaultType}};
18 | {{/each}}
19 | {{#each customTypes}}
20 | AllowedAttributesStatic["{{attribute}}"] = {{customName}};
21 | {{/each}}
22 | }
23 |
24 | public override AllowedAttributes? AllowedFields
25 | {
26 | get
27 | {
28 | var inherited = base.AllowedFields;
29 |
30 | if (inherited == null || inherited.Count == 0)
31 | {
32 | return AllowedAttributesStatic;
33 | }
34 |
35 | var result = new AllowedAttributes(AllowedAttributesStatic);
36 |
37 | foreach (var (key, value) in inherited)
38 | {
39 | result[key] = value;
40 | }
41 |
42 | return result;
43 | }
44 | }
45 |
46 | public override string? GetAttribute(string? name)
47 | {
48 | switch (name)
49 | {
50 | {{#each normalFields}}
51 | case "{{attribute}}":
52 | return {{name}};
53 | {{/each}}
54 | }
55 |
56 | return Binder.GetAttribute(name);
57 | }
58 |
59 | public override void Bind(Mjml.Net.GlobalContext context)
60 | {
61 | {{#each normalFields}}
62 | var source{{name}} = Binder.GetAttribute("{{attribute}}");
63 | if (source{{name}} != null)
64 | {
65 | {{#if isCustom }}
66 | this.{{name}} = {{customName}}.Coerce(source{{name}});
67 | {{/if}}
68 | {{#unless isCustom }}
69 | this.{{name}} = AttributeTypes.{{defaultType}}.Coerce(source{{name}});
70 | {{/unless}}
71 | }
72 | {{/each}}
73 |
74 | {{#each expandedFields}}
75 | if ({{name}} != null && ({{name}}Top == null || {{name}}Right == null || {{name}}Bottom == null || {{name}}Left == null))
76 | {
77 | {{#if isBorder }}
78 | var (t, r, b, l) = BindingHelper.ParseShorthandBorder({{name}});
79 | {{/if}}
80 | {{#unless isBorder }}
81 | var (t, r, b, l) = BindingHelper.ParseShorthandValue({{name}});
82 | {{/unless}}
83 |
84 | if ({{name}}Top == null)
85 | {
86 | {{name}}Top = t;
87 | }
88 |
89 | if ({{name}}Right == null)
90 | {
91 | {{name}}Right = r;
92 | }
93 |
94 | if ({{name}}Bottom == null)
95 | {
96 | {{name}}Bottom = b;
97 | }
98 |
99 | if ({{name}}Left == null)
100 | {
101 | {{name}}Left = l;
102 | }
103 | }
104 | {{/each}}
105 | {{#each textFields}}
106 | {{name}} = Binder.GetText();
107 | {{/each}}
108 |
109 | base.Bind(context);
110 | }
111 | }
--------------------------------------------------------------------------------
/Mjml.Net.Generator/TemplateField.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net.Generator;
2 |
3 | internal sealed record TemplateField
4 | {
5 | public string Name { get; set; }
6 |
7 | public string Attribute { get; set; }
8 |
9 | public string DefaultValue { get; set; }
10 |
11 | public string DefaultType { get; set; }
12 |
13 | public string CustomType { get; set; }
14 |
15 | public string CustomName { get; set; }
16 |
17 | public bool IsText { get; set; }
18 |
19 | public bool IsBorder => Attribute is "border" or "inner-border";
20 |
21 | public bool IsCustom => CustomType != null;
22 | }
23 |
--------------------------------------------------------------------------------
/Mjml.Net.Generator/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SebastianStehle/mjml-net/d04bfff1eeb72479553a3f949f9169ce0a8dcd63/Mjml.Net.Generator/icon.png
--------------------------------------------------------------------------------
/Mjml.Net.PostProcessors/AngleSharpExtensions.cs:
--------------------------------------------------------------------------------
1 | using AngleSharp.Dom;
2 |
3 | namespace Mjml.Net;
4 |
5 | public static class AngleSharpExtensions
6 | {
7 | public static void Traverse(this INode node, Action action)
8 | {
9 | foreach (var child in node.ChildNodes.ToList())
10 | {
11 | Traverse(child, action);
12 | }
13 |
14 | if (node is IElement element)
15 | {
16 | action(element);
17 | }
18 | }
19 |
20 | public static IEnumerable Children(this IElement node, string tagName)
21 | {
22 | return node.Children.Where(x => string.Equals(x.NodeName, tagName, StringComparison.OrdinalIgnoreCase));
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Mjml.Net.PostProcessors/AngleSharpPostProcessor.cs:
--------------------------------------------------------------------------------
1 | using AngleSharp;
2 | using AngleSharp.Css;
3 | using AngleSharp.Css.Parser;
4 | using AngleSharp.Dom;
5 | using Mjml.Net.Declarations;
6 |
7 | namespace Mjml.Net;
8 |
9 | public sealed class AngleSharpPostProcessor : IPostProcessor, INestingPostProcessor
10 | {
11 | private static readonly IConfiguration HtmlConfiguration =
12 | Configuration.Default
13 | .WithCss(new CssParserOptions
14 | {
15 | IsIncludingUnknownDeclarations = true,
16 | IsIncludingUnknownRules = true
17 | })
18 | .WithRenderDevice(new DefaultRenderDevice { FontSize = -1 })
19 | .Without()
20 | .Without()
21 | .With(_ => new FallbackDeclarationFactory());
22 |
23 | public static readonly IPostProcessor Default = new AngleSharpPostProcessor(new InlineCssPostProcessor(), new AttributesPostProcessor());
24 |
25 | private readonly IAngleSharpPostProcessor[] inner;
26 |
27 | public bool Has()
28 | {
29 | return inner.Any(x => x is T);
30 | }
31 |
32 | public AngleSharpPostProcessor(params IAngleSharpPostProcessor[] inner)
33 | {
34 | this.inner = inner ?? throw new ArgumentNullException(nameof(inner));
35 | }
36 |
37 | public async ValueTask PostProcessAsync(string html, MjmlOptions options,
38 | CancellationToken ct)
39 | {
40 | var document = await ParseAsync(html, ct);
41 |
42 | foreach (var processor in inner)
43 | {
44 | await processor.ProcessAsync(document, options, ct);
45 | }
46 |
47 | var result = document.ToHtml();
48 |
49 | return result;
50 | }
51 |
52 | private static async Task ParseAsync(string html, CancellationToken ct)
53 | {
54 | var context = BrowsingContext.New(HtmlConfiguration);
55 |
56 | return await context.OpenAsync(req => req.Content(html), ct);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Mjml.Net.PostProcessors/AttributesPostProcessor.cs:
--------------------------------------------------------------------------------
1 | using AngleSharp.Dom;
2 |
3 | namespace Mjml.Net;
4 |
5 | public sealed class AttributesPostProcessor : IAngleSharpPostProcessor
6 | {
7 | public static readonly IPostProcessor Instance = new AngleSharpPostProcessor(new AttributesPostProcessor());
8 |
9 | public ValueTask ProcessAsync(IDocument document, MjmlOptions options, CancellationToken ct)
10 | {
11 | foreach (var attributes in document.QuerySelectorAll("mj-html-attributes"))
12 | {
13 | foreach (var selector in attributes.Children("mj-selector"))
14 | {
15 | var path = selector.GetAttribute("path");
16 |
17 | if (string.IsNullOrEmpty(path))
18 | {
19 | continue;
20 | }
21 |
22 | var attributeValues = selector.Children("mj-html-attribute")
23 | .Select(x =>
24 | {
25 | var attributeName = x.GetAttribute("name")!;
26 | var attributeValue = x.TextContent;
27 |
28 | return (Name: attributeName, Value: attributeValue);
29 | })
30 | .Where(x => !string.IsNullOrWhiteSpace(x.Name))
31 | .ToList();
32 |
33 | foreach (var target in document.QuerySelectorAll(path))
34 | {
35 | foreach (var (name, value) in attributeValues)
36 | {
37 | target.SetAttribute(name, value?.Trim());
38 | }
39 | }
40 | }
41 | }
42 |
43 | RemoveAll(document, "mj-html-attributes");
44 | RemoveAll(document, "mj-html-attribute");
45 | RemoveAll(document, "mj-selector");
46 |
47 | return default;
48 | }
49 |
50 | private static void RemoveAll(IDocument document, string selector)
51 | {
52 | foreach (var element in document.QuerySelectorAll(selector))
53 | {
54 | element.Remove();
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Mjml.Net.PostProcessors/Components/AttributeSelectorComponent.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net;
2 |
3 | public partial class SelectorComponent : Component
4 | {
5 | private static readonly AllowedParents Parents =
6 | [
7 | "mj-html-attributes"
8 | ];
9 |
10 | public override AllowedParents? AllowedParents => Parents;
11 |
12 | public override ContentType ContentType => ContentType.Complex;
13 |
14 | public override string ComponentName => "mj-selector";
15 |
16 | [Bind("path", BindType.RequiredString)]
17 | public string Path;
18 |
19 | public override void Render(IHtmlRenderer renderer, GlobalContext context)
20 | {
21 | if (!context.Async || !context.Options.HasProcessor())
22 | {
23 | return;
24 | }
25 |
26 | renderer.StartElement(ComponentName).Attr("path", Path);
27 | RenderChildren(renderer, context);
28 | renderer.EndElement(ComponentName);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Mjml.Net.PostProcessors/Components/HtmlAttributeComponent.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net;
2 |
3 | public partial class HtmlAttributeComponent : Component
4 | {
5 | private static readonly AllowedParents Parents =
6 | [
7 | "mj-selector"
8 | ];
9 |
10 | public override AllowedParents? AllowedParents => Parents;
11 |
12 | public override ContentType ContentType => ContentType.Text;
13 |
14 | public override string ComponentName => "mj-html-attribute";
15 |
16 | [Bind("name", BindType.RequiredString)]
17 | public string Name;
18 |
19 | [BindText]
20 | public InnerTextOrHtml? Text;
21 |
22 | public override void Render(IHtmlRenderer renderer, GlobalContext context)
23 | {
24 | if (!context.Async || !context.Options.HasProcessor())
25 | {
26 | return;
27 | }
28 |
29 | renderer.StartElement(ComponentName).Attr("name", Name);
30 | renderer.Content(Text);
31 | renderer.EndElement(ComponentName);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Mjml.Net.PostProcessors/Components/HtmlAttributesComponent.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net.Components;
2 |
3 | public partial class HtmlAttributesComponent : Component
4 | {
5 | private static readonly AllowedParents Parents =
6 | [
7 | "mj-head"
8 | ];
9 |
10 | public override AllowedParents? AllowedParents => Parents;
11 |
12 | public override ContentType ContentType => ContentType.Complex;
13 |
14 | public override string ComponentName => "mj-html-attributes";
15 |
16 | public override void Render(IHtmlRenderer renderer, GlobalContext context)
17 | {
18 | if (!context.Async || !context.Options.HasProcessor())
19 | {
20 | return;
21 | }
22 |
23 | renderer.StartElement(ComponentName);
24 | RenderChildren(renderer, context);
25 | renderer.EndElement(ComponentName);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Mjml.Net.PostProcessors/Declarations/FallbackConverter.cs:
--------------------------------------------------------------------------------
1 | using AngleSharp.Css;
2 | using AngleSharp.Css.Dom;
3 | using AngleSharp.Css.Parser;
4 | using AngleSharp.Text;
5 |
6 | namespace Mjml.Net.Declarations;
7 |
8 | internal class FallbackConverter(IValueConverter inner) : IValueConverter
9 | {
10 | public ICssValue Convert(StringSource source)
11 | {
12 | var result = inner.Convert(source);
13 | if (result != null)
14 | {
15 | return result;
16 | }
17 |
18 | var value = source.Content;
19 | source.Next(value.Length);
20 | return new FallbackCssValue(value);
21 | }
22 |
23 | public ICssValue Merge(ICssValue[] values)
24 | {
25 | throw new NotImplementedException();
26 | }
27 |
28 | public ICssValue[] Split(ICssValue value)
29 | {
30 | throw new NotImplementedException();
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Mjml.Net.PostProcessors/Declarations/FallbackCssValue.cs:
--------------------------------------------------------------------------------
1 | using AngleSharp.Css.Dom;
2 | using AngleSharp.Css.Values;
3 |
4 | namespace Mjml.Net.Declarations;
5 |
6 | internal sealed class FallbackCssValue(string text) : ICssValue
7 | {
8 | public string CssText => text;
9 |
10 | ICssValue? ICssValue.Compute(ICssComputeContext context)
11 | {
12 | var converter = context.Converter;
13 |
14 | if (converter is not null && converter is not FallbackCssValueConverter)
15 | {
16 | var value = converter.Convert(text);
17 | return value?.Compute(context);
18 | }
19 |
20 | return null;
21 | }
22 |
23 | public bool Equals(ICssValue? other)
24 | {
25 | return other is FallbackCssValue f && f.CssText == CssText;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Mjml.Net.PostProcessors/Declarations/FallbackCssValueConverter.cs:
--------------------------------------------------------------------------------
1 | using AngleSharp.Css;
2 | using AngleSharp.Css.Dom;
3 | using AngleSharp.Text;
4 |
5 | namespace Mjml.Net.Declarations;
6 |
7 | internal class FallbackCssValueConverter(IValueConverter inner)
8 | : IValueConverter
9 | {
10 | public ICssValue Convert(StringSource source)
11 | {
12 | var result = inner.Convert(source);
13 | if (result != null)
14 | {
15 | return result;
16 | }
17 |
18 | var value = source.Content;
19 | source.Next(value.Length);
20 | return new FallbackCssValue(value);
21 | }
22 | }
23 |
24 | internal sealed class FallbackCssValueConverterWithAggregate(IValueConverter inner, IValueAggregator innerAggregator)
25 | : FallbackCssValueConverter(inner), IValueAggregator
26 | {
27 | public ICssValue Merge(ICssValue[] values)
28 | {
29 | return innerAggregator.Merge(values);
30 | }
31 |
32 | public ICssValue[] Split(ICssValue value)
33 | {
34 | return innerAggregator.Split(value);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Mjml.Net.PostProcessors/Declarations/FallbackDeclarationFactory.cs:
--------------------------------------------------------------------------------
1 | using AngleSharp.Css;
2 |
3 | namespace Mjml.Net.Declarations;
4 |
5 | public class FallbackDeclarationFactory : IDeclarationFactory
6 | {
7 | private readonly DefaultDeclarationFactory defaultFactory = new DefaultDeclarationFactory();
8 |
9 | public DeclarationInfo Create(string propertyName)
10 | {
11 | var declaration = defaultFactory.Create(propertyName);
12 |
13 | var converter =
14 | declaration.Converter is IValueAggregator aggregator ?
15 | new FallbackCssValueConverterWithAggregate(declaration.Converter, aggregator) :
16 | new FallbackCssValueConverter(declaration.Converter);
17 |
18 | var withConverter = new DeclarationInfo(
19 | declaration.Name,
20 | converter,
21 | declaration.Flags,
22 | declaration.InitialValue,
23 | declaration.Shorthands,
24 | declaration.Longhands);
25 |
26 | return withConverter;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Mjml.Net.PostProcessors/Declarations/ValueConverterExtensions.cs:
--------------------------------------------------------------------------------
1 | using AngleSharp.Css;
2 | using AngleSharp.Css.Dom;
3 | using AngleSharp.Css.Parser;
4 | using AngleSharp.Text;
5 |
6 | namespace Mjml.Net.Declarations;
7 |
8 | internal static class ValueConverterExtensions
9 | {
10 | public static ICssValue? Convert(this IValueConverter converter, string value)
11 | {
12 | var source = new StringSource(value);
13 | source.SkipSpacesAndComments();
14 | var varRefs = source.ParseVars();
15 |
16 | if (varRefs == null)
17 | {
18 | var result = converter.Convert(source);
19 | source.SkipSpacesAndComments();
20 | return source.IsDone ? result : null;
21 | }
22 |
23 | return varRefs;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Mjml.Net.PostProcessors/IAngleSharpPostProcessor.cs:
--------------------------------------------------------------------------------
1 | using AngleSharp.Dom;
2 |
3 | namespace Mjml.Net;
4 |
5 | public interface IAngleSharpPostProcessor
6 | {
7 | ValueTask ProcessAsync(IDocument document, MjmlOptions options,
8 | CancellationToken ct);
9 | }
10 |
--------------------------------------------------------------------------------
/Mjml.Net.PostProcessors/INestingPostProcessor.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net;
2 |
3 | public interface INestingPostProcessor
4 | {
5 | bool Has();
6 | }
7 |
--------------------------------------------------------------------------------
/Mjml.Net.PostProcessors/Mjml.Net.PostProcessors.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0;net7.0;net8.0;net9.0
5 | enable
6 | enable
7 | true
8 | true
9 | 1591
10 | en
11 | latest
12 | Mjml.Net
13 | README.md
14 |
15 |
16 |
17 |
18 |
19 |
20 | all
21 | runtime; build; native; contentfiles; analyzers; buildtransitive
22 |
23 |
24 | all
25 | runtime; build; native; contentfiles; analyzers; buildtransitive
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | True
38 |
39 |
40 |
41 |
42 | True
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/Mjml.Net.PostProcessors/PostProcessorExtensions.cs:
--------------------------------------------------------------------------------
1 | using Mjml.Net.Components;
2 |
3 | namespace Mjml.Net;
4 |
5 | public static class PostProcessorExtensions
6 | {
7 | public static MjmlOptions WithPostProcessors(this MjmlOptions options)
8 | {
9 | options.PostProcessors = [AngleSharpPostProcessor.Default];
10 | return options;
11 | }
12 |
13 | public static IMjmlRenderer AddHtmlAttributes(this IMjmlRenderer renderer)
14 | {
15 | renderer.Add();
16 | renderer.Add();
17 | renderer.Add();
18 | return renderer;
19 | }
20 |
21 | public static bool HasProcessor(this MjmlOptions options)
22 | {
23 | if (options.PostProcessors == null || options.PostProcessors.Length == 0)
24 | {
25 | return false;
26 | }
27 |
28 | foreach (var processor in options.PostProcessors)
29 | {
30 | if (processor is T)
31 | {
32 | return true;
33 | }
34 |
35 | if (processor is INestingPostProcessor nesting && nesting.Has())
36 | {
37 | return true;
38 | }
39 | }
40 |
41 | return false;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Mjml.Net.PostProcessors/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SebastianStehle/mjml-net/d04bfff1eeb72479553a3f949f9169ce0a8dcd63/Mjml.Net.PostProcessors/icon.png
--------------------------------------------------------------------------------
/Mjml.Net/AllowedAttributes.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net;
2 |
3 | public sealed class AllowedAttributes : Dictionary
4 | {
5 | public AllowedAttributes()
6 | {
7 | }
8 |
9 | public AllowedAttributes(AllowedAttributes source)
10 | : base(source)
11 | {
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Mjml.Net/AllowedParents.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net;
2 |
3 | public sealed class AllowedParents : List
4 | {
5 | }
6 |
--------------------------------------------------------------------------------
/Mjml.Net/AttributeTypes.cs:
--------------------------------------------------------------------------------
1 | using Mjml.Net.Types;
2 |
3 | namespace Mjml.Net;
4 |
5 | public static class AttributeTypes
6 | {
7 | public static readonly IType Align = new EnumType(false, "left", "center", "right");
8 |
9 | public static readonly IType AlignJustify = new EnumType(false, "left", "center", "right", "justify");
10 |
11 | public static readonly IType Boolean = new EnumType(false, "true", "false");
12 |
13 | public static readonly IType Color = new ColorType();
14 |
15 | public static readonly IType Direction = new EnumType(false, "ltr", "rtl");
16 |
17 | public static readonly IType LeftRight = new EnumType(false, "left", "right");
18 |
19 | public static readonly IType IncludeType = new EnumType(true, "mjml", "html", "css");
20 |
21 | public static readonly IType Inline = new EnumType(true, "inline");
22 |
23 | public static readonly IType Pixels = new NumberType(Unit.Pixels);
24 |
25 | public static readonly IType PixelsOrAuto = new OneOfType(new EnumType(false, "auto"), Pixels);
26 |
27 | public static readonly IType PixelsOrEm = new NumberType(Unit.Pixels, Unit.Em);
28 |
29 | public static readonly IType PixelsOrPercent = new NumberType(Unit.Pixels, Unit.Percent);
30 |
31 | public static readonly IType PixelsOrPercentOrNone = new NumberType(Unit.Pixels, Unit.Percent, Unit.None);
32 |
33 | public static readonly IType FourPixelsOrPercent = new ManyType(PixelsOrPercent, 1, 4);
34 |
35 | public static readonly IType String = new StringType(false);
36 |
37 | public static readonly IType RequiredString = new StringType(true);
38 |
39 | public static readonly IType VerticalAlign = new EnumType(false, "top", "middle", "bottom");
40 |
41 | public static readonly IType SocialTableLayout = new EnumType(false, "auto", "fixed");
42 |
43 | public static readonly IType SocialMode = new EnumType(false, "vertical", "horizontal");
44 |
45 | public static readonly IType TextAlign = new EnumType(false, "left", "right", "center", "justify");
46 | }
47 |
--------------------------------------------------------------------------------
/Mjml.Net/Attributes.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net;
2 |
3 | public sealed class Attributes : Dictionary
4 | {
5 | }
6 |
--------------------------------------------------------------------------------
/Mjml.Net/BindAttribute.cs:
--------------------------------------------------------------------------------
1 | #pragma warning disable IDE0060 // Remove unused parameter
2 |
3 | namespace Mjml.Net;
4 |
5 | [AttributeUsage(AttributeTargets.Field)]
6 | public sealed class BindAttribute : Attribute
7 | {
8 | public BindAttribute(string name)
9 | {
10 | }
11 |
12 | public BindAttribute(string name, BindType type)
13 | {
14 | }
15 |
16 | public BindAttribute(string name, Type type)
17 | {
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Mjml.Net/BindTextAttribute.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net;
2 |
3 | [AttributeUsage(AttributeTargets.Field)]
4 | public sealed class BindTextAttribute : Attribute
5 | {
6 | }
7 |
--------------------------------------------------------------------------------
/Mjml.Net/BindType.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net;
2 |
3 | public enum BindType
4 | {
5 | Align,
6 | AlignJustify,
7 | Boolean,
8 | Color,
9 | Direction,
10 | File,
11 | FourPixelsOrPercent,
12 | LeftRight,
13 | Inline,
14 | Pixels,
15 | PixelsOrAuto,
16 | PixelsOrEm,
17 | PixelsOrPercent,
18 | PixelsOrPercentOrNone,
19 | RequiredString,
20 | SocialMode,
21 | SocialTableLayout,
22 | String,
23 | TextAlign,
24 | VerticalAlign,
25 | }
26 |
--------------------------------------------------------------------------------
/Mjml.Net/BindingHelper.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net;
2 |
3 | public static class BindingHelper
4 | {
5 | public static string MakeLowerEndpoint(string breakpoint)
6 | {
7 | var (value, unit) = UnitParser.Parse(breakpoint);
8 |
9 | if (unit == Unit.Pixels)
10 | {
11 | return $"{value - 1}px";
12 | }
13 |
14 | return breakpoint;
15 | }
16 |
17 | public static (string? Top, string? Right, string? Bottom, string? Left) ParseShorthandBorder(string value)
18 | {
19 | if (string.IsNullOrEmpty(value))
20 | {
21 | return (null, null, null, null);
22 | }
23 |
24 | return (value, value, value, value);
25 | }
26 |
27 | public static (string? Top, string? Right, string? Bottom, string? Left) ParseShorthandValue(string value)
28 | {
29 | if (string.IsNullOrEmpty(value))
30 | {
31 | return (null, null, null, null);
32 | }
33 |
34 | var parts = value.Split(' ', StringSplitOptions.RemoveEmptyEntries);
35 |
36 | string? t = null;
37 | string? r = null;
38 | string? b = null;
39 | string? l = null;
40 |
41 | switch (parts.Length)
42 | {
43 | case 1:
44 | t = parts[0];
45 | r = parts[0];
46 | b = parts[0];
47 | l = parts[0];
48 | break;
49 | case 2:
50 | t = parts[0];
51 | r = parts[1];
52 | b = parts[0];
53 | l = parts[1];
54 | break;
55 | case 3:
56 | t = parts[0];
57 | r = parts[1];
58 | b = parts[2];
59 | l = parts[1];
60 | break;
61 | case 4:
62 | t = parts[0];
63 | r = parts[1];
64 | b = parts[2];
65 | l = parts[3];
66 | break;
67 | }
68 |
69 | return (t, r, b, l);
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Mjml.Net/Components/Body/BodyComponent.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net.Components.Body;
2 |
3 | public partial class BodyComponent : Component
4 | {
5 | private static readonly AllowedParents Parents =
6 | [
7 | "mjml"
8 | ];
9 |
10 | public override AllowedParents? AllowedParents => Parents;
11 |
12 | public override string ComponentName => "mj-body";
13 |
14 | [Bind("css-class")]
15 | public string? CssClass;
16 |
17 | [Bind("background-color")]
18 | public string? BackgroundColor;
19 |
20 | [Bind("width", BindType.Pixels)]
21 | public string Width = "600px";
22 |
23 | public override void Measure(GlobalContext context, double parentWidth, int numSiblings, int numNonRawSiblings)
24 | {
25 | ActualWidth = (int)UnitParser.Parse(Width).Value;
26 |
27 | MeasureChildren(context, ActualWidth);
28 | }
29 |
30 | public override void Render(IHtmlRenderer renderer, GlobalContext context)
31 | {
32 | if (BackgroundColor != null)
33 | {
34 | context.SetGlobalData("default", new Background(BackgroundColor));
35 | }
36 |
37 | renderer.StartBuffer();
38 |
39 | renderer.StartElement("div")
40 | .Attr("lang", context.GlobalData.Values.OfType().FirstOrDefault()?.Value)
41 | .Attr("dir", context.GlobalData.Values.OfType().FirstOrDefault()?.Value)
42 | .Class(CssClass)
43 | .Style("background-color", BackgroundColor);
44 |
45 | RenderChildren(renderer, context);
46 |
47 | renderer.EndElement("div");
48 |
49 | context.AddGlobalData(new BodyBuffer(renderer.EndBuffer()));
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Mjml.Net/Components/Body/BodyComponentBase.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net.Components.Body;
2 |
3 | public abstract partial class BodyComponentBase : Component
4 | {
5 | [Bind("css-class")]
6 | public string? CssClass;
7 | }
8 |
--------------------------------------------------------------------------------
/Mjml.Net/Components/Body/RawComponent.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net.Components.Body;
2 |
3 | public partial class RawComponent : BodyComponentBase
4 | {
5 | private static readonly AllowedParents Parents =
6 | [
7 | "mj-accordion",
8 | "mj-accordion-element",
9 | "mj-body",
10 | "mj-column",
11 | "mj-group",
12 | "mj-head",
13 | "mj-hero",
14 | "mjml",
15 | "mj-navbar",
16 | "mj-section",
17 | "mj-social",
18 | "mj-wrapper"
19 | ];
20 |
21 | public override AllowedParents? AllowedParents => Parents;
22 |
23 | public override ContentType ContentType => ContentType.Raw;
24 |
25 | public override string ComponentName => "mj-raw";
26 |
27 | public override bool Raw => true;
28 |
29 | public override void Render(IHtmlRenderer renderer, GlobalContext context)
30 | {
31 | RenderRaw(renderer);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Mjml.Net/Components/Body/SpacerComponent.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net.Components.Body;
2 |
3 | public partial class SpacerComponent : BodyComponentBase
4 | {
5 | private static readonly AllowedParents Parents =
6 | [
7 | "mj-column",
8 | "mj-hero"
9 | ];
10 |
11 | public override AllowedParents? AllowedParents => Parents;
12 |
13 | public override string ComponentName => "mj-spacer";
14 |
15 | [Bind("border")]
16 | public string? Border;
17 |
18 | [Bind("border-bottom")]
19 | public string? BorderBottom;
20 |
21 | [Bind("border-left")]
22 | public string? BorderLeft;
23 |
24 | [Bind("border-right")]
25 | public string? BorderRight;
26 |
27 | [Bind("border-top")]
28 | public string? BorderTop;
29 |
30 | [Bind("container-background-color", BindType.Color)]
31 | public string? ContainerBackgroundColor;
32 |
33 | [Bind("height", BindType.Pixels)]
34 | public string Height = "20px";
35 |
36 | [Bind("padding", BindType.FourPixelsOrPercent)]
37 | public string? Padding;
38 |
39 | [Bind("padding-bottom", BindType.PixelsOrPercent)]
40 | public string? PaddingBottom;
41 |
42 | [Bind("padding-left", BindType.PixelsOrPercent)]
43 | public string? PaddingLeft;
44 |
45 | [Bind("padding-right", BindType.PixelsOrPercent)]
46 | public string? PaddingRight;
47 |
48 | [Bind("padding-top", BindType.PixelsOrPercent)]
49 | public string? PaddingTop;
50 |
51 | public override void Render(IHtmlRenderer renderer, GlobalContext context)
52 | {
53 | renderer.StartElement("div")
54 | .Style("height", Height)
55 | .Style("line-height", Height);
56 |
57 | renderer.Content(" ");
58 |
59 | renderer.EndElement("div");
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Mjml.Net/Components/Body/WrapperComponent.cs:
--------------------------------------------------------------------------------
1 | using Mjml.Net.Extensions;
2 |
3 | namespace Mjml.Net.Components.Body;
4 |
5 | public partial class WrapperComponent : SectionComponent
6 | {
7 | private static readonly AllowedParents Parents =
8 | [
9 | "mj-body"
10 | ];
11 |
12 | public override AllowedParents? AllowedParents => Parents;
13 |
14 | public override string ComponentName => "mj-wrapper";
15 |
16 | protected override void RenderWrappedChildren(IHtmlRenderer renderer, GlobalContext context)
17 | {
18 | foreach (var child in ChildNodes)
19 | {
20 | if (child.Raw)
21 | {
22 | child.Render(renderer, context);
23 | }
24 | else
25 | {
26 | renderer.StartConditional("");
35 |
36 | child.Render(renderer, context);
37 |
38 | renderer.StartConditional("");
44 | }
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Mjml.Net/Components/CommentComponent.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net.Components;
2 |
3 | public sealed class CommentComponent : Component
4 | {
5 | public override string ComponentName => "comment";
6 |
7 | public override bool Raw => true;
8 |
9 | public string Text;
10 |
11 | public override void Render(IHtmlRenderer renderer, GlobalContext context)
12 | {
13 | renderer.Content($"");
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Mjml.Net/Components/Extensions/List/ListComponent.cs:
--------------------------------------------------------------------------------
1 | using Mjml.Net.Components.Body;
2 |
3 | namespace Mjml.Net.Components.Extensions.List;
4 |
5 | public partial class ListComponent : ColumnComponent
6 | {
7 | public override string ComponentName => "mj-list";
8 | }
9 |
--------------------------------------------------------------------------------
/Mjml.Net/Components/Extensions/List/ListExtensions.cs:
--------------------------------------------------------------------------------
1 | using Mjml.Net.Components.Body;
2 | using Mjml.Net.Components.Extensions.List;
3 |
4 | namespace Mjml.Net;
5 |
6 | public static class ListExtensions
7 | {
8 | public static IMjmlRenderer AddList(this IMjmlRenderer renderer)
9 | {
10 | return renderer.Add().Add();
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Mjml.Net/Components/Head/BreakpointComponent.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net.Components.Head;
2 |
3 | public partial class BreakpointComponent : HeadComponentBase
4 | {
5 | public override string ComponentName => "mj-breakpoint";
6 |
7 | [Bind("width")]
8 | public string Width;
9 |
10 | public override void Render(IHtmlRenderer renderer, GlobalContext context)
11 | {
12 | // Just in case that validation is disabled.
13 | if (Width != null)
14 | {
15 | context.Options.Breakpoint = Width;
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Mjml.Net/Components/Head/FontComponent.cs:
--------------------------------------------------------------------------------
1 | using Mjml.Net.Helpers;
2 |
3 | namespace Mjml.Net.Components.Head;
4 |
5 | public partial class FontComponent : HeadComponentBase
6 | {
7 | public override string ComponentName => "mj-font";
8 |
9 | [Bind("name")]
10 | public string? Name;
11 |
12 | [Bind("href")]
13 | public string? Href;
14 |
15 | public override void Render(IHtmlRenderer renderer, GlobalContext context)
16 | {
17 | // Just in case that validation is disabled.
18 | if (Href != null)
19 | {
20 | context.SetGlobalData(Name ?? Href, new Font(Href));
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Mjml.Net/Components/Head/HeadComponent.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net.Components.Head;
2 |
3 | public sealed class HeadComponent : Component
4 | {
5 | private static readonly AllowedParents Parents =
6 | [
7 | "mjml"
8 | ];
9 |
10 | public override AllowedParents? AllowedParents => Parents;
11 |
12 | public override string ComponentName => "mj-head";
13 |
14 | public override void Render(IHtmlRenderer renderer, GlobalContext context)
15 | {
16 | renderer.StartBuffer();
17 |
18 | RenderChildren(renderer, context);
19 |
20 | context.AddGlobalData(new HeadBuffer(renderer.EndBuffer()));
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Mjml.Net/Components/Head/HeadComponentBase.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net.Components.Head;
2 |
3 | public abstract class HeadComponentBase : Component
4 | {
5 | private static readonly AllowedParents? Parents =
6 | [
7 | "mj-head"
8 | ];
9 |
10 | public override AllowedParents? AllowedParents => Parents;
11 | }
12 |
--------------------------------------------------------------------------------
/Mjml.Net/Components/Head/PreviewComponent.cs:
--------------------------------------------------------------------------------
1 | using Mjml.Net.Helpers;
2 |
3 | namespace Mjml.Net.Components.Head;
4 |
5 | public partial class PreviewComponent : HeadComponentBase
6 | {
7 | public override ContentType ContentType => ContentType.Text;
8 |
9 | public override string ComponentName => "mj-preview";
10 |
11 | [BindText]
12 | public InnerTextOrHtml? Text;
13 |
14 | public override void Render(IHtmlRenderer renderer, GlobalContext context)
15 | {
16 | // Just in case that validation is disabled.
17 | if (Text != null)
18 | {
19 | var preview = new Preview(Text);
20 |
21 | // Allow multiple previews.
22 | context.AddGlobalData(preview);
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Mjml.Net/Components/Head/StyleComponent.cs:
--------------------------------------------------------------------------------
1 | using Mjml.Net.Helpers;
2 |
3 | namespace Mjml.Net.Components.Head;
4 |
5 | public partial class StyleComponent : HeadComponentBase
6 | {
7 | public override ContentType ContentType => ContentType.Text;
8 |
9 | public override string ComponentName => "mj-style";
10 |
11 | [Bind("inline", BindType.Inline)]
12 | public string? Inline;
13 |
14 | [BindText]
15 | public InnerTextOrHtml? Text;
16 |
17 | public override void Render(IHtmlRenderer renderer, GlobalContext context)
18 | {
19 | // Just in case that validation is disabled.
20 | if (Text != null)
21 | {
22 | var isInline = string.Equals(Inline, "inline", StringComparison.OrdinalIgnoreCase);
23 |
24 | var style = Style.Static(Text, isInline);
25 |
26 | // Allow multiple styles.
27 | context.AddGlobalData(style);
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Mjml.Net/Components/Head/TitleComponent.cs:
--------------------------------------------------------------------------------
1 | using Mjml.Net.Helpers;
2 |
3 | namespace Mjml.Net.Components.Head;
4 |
5 | public partial class TitleComponent : HeadComponentBase
6 | {
7 | public override ContentType ContentType => ContentType.Text;
8 |
9 | public override string ComponentName => "mj-title";
10 |
11 | [BindText]
12 | public InnerTextOrHtml? Text;
13 |
14 | public override void Render(IHtmlRenderer renderer, GlobalContext context)
15 | {
16 | // Just in case that validation is disabled.
17 | if (Text != null)
18 | {
19 | context.SetGlobalData("default", new Title(Text));
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Mjml.Net/Components/IncludeType.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net.Components;
2 |
3 | public enum IncludeType
4 | {
5 | Mjml,
6 | Html,
7 | Css
8 | }
9 |
--------------------------------------------------------------------------------
/Mjml.Net/Components/RootData.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 |
3 | #pragma warning disable SA1313 // Parameter names should begin with lower-case letter
4 |
5 | namespace Mjml.Net.Components;
6 |
7 | public sealed record BodyBuffer(StringBuilder? Buffer) : GlobalData;
8 |
9 | public sealed record Background(string Color) : GlobalData;
10 |
11 | public sealed record HeadBuffer(StringBuilder? Buffer) : GlobalData;
12 |
13 | public sealed record Language(string Value) : GlobalData;
14 |
15 | public sealed record Direction(string Value) : GlobalData;
16 |
17 | public sealed record ForceOWADesktop(bool Value) : GlobalData;
18 |
--------------------------------------------------------------------------------
/Mjml.Net/ContentType.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net;
2 |
3 | ///
4 | /// Defines what kind of content the component needs.
5 | ///
6 | public enum ContentType
7 | {
8 | ///
9 | /// The content is rendered raw.
10 | ///
11 | Raw,
12 |
13 | ///
14 | /// The component is complex and has other components as children.
15 | ///
16 | Complex,
17 |
18 | ///
19 | /// The content needs text contents.
20 | ///
21 | Text
22 | }
23 |
--------------------------------------------------------------------------------
/Mjml.Net/DefaultIDGenerator.cs:
--------------------------------------------------------------------------------
1 | using Mjml.Net.Extensions;
2 |
3 | namespace Mjml.Net;
4 |
5 | ///
6 | /// The default ID generator.
7 | ///
8 | public sealed class DefaultIDGenerator : IIdGenerator
9 | {
10 | private int counter;
11 |
12 | ///
13 | /// The only instance of the .
14 | ///
15 | public static readonly IIdGenerator Instance = new DefaultIDGenerator();
16 |
17 | private DefaultIDGenerator()
18 | {
19 | }
20 |
21 | ///
22 | public string Next()
23 | {
24 | Interlocked.Increment(ref counter);
25 |
26 | return counter.ToInvariantString();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Mjml.Net/DefaultPools.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 | using Microsoft.Extensions.ObjectPool;
3 | using Mjml.Net.Internal;
4 |
5 | namespace Mjml.Net;
6 |
7 | internal static class DefaultPools
8 | {
9 | public static readonly ObjectPool StringBuilders = new DefaultObjectPool(new StringBuilderPooledObjectPolicy());
10 |
11 | public static readonly ObjectPool Binders = new DefaultObjectPool(new BinderPolicy());
12 |
13 | public static readonly ObjectPool RenderContexts = new DefaultObjectPool(new MjmlRenderContextPolicy());
14 |
15 | private sealed class MjmlRenderContextPolicy : PooledObjectPolicy
16 | {
17 | public override MjmlRenderContext Create()
18 | {
19 | return new MjmlRenderContext();
20 | }
21 |
22 | public override bool Return(MjmlRenderContext obj)
23 | {
24 | obj.Clear();
25 |
26 | return true;
27 | }
28 | }
29 |
30 | private sealed class BinderPolicy : PooledObjectPolicy
31 | {
32 | public override Binder Create()
33 | {
34 | return new Binder();
35 | }
36 |
37 | public override bool Return(Binder obj)
38 | {
39 | obj.Clear();
40 | return true;
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Mjml.Net/Extensions/StringExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Globalization;
2 |
3 | namespace Mjml.Net.Extensions;
4 |
5 | public static class StringExtensions
6 | {
7 | private static readonly char[] TrimChars = [' ', '\n', '\r'];
8 |
9 | public static string ToInvariantString(this double value)
10 | {
11 | return value.ToString(CultureInfo.InvariantCulture);
12 | }
13 |
14 | public static string ToInvariantString(this int value)
15 | {
16 | return value.ToString(CultureInfo.InvariantCulture);
17 | }
18 |
19 | public static ReadOnlySpan TrimInputStart(this ReadOnlySpan source)
20 | {
21 | return source.TrimStart(TrimChars);
22 | }
23 |
24 | public static ReadOnlySpan TrimInputEnd(this ReadOnlySpan source)
25 | {
26 | return source.TrimEnd(TrimChars);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Mjml.Net/Extensions/WriterExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net.Extensions;
2 |
3 | public static class WriterExtensions
4 | {
5 | public static IHtmlStyleRenderer StyleIf(this IHtmlStyleRenderer renderer, string name, bool condition, string? value)
6 | {
7 | if (condition)
8 | {
9 | renderer.Style(name, value);
10 | }
11 |
12 | return renderer;
13 | }
14 |
15 | public static IHtmlStyleRenderer StyleIf(this IHtmlStyleRenderer renderer, string name, bool condition, double value, string unit)
16 | {
17 | if (condition)
18 | {
19 | renderer.Style(name, $"{value}{unit}");
20 | }
21 |
22 | return renderer;
23 | }
24 |
25 | public static IHtmlStyleRenderer StyleIfNumber(this IHtmlStyleRenderer renderer, string name, double value, string unit)
26 | {
27 | if (double.IsNaN(value))
28 | {
29 | return renderer;
30 | }
31 | else
32 | {
33 | return renderer.Style(name, $"{value}{unit}");
34 | }
35 | }
36 |
37 | public static IHtmlAttrRenderer AttrOrAuto(this IHtmlAttrRenderer renderer, string name, string? value)
38 | {
39 | if (value == "auto")
40 | {
41 | return renderer.Attr(name, "auto");
42 | }
43 | else
44 | {
45 | return renderer.Attr(name, $"{UnitParser.Parse(value).Value}");
46 | }
47 | }
48 |
49 | public static IHtmlClassRenderer Classes(this IHtmlClassRenderer renderer, string? classNames, string suffix)
50 | {
51 | if (string.IsNullOrEmpty(classNames))
52 | {
53 | return renderer;
54 | }
55 |
56 | if (string.IsNullOrWhiteSpace(suffix))
57 | {
58 | renderer.Class(classNames);
59 | return renderer;
60 | }
61 |
62 | var span = classNames.AsSpan().Trim();
63 |
64 | while (span.Length > 0)
65 | {
66 | var index = span.IndexOf(' ');
67 |
68 | if (index > 0)
69 | {
70 | renderer.Class($"{span[..index]}-{suffix}");
71 |
72 | span = span[index..].Trim();
73 | }
74 | else
75 | {
76 | break;
77 | }
78 | }
79 |
80 | if (span.Length > 0)
81 | {
82 | renderer.Class($"{span}-{suffix}");
83 | }
84 |
85 | return renderer;
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Mjml.Net/GlobalContext.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net;
2 |
3 | public record struct AttributeKey(string ClassOrType, string Name);
4 |
5 | public record struct AttributeParentKey(string ParentClass, string ClassOrType, string Name);
6 |
7 | public sealed class GlobalContext
8 | {
9 | private readonly Dictionary attributesByName = new Dictionary(10);
10 | private readonly Dictionary attributesByClass = new Dictionary(10);
11 | private readonly Dictionary attributesByParentClass = new Dictionary(10);
12 | private IFileLoader? fileLoader;
13 |
14 | public Dictionary<(Type Type, object Identifier), GlobalData> GlobalData { get; } = [];
15 |
16 | public IReadOnlyDictionary AttributesByClass => attributesByClass;
17 |
18 | public IReadOnlyDictionary AttributesByParentClass => attributesByParentClass;
19 |
20 | public IReadOnlyDictionary AttributesByName => attributesByName;
21 |
22 | public MjmlOptions Options { get; set; }
23 |
24 | public bool Async { get; set; }
25 |
26 | public IFileLoader? FileLoader
27 | {
28 | get => fileLoader ??= Options?.FileLoader?.Invoke();
29 | }
30 |
31 | public void Clear()
32 | {
33 | GlobalData.Clear();
34 | fileLoader = null;
35 | attributesByClass.Clear();
36 | attributesByName.Clear();
37 | Options = null!;
38 | }
39 |
40 | public void SetGlobalData(object identifier, T value, bool doNotOverride = false) where T : GlobalData
41 | {
42 | var key = (typeof(T), identifier);
43 |
44 | if (doNotOverride && GlobalData.ContainsKey(key))
45 | {
46 | return;
47 | }
48 |
49 | GlobalData[key] = value;
50 | }
51 |
52 | public void AddGlobalData(T value) where T : GlobalData
53 | {
54 | var key = (typeof(T), Guid.NewGuid());
55 |
56 | GlobalData[key] = value;
57 | }
58 |
59 | public void SetTypeAttribute(string name, string type, string value)
60 | {
61 | attributesByName[new AttributeKey(type, name)] = value;
62 | }
63 |
64 | public void SetClassAttribute(string name, string className, string value)
65 | {
66 | attributesByClass[new AttributeKey(className, name)] = value;
67 | }
68 |
69 | public void SetParentClassAttribute(string name, string parentClassName, string type, string value)
70 | {
71 | attributesByParentClass[new AttributeParentKey(parentClassName, type, name)] = value;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Mjml.Net/GlobalData.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net;
2 |
3 | public abstract record GlobalData;
4 |
--------------------------------------------------------------------------------
/Mjml.Net/HelperTarget.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net;
2 |
3 | ///
4 | /// Defines which parts is rendered.
5 | ///
6 | public enum HelperTarget
7 | {
8 | ///
9 | /// The start of the head.
10 | ///
11 | HeadStart,
12 |
13 | ///
14 | /// The end of the head.
15 | ///
16 | HeadEnd,
17 |
18 | ///
19 | /// The start of the body.
20 | ///
21 | BodyStart,
22 |
23 | ///
24 | /// The end of the body.
25 | ///
26 | BodyEnd
27 | }
28 |
--------------------------------------------------------------------------------
/Mjml.Net/Helpers/Font.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net.Helpers;
2 |
3 | #pragma warning disable SA1313 // Parameter names should begin with lower-case letter
4 | public sealed record Font(string Href) : GlobalData;
5 | #pragma warning restore SA1313 // Parameter names should begin with lower-case letter
6 |
7 | public sealed class FontHelper : IHelper
8 | {
9 | public void Render(IHtmlRenderer renderer, HelperTarget target, GlobalContext context)
10 | {
11 | if (target != HelperTarget.HeadEnd)
12 | {
13 | return;
14 | }
15 |
16 | var hasFont = context.GlobalData.Values.Any(x => x is Font);
17 |
18 | if (!hasFont)
19 | {
20 | return;
21 | }
22 |
23 | renderer.Content("");
24 |
25 | foreach (var (_, value) in context.GlobalData)
26 | {
27 | if (value is Font font)
28 | {
29 | renderer.StartElement("link")
30 | .Attr("href", font.Href)
31 | .Attr("rel", "stylesheet")
32 | .Attr("type", "text/css");
33 | }
34 | }
35 |
36 | renderer.StartElement("style")
37 | .Attr("type", "text/css");
38 |
39 | foreach (var (_, value) in context.GlobalData)
40 | {
41 | if (value is Font font)
42 | {
43 | renderer.Content($"@import url({font.Href});");
44 | }
45 | }
46 |
47 | renderer.EndElement("style");
48 |
49 | renderer.Content("");
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Mjml.Net/Helpers/Preview.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net.Helpers;
2 |
3 | #pragma warning disable SA1313 // Parameter names should begin with lower-case letter
4 | public sealed record Preview(InnerTextOrHtml Value) : GlobalData
5 | #pragma warning restore SA1313 // Parameter names should begin with lower-case letter
6 | {
7 | }
8 |
9 | public sealed class PreviewHelper : IHelper
10 | {
11 | public void Render(IHtmlRenderer renderer, HelperTarget target, GlobalContext context)
12 | {
13 | if (target != HelperTarget.BodyStart)
14 | {
15 | return;
16 | }
17 |
18 | if (context.GlobalData.Values.OfType().Any())
19 | {
20 | renderer.StartElement("div")
21 | .Attr("style", "display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;");
22 |
23 | foreach (var preview in context.GlobalData.Values.OfType())
24 | {
25 | renderer.Content(preview.Value);
26 | }
27 |
28 | renderer.EndElement("div");
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Mjml.Net/Helpers/Title.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net.Helpers;
2 |
3 | #pragma warning disable SA1313 // Parameter names should begin with lower-case letter
4 | public sealed record Title(InnerTextOrHtml Value) : GlobalData;
5 | #pragma warning restore SA1313 // Parameter names should begin with lower-case letter
6 |
7 | public sealed class TitleHelper : IHelper
8 | {
9 | public void Render(IHtmlRenderer renderer, HelperTarget target, GlobalContext context)
10 | {
11 | if (target != HelperTarget.HeadStart)
12 | {
13 | return;
14 | }
15 |
16 | var title = context.GlobalData.Values.OfType().FirstOrDefault()?.Value;
17 |
18 | renderer.StartElement("title");
19 | renderer.Content(title);
20 | renderer.EndElement("title");
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Mjml.Net/HtmlError.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net;
2 |
3 | public record struct HtmlError(int LineNumber, int LinePosition, string Message);
4 |
--------------------------------------------------------------------------------
/Mjml.Net/IBinder.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net;
2 |
3 | ///
4 | /// Providers values from MJML.
5 | ///
6 | public interface IBinder
7 | {
8 | ///
9 | /// Get the attribute of the node with the given name.
10 | ///
11 | /// The name of the attribute.
12 | ///
13 | /// The attribute of the node or null if not found.
14 | ///
15 | string? GetAttribute(string name);
16 |
17 | ///
18 | /// Gets the class names.
19 | ///
20 | string[] ClassNames { get; }
21 |
22 | ///
23 | /// Get the text content of the node.
24 | ///
25 | /// The content of the node or null if not found.
26 | InnerTextOrHtml? GetText();
27 | }
28 |
--------------------------------------------------------------------------------
/Mjml.Net/IComponent.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net;
2 |
3 | public interface IComponent
4 | {
5 | AllowedParents? AllowedParents { get; }
6 |
7 | AllowedAttributes? AllowedFields { get; }
8 |
9 | IEnumerable ChildNodes { get; }
10 |
11 | IComponent? Parent { get; set; }
12 |
13 | IBinder Binder { get; }
14 |
15 | SourcePosition Position { get; set; }
16 |
17 | ContentType ContentType { get; }
18 |
19 | bool Raw { get; }
20 |
21 | string ComponentName { get; }
22 |
23 | string? GetDefaultValue(string name);
24 |
25 | string? GetInheritingAttribute(string name);
26 |
27 | string? GetAttribute(string name);
28 |
29 | double ActualWidth { get; }
30 |
31 | void SetBinder(IBinder binder);
32 |
33 | void Read(IHtmlReader htmlReader, IMjmlReader mjmlReader, GlobalContext context);
34 |
35 | void Bind(GlobalContext context);
36 |
37 | void AddChild(IComponent child);
38 |
39 | void AddChild(InnerTextOrHtml rawXml);
40 |
41 | void Render(IHtmlRenderer renderer, GlobalContext context);
42 |
43 | void Measure(GlobalContext context, double parentWidth, int numSiblings, int numNonRawSiblings);
44 | }
45 |
--------------------------------------------------------------------------------
/Mjml.Net/IFileLoader.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net;
2 |
3 | ///
4 | /// Provides files for mj-include components.
5 | ///
6 | public interface IFileLoader
7 | {
8 | ///
9 | /// Loads the file as text from the specified path and usigng context object from the parent file.
10 | ///
11 | /// The path to the file.
12 | ///
13 | /// The text of the file or null, if not found and an optional context.
14 | ///
15 | string? LoadText(string path);
16 | }
17 |
--------------------------------------------------------------------------------
/Mjml.Net/IHelper.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net;
2 |
3 | ///
4 | /// A helper is responsible to set global data to tag, which will be mainly the head tag.
5 | ///
6 | ///
7 | /// Used for fonts and styles.
8 | ///
9 | public interface IHelper
10 | {
11 | ///
12 | /// Renders the global data.
13 | ///
14 | /// The renderer.
15 | /// The target where the helpers are rendered.
16 | /// The context to render.
17 | public void Render(IHtmlRenderer renderer, HelperTarget target, GlobalContext context);
18 | }
19 |
--------------------------------------------------------------------------------
/Mjml.Net/IHtmlReader.cs:
--------------------------------------------------------------------------------
1 | using HtmlPerformanceKit;
2 |
3 | namespace Mjml.Net;
4 |
5 | public interface IHtmlReader
6 | {
7 | public Action? OnError { get; set; }
8 |
9 | int LineNumber { get; }
10 |
11 | int LinePosition { get; }
12 |
13 | int AttributeCount { get; }
14 |
15 | string Name { get; }
16 |
17 | string Text { get; }
18 |
19 | ReadOnlySpan NameAsSpan { get; }
20 |
21 | ReadOnlySpan TextAsSpan { get; }
22 |
23 | bool SelfClosingElement { get; }
24 |
25 | HtmlTokenKind TokenKind { get; }
26 |
27 | bool Read();
28 |
29 | string GetAttribute(string name);
30 |
31 | string GetAttribute(int index);
32 |
33 | string GetAttributeName(int index);
34 |
35 | InnerTextOrHtml ReadInnerHtml();
36 |
37 | InnerTextOrHtml ReadInnerText();
38 |
39 | IHtmlReader ReadSubtree();
40 | }
41 |
--------------------------------------------------------------------------------
/Mjml.Net/IIdGenerator.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net;
2 |
3 | ///
4 | /// Generates IDs for HTML tags.
5 | ///
6 | public interface IIdGenerator
7 | {
8 | ///
9 | /// Generates a new ID and returns the result.
10 | ///
11 | /// The created ID.
12 | string Next();
13 | }
14 |
--------------------------------------------------------------------------------
/Mjml.Net/IMjmlReader.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net;
2 |
3 | ///
4 | /// Reads MJML fragments.
5 | ///
6 | public interface IMjmlReader
7 | {
8 | ///
9 | /// Read a xml fragment from a string.
10 | ///
11 | /// The mjml fragment reader.
12 | /// The fle for debugging purposes.
13 | /// The parent component.
14 | void ReadFragment(string mjml, string? file, IComponent parent);
15 | }
16 |
--------------------------------------------------------------------------------
/Mjml.Net/IPostProcessor.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net;
2 |
3 | public interface IPostProcessor
4 | {
5 | ValueTask PostProcessAsync(string html, MjmlOptions options,
6 | CancellationToken ct);
7 | }
8 |
--------------------------------------------------------------------------------
/Mjml.Net/IType.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net;
2 |
3 | public interface IType
4 | {
5 | bool Validate(string value, ref ValidationContext context);
6 |
7 | public string Coerce(string value)
8 | {
9 | return value;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Mjml.Net/IValidator.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net;
2 |
3 | public interface IValidator
4 | {
5 | void Attribute(string name, string value, IComponent component, ValidationErrors errors, ref ValidationContext context);
6 |
7 | void Components(IComponent root, ValidationErrors errors, ref ValidationContext context);
8 | }
9 |
--------------------------------------------------------------------------------
/Mjml.Net/InMemoryFileLoader.cs:
--------------------------------------------------------------------------------
1 | using Mjml.Net;
2 |
3 | namespace Tests.Internal;
4 |
5 | ///
6 | /// Provides the files from an in memory store.
7 | ///
8 | ///
9 | /// Useful for preloading.
10 | ///
11 | public sealed class InMemoryFileLoader : IFileLoader
12 | {
13 | private readonly Stack pathStack = new Stack();
14 | private readonly IReadOnlyDictionary content;
15 |
16 | public InMemoryFileLoader(IReadOnlyDictionary content)
17 | {
18 | this.content = content;
19 | }
20 |
21 | ///
22 | public string? LoadText(string path)
23 | {
24 | ArgumentNullException.ThrowIfNull(path);
25 |
26 | pathStack.TryPeek(out var parentPath);
27 |
28 | path = BuildPath(path, parentPath);
29 |
30 | pathStack.Push(path);
31 |
32 | return content.GetValueOrDefault(path);
33 | }
34 |
35 | private static string BuildPath(string path, string? parentPath)
36 | {
37 | if (path.StartsWith('/') || path.StartsWith('\\'))
38 | {
39 | return path;
40 | }
41 |
42 | if (parentPath == null)
43 | {
44 | return path;
45 | }
46 |
47 | var folderIndex = parentPath.LastIndexOf('/');
48 |
49 | if (folderIndex >= 0)
50 | {
51 | var folderPart = parentPath[..folderIndex];
52 |
53 | return $"{folderPart}/{path}";
54 | }
55 |
56 | folderIndex = parentPath.LastIndexOf('\\');
57 |
58 | if (folderIndex >= 0)
59 | {
60 | var folderPart = parentPath[..folderIndex];
61 |
62 | return $"{folderPart}\\{path}";
63 | }
64 |
65 | return path;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Mjml.Net/Internal/Constants.cs:
--------------------------------------------------------------------------------
1 | namespace Mjml.Net.Internal;
2 |
3 | internal static class Constants
4 | {
5 | public const string All = "mj-all";
6 |
7 | public const string MjClass = "mj-class";
8 | }
9 |
--------------------------------------------------------------------------------
/Mjml.Net/Internal/ReflectionHelper.cs:
--------------------------------------------------------------------------------
1 | using System.Linq.Expressions;
2 | using System.Reflection;
3 | using System.Reflection.Emit;
4 |
5 | namespace Mjml.Net.Internal;
6 |
7 | internal static class ReflectionHelper
8 | {
9 | public static Func CreateFactory(this ConstructorInfo constructorInfo)
10 | {
11 | var parameters = new[]
12 | {
13 | Expression.Parameter(typeof(T1)),
14 | Expression.Parameter(typeof(T2))
15 | };
16 |
17 | var constructorExpression = Expression.New(constructorInfo, parameters);
18 |
19 | return Expression.Lambda>(constructorExpression, parameters).Compile();
20 | }
21 |
22 | public static Func
", result, StringComparison.OrdinalIgnoreCase);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Tests/Components/ButtonTests.cs:
--------------------------------------------------------------------------------
1 | using Tests.Internal;
2 |
3 | namespace Tests.Components;
4 |
5 | public class ButtonTests
6 | {
7 | [Fact]
8 | public void Should_render_button()
9 | {
10 | var source = @"
11 |
12 | Button
13 |
14 | ";
15 |
16 | var (result, _) = TestHelper.Render(source);
17 |
18 | AssertHelpers.HtmlFileAssert("Components.Outputs.Button.html", result);
19 | }
20 |
21 | [Fact]
22 | public void Should_render_button_link()
23 | {
24 | var source = @"
25 |
26 | Button Link
27 |
28 | ";
29 |
30 | var (result, _) = TestHelper.Render(source);
31 |
32 | AssertHelpers.HtmlFileAssert("Components.Outputs.ButtonLink.html", result);
33 | }
34 |
35 | [Fact]
36 | public void Should_render_button_link_with_rel()
37 | {
38 | var source = @"
39 |
40 | Button Link
41 |
42 | ";
43 |
44 | var (result, _) = TestHelper.Render(source);
45 |
46 | AssertHelpers.HtmlFileAssert("Components.Outputs.ButtonLinkWithRel.html", result);
47 | }
48 |
49 | [Fact]
50 | public void Should_render_button_with_mixed_content()
51 | {
52 | var source = @"
53 |
54 | Hello MJML
55 |
56 | ";
57 |
58 | var (result, _) = TestHelper.Render(source);
59 |
60 | AssertHelpers.HtmlFileAssert("Components.Outputs.ButtonMixedContent.html", result);
61 | }
62 |
63 | [Fact]
64 | public void Should_render_button_with_mixed_content2()
65 | {
66 | var source = @"
67 |
68 | Hello MJML
69 |
70 | ";
71 |
72 | var (result, _) = TestHelper.Render(source);
73 |
74 | AssertHelpers.HtmlFileAssert("Components.Outputs.ButtonMixedContent2.html", result);
75 | }
76 |
77 | [Fact]
78 | public void Should_render_button_without_width_unit()
79 | {
80 | var source = @"
81 |
82 | Button
83 |
84 | ";
85 |
86 | var (result, _) = TestHelper.Render(source);
87 |
88 | AssertHelpers.HtmlFileAssert("Components.Outputs.ButtonWithoutWidthUnit.html", result);
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/Tests/Components/CommentTests.cs:
--------------------------------------------------------------------------------
1 | using Mjml.Net;
2 | using Tests.Internal;
3 |
4 | namespace Tests.Components;
5 |
6 | public class CommentTests
7 | {
8 | [Fact]
9 | public void Should_keep_comments()
10 | {
11 | var source = @"
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | ";
20 |
21 | var (result, _) = TestHelper.Render(source, new MjmlOptions
22 | {
23 | KeepComments = true
24 | });
25 |
26 | AssertHelpers.HtmlFileAssert("Components.Outputs.Comments.html", result);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Tests/Components/DividerTests.cs:
--------------------------------------------------------------------------------
1 | using Tests.Internal;
2 |
3 | namespace Tests.Components;
4 |
5 | public class DividerTests
6 | {
7 | [Fact]
8 | public void Should_render_divider()
9 | {
10 | var source = @"";
11 |
12 | var (result, _) = TestHelper.Render(source);
13 |
14 | AssertHelpers.HtmlFileAssert("Components.Outputs.Divider.html", result);
15 | }
16 |
17 | [Fact]
18 | public void Should_render_without_width_unit()
19 | {
20 | var source = @"";
21 |
22 | var (result, _) = TestHelper.Render(source);
23 |
24 | AssertHelpers.HtmlFileAssert("Components.Outputs.DividerWithoutWidthUnit.html", result);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Tests/Components/FontTests.cs:
--------------------------------------------------------------------------------
1 | using Mjml.Net.Helpers;
2 | using Tests.Internal;
3 |
4 | namespace Tests.Components;
5 |
6 | public class FontTests
7 | {
8 | [Fact]
9 | public void Should_render_font()
10 | {
11 | var source = @"
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | ";
20 |
21 | var (result, _) = TestHelper.Render(source, helpers: [new FontHelper()]);
22 |
23 | AssertHelpers.HtmlFileAssert("Components.Outputs.Font.html", result);
24 | }
25 |
26 | [Fact]
27 | public void Should_add_font_implicitely()
28 | {
29 | var source = @"
30 |
31 |
32 |
33 |
34 |
35 | ";
36 |
37 | var (result, _) = TestHelper.Render(source, helpers: [new FontHelper()]);
38 |
39 | AssertHelpers.HtmlFileAssert("Components.Outputs.FontUbuntu.html", result);
40 | }
41 |
42 | [Fact]
43 | public void Should_add_font_implicitely_but_not_override_custom()
44 | {
45 | var source = @"
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | ";
55 |
56 | var (result, _) = TestHelper.Render(source, helpers: [new FontHelper()]);
57 |
58 | AssertHelpers.HtmlFileAssert("Components.Outputs.FontUbuntu2.html", result);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Tests/Components/GroupTests.cs:
--------------------------------------------------------------------------------
1 | using Tests.Internal;
2 |
3 | namespace Tests.Components;
4 |
5 | public class GroupTests
6 | {
7 | [Fact]
8 | public void Should_render_groups()
9 | {
10 | var source = @"
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | ";
20 |
21 | var (result, _) = TestHelper.Render(source);
22 |
23 | AssertHelpers.HtmlFileAssert("Components.Outputs.Group.html", result);
24 | }
25 |
26 | [Fact]
27 | public void Should_render_group_with_columns()
28 | {
29 | var source = @"
30 |
31 |
32 |
33 |
34 |
35 |
36 | ";
37 |
38 | var (result, _) = TestHelper.Render(source);
39 |
40 | AssertHelpers.HtmlFileAssert("Components.Outputs.GroupWithColumns.html", result);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Tests/Components/HeroTests.cs:
--------------------------------------------------------------------------------
1 | using Tests.Internal;
2 |
3 | namespace Tests.Components;
4 |
5 | public class HeroTests
6 | {
7 | [Fact]
8 | public void Should_render_hero()
9 | {
10 | var source = @"
11 |
12 |
13 | ";
14 |
15 | var (result, _) = TestHelper.Render(source);
16 |
17 | AssertHelpers.HtmlFileAssert("Components.Outputs.Hero.html", result);
18 | }
19 |
20 | [Fact]
21 | public void Should_render_hero_without_width_unit()
22 | {
23 | var source = @"
24 |
25 |
26 | ";
27 |
28 | var (result, _) = TestHelper.Render(source);
29 |
30 | AssertHelpers.HtmlFileAssert("Components.Outputs.Hero.html", result);
31 | }
32 |
33 | [Fact]
34 | public void Should_render_hero_with_child()
35 | {
36 | var source = @"
37 |
38 |
39 |
40 | ";
41 |
42 | var (result, _) = TestHelper.Render(source);
43 |
44 | AssertHelpers.HtmlFileAssert("Components.Outputs.HeroDivider.html", result);
45 | }
46 |
47 | [Fact]
48 | public void Should_render_hero_with_children()
49 | {
50 | var source = @"
51 |
52 |
53 |
54 |
55 | ";
56 |
57 | var (result, _) = TestHelper.Render(source);
58 |
59 | AssertHelpers.HtmlFileAssert("Components.Outputs.HeroDividers.html", result);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Tests/Components/ImageTests.cs:
--------------------------------------------------------------------------------
1 | using Tests.Internal;
2 |
3 | namespace Tests.Components;
4 |
5 | public class ImageTests
6 | {
7 | [Fact]
8 | public void Should_render_image()
9 | {
10 | var source = @"";
11 |
12 | var (result, _) = TestHelper.Render(source);
13 |
14 | AssertHelpers.HtmlFileAssert("Components.Outputs.Image.html", result);
15 | }
16 |
17 | [Fact]
18 | public void Should_render_image_with_link()
19 | {
20 | var source = @"";
21 |
22 | var (result, _) = TestHelper.Render(source);
23 |
24 | AssertHelpers.HtmlFileAssert("Components.Outputs.ImageWithLink.html", result);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Tests/Components/ListTests.cs:
--------------------------------------------------------------------------------
1 | using Tests.Internal;
2 |
3 | namespace Tests.Components;
4 |
5 | public class ListTests
6 | {
7 | [Fact]
8 | public void Should_render_lists()
9 | {
10 | var source = @"
11 |
12 | List item one.
13 | List item two.
14 | List item three.
15 | List item four.
16 | ";
17 |
18 | var (result, _) = TestHelper.Render(source);
19 |
20 | AssertHelpers.HtmlFileAssert("Components.Outputs.List.html", result);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Tests/Components/MsoButtonTests.cs:
--------------------------------------------------------------------------------
1 | using Tests.Internal;
2 |
3 | namespace Tests.Components;
4 |
5 | public class MsoButtonTests
6 | {
7 | [Fact]
8 | public void Should_render_regular_button_by_default()
9 | {
10 | var source = @"
11 |
12 | Button
13 |
14 | ";
15 |
16 | var (result, _) = TestHelper.Render(source);
17 |
18 | AssertHelpers.HtmlFileAssert("Components.Outputs.Button.html", result);
19 | }
20 |
21 | [Fact]
22 | public void Should_render_mso_proof_button()
23 | {
24 | var source = @"
25 |
26 | Button
27 |
28 | ";
29 |
30 | var (result, _) = TestHelper.Render(source);
31 |
32 | AssertHelpers.HtmlFileAssert("Components.Outputs.MsoButton.html", result);
33 | }
34 |
35 | [Fact]
36 | public void Should_render_mso_proof_button_with_border()
37 | {
38 | var source = @"
39 |
40 | Reset Password
41 |
42 | ";
43 |
44 | var (result, _) = TestHelper.Render(source);
45 |
46 | AssertHelpers.HtmlFileAssert("Components.Outputs.MsoButtonWithBorder.html", result);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Tests/Components/NavbarTests.cs:
--------------------------------------------------------------------------------
1 | using Mjml.Net;
2 | using Tests.Internal;
3 |
4 | namespace Tests.Components;
5 |
6 | public class NavbarTests
7 | {
8 | [Fact]
9 | public void Should_render_empty_navbar()
10 | {
11 | var source = @"";
12 |
13 | var (result, _) = TestHelper.Render(source, new MjmlOptions
14 | {
15 | IdGenerator = new StaticIdGenerator("4c48e6fe53b37010")
16 | });
17 |
18 | AssertHelpers.HtmlFileAssert("Components.Outputs.Navbar.html", result);
19 | }
20 |
21 | [Fact]
22 | public void Should_render_empty_navbar_without_hamburger()
23 | {
24 | var source = @"";
25 |
26 | var (result, _) = TestHelper.Render(source, new MjmlOptions
27 | {
28 | IdGenerator = new StaticIdGenerator("b5e7b1c2f1d5bc37")
29 | });
30 |
31 | AssertHelpers.HtmlFileAssert("Components.Outputs.NavbarWithoutHamburger.html", result);
32 | }
33 |
34 | [Fact]
35 | public void Should_render_navbar()
36 | {
37 | var source = @"
38 |
39 | Link1
40 | Link2
41 | Link3
42 | ";
43 |
44 | var (result, _) = TestHelper.Render(source, new MjmlOptions
45 | {
46 | IdGenerator = new StaticIdGenerator("b5e7b1c2f1d5bc37")
47 | });
48 |
49 | AssertHelpers.HtmlFileAssert("Components.Outputs.NavbarWithLinks.html", result);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/Breakpoint.html:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/Button.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Button
6 | |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/ButtonLink.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/ButtonLinkWithRel.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/ButtonMixedContent.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Hello MJML
7 |
8 | |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/ButtonMixedContent2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Hello MJML
7 |
8 | |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/ButtonWithoutWidthUnit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Button
5 | |
6 |
7 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/ChildClasses.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/ColumnClass.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
8 | |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/ColumnFour.html:
--------------------------------------------------------------------------------
1 |
8 |
15 |
22 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/ColumnOne.html:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/ColumnOneWithInnerBorder.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Hello World!
12 |
13 | |
14 |
15 |
16 |
17 | |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/ColumnOneWithPadding.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 | |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/ColumnThree.html:
--------------------------------------------------------------------------------
1 |
7 |
13 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/ColumnTwo.html:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/Comments.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/Divider.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/DividerWithoutWidthUnit.html:
--------------------------------------------------------------------------------
1 |
2 |
9 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/Font.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/FontUbuntu.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/FontUbuntu2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/Group.html:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/GroupWithColumns.html:
--------------------------------------------------------------------------------
1 |
2 |
7 |
13 |
17 |
23 |
28 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/Hero.html:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
17 |
18 |
19 |
20 |
21 |
22 |
26 | |
27 |
28 |
29 |
30 |
31 |
36 | |
37 |
38 |
39 |
40 |
41 |
46 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/HtmlAttributeInvalid.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/HtmlAttributes.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/HtmlAttributesNoProcessor.html:
--------------------------------------------------------------------------------
1 |
6 |
7 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/Image.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/ImageWithLink.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/MsoButton.html:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
19 | Button
20 |
21 | |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/MsoButtonWithBorder.html:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
19 | Reset Password
20 |
21 | |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/Navbar.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
16 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/NavbarWithLinks.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
16 |
Link1
17 |
21 |
Link2
22 |
26 |
Link3
27 |
32 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/NavbarWithoutHamburger.html:
--------------------------------------------------------------------------------
1 |
2 |
7 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/Preview.html:
--------------------------------------------------------------------------------
1 |
2 | Hello MJML
3 |
4 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/Section.html:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
16 | |
17 |
18 |
19 |
20 |
21 |
26 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/SectionWithBackgroundColor.html:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
16 | |
17 |
18 |
19 |
20 |
21 |
25 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/SectionWithBackgroundImage.html:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
20 | |
21 |
22 |
23 |
24 |
25 |
26 |
33 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/SocialEmpty.html:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/SocialRaw.html:
--------------------------------------------------------------------------------
1 |
5 | Hello MJML
6 |
10 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/Spacer.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/SpacerWithHeight.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/Style.html:
--------------------------------------------------------------------------------
1 |
5 |
6 |
8 |
9 |
11 |
12 |
17 |
18 |
23 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/StyleInclude.html:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
9 |
10 |
12 |
13 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/StyleInline.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
10 |
11 |
13 |
14 |
16 |
17 |
18 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/StyleInline2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
10 |
11 |
13 |
14 |
16 |
17 |
18 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/StyleInline3.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
10 |
11 |
13 |
14 |
16 |
17 |
18 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/StyleInlineFallback.html:
--------------------------------------------------------------------------------
1 |
5 |
6 |
8 |
9 |
11 |
12 |
14 |
15 |
20 |
21 |
26 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/Table.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Year |
4 | Language |
5 | Inspired from |
6 |
7 |
8 | 1995 |
9 | PHP |
10 | C, Shell Unix |
11 |
12 |
13 | 1995 |
14 | JavaScript |
15 | Scheme, Self |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/TablePercent.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Year |
4 | Language |
5 | Inspired from |
6 |
7 |
8 | 1995 |
9 | PHP |
10 | C, Shell Unix |
11 |
12 |
13 | 1995 |
14 | JavaScript |
15 | Scheme, Self |
16 |
17 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/TablePixels.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Year |
4 | Language |
5 | Inspired from |
6 |
7 |
8 | 1995 |
9 | PHP |
10 | C, Shell Unix |
11 |
12 |
13 | 1995 |
14 | JavaScript |
15 | Scheme, Self |
16 |
17 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/Text.html:
--------------------------------------------------------------------------------
1 |
2 | Hello MJML
3 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/TextInclude.html:
--------------------------------------------------------------------------------
1 |
2 | Before Include
3 |
4 |
5 | Hello MJML
6 |
7 |
8 | After Include
9 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/TextRawWhitespace.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/TextWhitespace.html:
--------------------------------------------------------------------------------
1 |
2 | Hello MJML
3 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/TextWithEntity.html:
--------------------------------------------------------------------------------
1 |
2 | Hello ’MJML’
3 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/TextWithHtml.html:
--------------------------------------------------------------------------------
1 |
2 |
Hello MJML
3 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/TextWithHtml2.html:
--------------------------------------------------------------------------------
1 |
2 | Hello
MJML
3 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/TextWithHtmlAndWhitespace.html:
--------------------------------------------------------------------------------
1 |
This should respect whitespaces. after the HTML Tags
2 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/Title.html:
--------------------------------------------------------------------------------
1 |
2 | Hello MJML
3 |
--------------------------------------------------------------------------------
/Tests/Components/Outputs/Wrapper.html:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
16 |
17 |
23 |
24 |
29 | |
30 |
31 |
32 |
33 |
34 |
39 |
--------------------------------------------------------------------------------
/Tests/Components/PreviewTests.cs:
--------------------------------------------------------------------------------
1 | using Mjml.Net.Helpers;
2 | using Tests.Internal;
3 |
4 | namespace Tests.Components;
5 |
6 | public class PreviewTests
7 | {
8 | [Fact]
9 | public void Should_render_preview()
10 | {
11 | var source = @"
12 |
13 |
14 | Hello MJML
15 |
16 |
17 |
18 |
19 | ";
20 |
21 | var (result, _) = TestHelper.Render(source, helpers: [new PreviewHelper()]);
22 |
23 | AssertHelpers.HtmlFileAssert("Components.Outputs.Preview.html", result);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Tests/Components/RawTests.cs:
--------------------------------------------------------------------------------
1 | using Tests.Internal;
2 |
3 | namespace Tests.Components;
4 |
5 | public class RawTests
6 | {
7 | public static readonly TheoryData
Tests = new TheoryData
8 | {
9 | @"
10 | Hello MJML",
11 | @"
12 | Hello MJML",
13 | @"
14 | Hello MJML",
15 | @"
16 | Hello MJML Whats Up",
17 | @"
18 | HelloEntity Whats Up",
19 | @"
20 | ",
21 | @"
22 |
23 |
24 |
",
25 | @"
26 |
27 |
28 |
29 |
",
30 | @"
31 |
32 |
33 |
34 |
35 |
36 |
"
37 | };
38 |
39 | [Theory]
40 | [MemberData(nameof(Tests))]
41 | public void Should_render_raw_nested(string html)
42 | {
43 | var source = $@"{html}";
44 |
45 | var (result, _) = TestHelper.Render(source);
46 |
47 | AssertHelpers.HtmlAssert(html, result);
48 | }
49 |
50 | [Fact]
51 | public void Should_render_raw_with_entity()
52 | {
53 | var source = $@"<
";
54 |
55 | var (result, _) = TestHelper.Render(source);
56 |
57 | AssertHelpers.HtmlAssert("<
", result);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Tests/Components/SectionTests.cs:
--------------------------------------------------------------------------------
1 | using Tests.Internal;
2 |
3 | namespace Tests.Components;
4 |
5 | public class SectionTests
6 | {
7 | [Fact]
8 | public void Should_render_section()
9 | {
10 | var source = @"";
11 |
12 | var (result, _) = TestHelper.Render(source);
13 |
14 | AssertHelpers.HtmlFileAssert("Components.Outputs.Section.html", result);
15 | }
16 |
17 | [Fact]
18 | public void Should_render_section_with_background_color()
19 | {
20 | var source = @"";
21 |
22 | var (result, _) = TestHelper.Render(source);
23 |
24 | AssertHelpers.HtmlFileAssert("Components.Outputs.SectionWithBackgroundColor.html", result);
25 | }
26 |
27 | [Fact]
28 | public void Should_render_section_with_background_image()
29 | {
30 | var source = @"";
31 |
32 | var (result, _) = TestHelper.Render(source);
33 |
34 | AssertHelpers.HtmlFileAssert("Components.Outputs.SectionWithBackgroundImage.html", result);
35 | }
36 |
37 | [Fact]
38 | public void Should_render_sections_with_columns()
39 | {
40 | var source = @"
41 |
42 |
43 |
44 |
45 | ";
46 |
47 | var (result, _) = TestHelper.Render(source);
48 |
49 | AssertHelpers.HtmlFileAssert("Components.Outputs.SectionWithColumns.html", result);
50 | }
51 |
52 | [Fact]
53 | public void Should_render_sections_with_groups()
54 | {
55 | var source = @"
56 |
57 |
58 |
59 |
60 | ";
61 |
62 | var (result, _) = TestHelper.Render(source);
63 |
64 | AssertHelpers.HtmlFileAssert("Components.Outputs.SectionWithGroups.html", result);
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Tests/Components/SocialTests.cs:
--------------------------------------------------------------------------------
1 | using Tests.Internal;
2 |
3 | namespace Tests.Components;
4 |
5 | public class SocialTests
6 | {
7 | [Fact]
8 | public void Should_render_empty_social()
9 | {
10 | var source = @"";
11 |
12 | var (result, _) = TestHelper.Render(source);
13 |
14 | AssertHelpers.HtmlFileAssert("Components.Outputs.SocialEmpty.html", result);
15 | }
16 |
17 | [Fact]
18 | public void Should_render_raw_social()
19 | {
20 | var source = @"
21 |
22 |
23 | Hello MJML
24 |
25 |
26 | ";
27 |
28 | var (result, _) = TestHelper.Render(source);
29 |
30 | AssertHelpers.HtmlFileAssert("Components.Outputs.SocialRaw.html", result);
31 | }
32 |
33 | [Fact]
34 | public void Should_render_social()
35 | {
36 | var source = @"
37 |
38 |
39 | Facebook
40 |
41 |
42 | Google
43 |
44 |
45 | ";
46 |
47 | var (result, _) = TestHelper.Render(source);
48 |
49 | AssertHelpers.HtmlFileAssert("Components.Outputs.Social.html", result);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Tests/Components/SpacerTests.cs:
--------------------------------------------------------------------------------
1 | using Tests.Internal;
2 |
3 | namespace Tests.Components;
4 |
5 | public class SpacerTests
6 | {
7 | [Fact]
8 | public void Should_render_spacer()
9 | {
10 | var source = @"";
11 |
12 | var (result, _) = TestHelper.Render(source);
13 |
14 | AssertHelpers.HtmlFileAssert("Components.Outputs.Spacer.html", result);
15 | }
16 |
17 | [Fact]
18 | public void Should_render_inline_just_normal_as_fallback()
19 | {
20 | var source = @"";
21 |
22 | var (result, _) = TestHelper.Render(source);
23 |
24 | AssertHelpers.HtmlFileAssert("Components.Outputs.SpacerWithHeight.html", result);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Tests/Components/TextTests.cs:
--------------------------------------------------------------------------------
1 | using Mjml.Net;
2 | using Tests.Internal;
3 |
4 | namespace Tests.Components;
5 |
6 | public class TextTests
7 | {
8 | [Fact]
9 | public void Should_render_text()
10 | {
11 | var source = @"Hello MJML";
12 |
13 | var (result, _) = TestHelper.Render(source);
14 |
15 | AssertHelpers.HtmlFileAssert("Components.Outputs.Text.html", result);
16 | }
17 |
18 | [Fact]
19 | public void Should_render_text_with_whitespace()
20 | {
21 | var source = @"Hello MJML";
22 |
23 | var (result, _) = TestHelper.Render(source);
24 |
25 | AssertHelpers.HtmlFileAssert("Components.Outputs.TextWhitespace.html", result);
26 | }
27 |
28 | [Fact]
29 | public void Should_render_text_with_html()
30 | {
31 | var source = @"Hello MJML
";
32 |
33 | var (result, _) = TestHelper.Render(source);
34 |
35 | AssertHelpers.HtmlFileAssert("Components.Outputs.TextWithHtml.html", result);
36 | }
37 |
38 | [Fact]
39 | public void Should_render_text_with_html2()
40 | {
41 | var source = @"Hello
MJML";
42 |
43 | var (result, _) = TestHelper.Render(source);
44 |
45 | AssertHelpers.HtmlFileAssert("Components.Outputs.TextWithHtml2.html", result);
46 | }
47 |
48 | [Fact]
49 | public void Should_render_text_with_entity()
50 | {
51 | var source = @"Hello ’MJML’";
52 |
53 | var (result, _) = TestHelper.Render(source);
54 |
55 | AssertHelpers.HtmlFileAssert("Components.Outputs.TextWithEntity.html", result);
56 | }
57 |
58 | [Fact]
59 | public void Should_render_raw_text_with_whitespace()
60 | {
61 | var source = @"
62 |
63 | Hello MJML
64 | ";
65 |
66 | var (result, _) = TestHelper.Render(source);
67 |
68 | AssertHelpers.HtmlFileAssert("Components.Outputs.TextRawWhitespace.html", result);
69 | }
70 |
71 | [Fact]
72 | public void Should_render_text_with_html_and_whitespace()
73 | {
74 | var source = @"This should respect whitespaces. after the HTML Tags";
75 |
76 | var renderer = new MjmlRenderer().Add();
77 |
78 | var result = renderer.Render(source, new MjmlOptions()
79 | {
80 | Beautify = false,
81 | }).Html;
82 |
83 | var expected = TestHelper.GetContent("Components.Outputs.TextWithHtmlAndWhitespace.html");
84 |
85 | Assert.Equal(expected.Trim(), result.Trim());
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Tests/Components/TitleTests.cs:
--------------------------------------------------------------------------------
1 | using Mjml.Net.Helpers;
2 | using Tests.Internal;
3 |
4 | namespace Tests.Components;
5 |
6 | public class TitleTests
7 | {
8 | [Fact]
9 | public void Should_render_title()
10 | {
11 | var source = @"
12 |
13 |
14 | Hello MJML
15 |
16 |
17 |
18 |
19 | ";
20 |
21 | var (result, _) = TestHelper.Render(source, helpers: [new TitleHelper()]);
22 |
23 | AssertHelpers.HtmlFileAssert("Components.Outputs.Title.html", result);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Tests/Components/WrapperTests.cs:
--------------------------------------------------------------------------------
1 | using Tests.Internal;
2 |
3 | namespace Tests.Components;
4 |
5 | public class WrapperTests
6 | {
7 | [Fact]
8 | public void Should_render_wrapper()
9 | {
10 | var source = @"
11 |
12 |
13 |
14 | ";
15 |
16 | var (result, _) = TestHelper.Render(source);
17 |
18 | AssertHelpers.HtmlFileAssert("Components.Outputs.Wrapper.html", result);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Tests/HtmlExtensionsTests.cs:
--------------------------------------------------------------------------------
1 | using Mjml.Net;
2 | using Mjml.Net.Extensions;
3 | using Tests.Internal;
4 |
5 | namespace Tests;
6 |
7 | public class HtmlExtensionsTests
8 | {
9 | private readonly MjmlRenderContext sut = new MjmlRenderContext();
10 |
11 | public HtmlExtensionsTests()
12 | {
13 | var options = new MjmlOptions
14 | {
15 | Beautify = true
16 | };
17 |
18 | sut.Setup(new MjmlRenderer(), false, options);
19 | sut.StartBuffer();
20 | }
21 |
22 | [Fact]
23 | public void Should_suffix_single_class()
24 | {
25 | sut.StartElement("div")
26 | .Classes("class1", "outlook");
27 |
28 | AssertHelpers.MultilineText(sut,
29 | @""
30 | );
31 | }
32 |
33 | [Fact]
34 | public void Should_suffix_multiple_classes()
35 | {
36 | sut.StartElement("div")
37 | .Classes("class1 class2", "outlook");
38 |
39 | AssertHelpers.MultilineText(sut,
40 | @"
"
41 | );
42 | }
43 |
44 | [Fact]
45 | public void Should_suffix_multiple_classes2()
46 | {
47 | sut.StartElement("div")
48 | .Classes("class1 class2", "outlook");
49 |
50 | AssertHelpers.MultilineText(sut,
51 | @"
");
52 | }
53 |
54 | [Fact]
55 | public void Should_suffix_multiple_classes3()
56 | {
57 | sut.StartElement("div")
58 | .Classes(" class1 class2 ", "outlook");
59 |
60 | AssertHelpers.MultilineText(sut,
61 | @"
"
62 | );
63 | }
64 |
65 | [Fact]
66 | public void Should_suffix_no_classes()
67 | {
68 | sut.StartElement("div")
69 | .Classes(string.Empty, "outlook");
70 |
71 | AssertHelpers.MultilineText(sut,
72 | "
"
73 | );
74 | }
75 |
76 | [Fact]
77 | public void Should_suffix_no_suffix()
78 | {
79 | sut.StartElement("div")
80 | .Classes("class1 class2", string.Empty);
81 |
82 | AssertHelpers.MultilineText(sut,
83 | @"
"
84 | );
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/Tests/HtmlSpecialCaseTests.cs:
--------------------------------------------------------------------------------
1 | using Mjml.Net;
2 | using Tests.Internal;
3 |
4 | namespace Tests;
5 |
6 | public class HtmlSpecialCaseTests
7 | {
8 | [Fact]
9 | public void Should_ignore_extra_elements()
10 | {
11 | var source = @"
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Button
24 |
25 | ";
26 |
27 | var (result, _) = TestHelper.Render(source);
28 |
29 | AssertHelpers.HtmlFileAssert("Components.Outputs.Button.html", result);
30 | }
31 |
32 | [Fact]
33 | public void Should_expose_html_errors()
34 | {
35 | var source = $@"
36 | <>
37 | ";
38 |
39 | var result = TestHelper.Render(source);
40 |
41 | Assert.Contains(
42 | new ValidationError(
43 | "Unexpected character in stream.",
44 | ValidationErrorType.InvalidHtml,
45 | new SourcePosition(2, 3, null)),
46 | result.Errors);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Tests/IncludeTests.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using Mjml.Net;
3 | using Mjml.Net.Validators;
4 | using Tests.Internal;
5 |
6 | namespace Tests;
7 |
8 | public class IncludeTests
9 | {
10 | [Fact]
11 | public void Should_render()
12 | {
13 | var renderedNode = CompileWithNode();
14 | var renderedNet = CompileWithNet();
15 |
16 | AssertHelpers.HtmlAssert("include", renderedNet, renderedNode, true);
17 | }
18 |
19 | private static string CompileWithNet()
20 | {
21 | var source = File.ReadAllText($"Templates/include/about.mjml");
22 |
23 | var files = new Dictionary
();
24 |
25 | foreach (var file in Directory.GetFiles("Templates/include", "*.mjml", SearchOption.TopDirectoryOnly).Select(x => new FileInfo(x)))
26 | {
27 | files.Add(file.Name, File.ReadAllText(file.FullName));
28 | }
29 |
30 | var options = new MjmlOptions
31 | {
32 | FileLoader = () => new InMemoryFileLoader(files),
33 |
34 | // Easier for debugging errors.
35 | Beautify = true,
36 |
37 | // Use validation, so that we also catch errors here.
38 | Validator = StrictValidator.Instance
39 | };
40 |
41 | var (html, errors) = new MjmlRenderer().Render(source, options);
42 |
43 | Assert.Empty(errors.Where(x => x.Type != ValidationErrorType.UnknownAttribute));
44 |
45 | return html;
46 | }
47 |
48 | private static string CompileWithNode()
49 | {
50 | var tempFile = Guid.NewGuid().ToString();
51 |
52 | try
53 | {
54 | var process = new Process();
55 | process.StartInfo.UseShellExecute = true;
56 | process.StartInfo.FileName = "npx";
57 | process.StartInfo.Arguments = $"mjml Templates/include/about.mjml -o {tempFile}";
58 | process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
59 | process.Start();
60 | process.WaitForExit();
61 |
62 | return File.ReadAllText(tempFile);
63 | }
64 | finally
65 | {
66 | File.Delete(tempFile);
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/Tests/InnerTextOrHtmlTests.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 | using Mjml.Net;
3 |
4 | namespace Tests;
5 |
6 | public class InnerTextOrHtmlTests
7 | {
8 | [Fact]
9 | public void Should_append()
10 | {
11 | var sut = new InnerTextOrHtml();
12 |
13 | sut.Add("a");
14 | sut.Add("b");
15 | sut.Add("c");
16 |
17 | var sb = new StringBuilder();
18 | sut.AppendTo(sb);
19 |
20 | Assert.Equal("abc", sb.ToString());
21 | }
22 |
23 | [Fact]
24 | public void Should_append_trimmed()
25 | {
26 | var sut = new InnerTextOrHtml();
27 |
28 | sut.Add(" \n\rStart-");
29 | sut.Add("a");
30 | sut.Add("b");
31 | sut.Add("c");
32 | sut.Add("-End\n\r ");
33 |
34 | var sb = new StringBuilder();
35 | sut.AppendTo(sb);
36 |
37 | Assert.Equal("Start-abc-End", sb.ToString());
38 | }
39 |
40 | [Fact]
41 | public void Should_not_trim_in_between()
42 | {
43 | var sut = new InnerTextOrHtml();
44 |
45 | sut.Add(" \n\rStart-");
46 | sut.Add("a");
47 | sut.Add(" \n\r");
48 | sut.Add("b");
49 | sut.Add(" \n\r");
50 | sut.Add("c");
51 | sut.Add("-End\n\r ");
52 |
53 | var sb = new StringBuilder();
54 | sut.AppendTo(sb);
55 |
56 | Assert.Equal("Start-a \n\rb \n\rc-End", sb.ToString());
57 | }
58 |
59 | [Fact]
60 | public void Should_ignore_full_whitespace_starts_and_ends()
61 | {
62 | var sut = new InnerTextOrHtml();
63 |
64 | sut.Add(" \n\r");
65 | sut.Add(" \n\r");
66 | sut.Add(" \n\rStart-");
67 | sut.Add("a");
68 | sut.Add("b");
69 | sut.Add("c");
70 | sut.Add("-End\n\r ");
71 | sut.Add(" \n\r");
72 | sut.Add(" \n\r");
73 |
74 | var sb = new StringBuilder();
75 | sut.AppendTo(sb);
76 |
77 | Assert.Equal("Start-abc-End", sb.ToString());
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Tests/Internal/StaticIdGenerator.cs:
--------------------------------------------------------------------------------
1 | using Mjml.Net;
2 |
3 | namespace Tests.Internal;
4 |
5 | internal sealed class StaticIdGenerator : IIdGenerator
6 | {
7 | private readonly string[] values;
8 | private int position = -1;
9 |
10 | public StaticIdGenerator(params string[] values)
11 | {
12 | this.values = values;
13 | }
14 |
15 | public string Next()
16 | {
17 | position++;
18 |
19 | if (position == values.Length)
20 | {
21 | position = 0;
22 | }
23 |
24 | return values[position];
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Tests/Internal/TestComponent.cs:
--------------------------------------------------------------------------------
1 | using Mjml.Net;
2 | using Mjml.Net.Components;
3 |
4 | namespace Tests.Internal;
5 |
6 | public partial class TestComponent : Component
7 | {
8 | public override string ComponentName => "mjml-test";
9 |
10 | [Bind("head")]
11 | public string? Head;
12 |
13 | [Bind("body")]
14 | public string? Body;
15 |
16 | public override void Render(IHtmlRenderer renderer, GlobalContext context)
17 | {
18 | RenderChildren(renderer, context);
19 |
20 | if (Head != "false")
21 | {
22 | RenderHead(renderer, context);
23 | }
24 |
25 | if (Body != "false")
26 | {
27 | RenderBody(renderer, context);
28 | }
29 | }
30 |
31 | private static void RenderHead(IHtmlRenderer renderer, GlobalContext context)
32 | {
33 | renderer.RenderHelpers(HelperTarget.HeadStart);
34 |
35 | foreach (var (_, value) in context.GlobalData)
36 | {
37 | if (value is HeadBuffer head && head.Buffer != null)
38 | {
39 | // Already formatted properly.
40 | renderer.Plain(head.Buffer);
41 | renderer.ReturnStringBuilder(head.Buffer);
42 | }
43 | }
44 |
45 | renderer.RenderHelpers(HelperTarget.HeadEnd);
46 | }
47 |
48 | private static void RenderBody(IHtmlRenderer renderer, GlobalContext context)
49 | {
50 | renderer.RenderHelpers(HelperTarget.BodyStart);
51 |
52 | foreach (var (_, value) in context.GlobalData)
53 | {
54 | if (value is BodyBuffer body && body.Buffer != null)
55 | {
56 | // Already formatted properly.
57 | renderer.Plain(body.Buffer);
58 | renderer.ReturnStringBuilder(body.Buffer);
59 | }
60 | }
61 |
62 | renderer.RenderHelpers(HelperTarget.BodyEnd);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Tests/Internal/TestHelper.cs:
--------------------------------------------------------------------------------
1 | using System.Globalization;
2 | using Mjml.Net;
3 |
4 | namespace Tests.Internal;
5 |
6 | public static class TestHelper
7 | {
8 | public static RenderResult Render(string source, MjmlOptions? options = null, IHelper[]? helpers = null)
9 | {
10 | var renderer = CreateRenderer(helpers);
11 |
12 | return renderer.Render(source, BuildOptions(options));
13 | }
14 |
15 | public static async Task RenderAsync(string source, MjmlOptions? options = null, IHelper[]? helpers = null)
16 | {
17 | var renderer = CreateRenderer(helpers);
18 |
19 | return await renderer.RenderAsync(source, BuildOptions(options));
20 | }
21 |
22 | private static MjmlOptions BuildOptions(MjmlOptions? options)
23 | {
24 | options ??= new MjmlOptions();
25 |
26 | return options with
27 | {
28 | Beautify = true
29 | };
30 | }
31 |
32 | private static IMjmlRenderer CreateRenderer(IHelper[]? helpers)
33 | {
34 | var renderer =
35 | new MjmlRenderer()
36 | .AddList()
37 | .AddHtmlAttributes()
38 | .Add();
39 |
40 | if (helpers != null)
41 | {
42 | renderer.ClearHelpers();
43 |
44 | foreach (var helper in helpers)
45 | {
46 | renderer.Add(helper);
47 | }
48 | }
49 |
50 | return renderer;
51 | }
52 |
53 | public static string GetContent(string content)
54 | {
55 | var stream = typeof(TestHelper).Assembly.GetManifestResourceStream($"Tests.{content}")!;
56 |
57 | return new StreamReader(stream).ReadToEnd();
58 | }
59 |
60 | public static void TestWithCulture(string cultureCode, Action action)
61 | {
62 | var culture = CultureInfo.GetCultureInfo(cultureCode);
63 |
64 | var currentCulture = CultureInfo.CurrentCulture;
65 | var currentUICulture = CultureInfo.CurrentUICulture;
66 | try
67 | {
68 | CultureInfo.CurrentCulture = culture;
69 | CultureInfo.CurrentUICulture = culture;
70 |
71 | action();
72 | }
73 | finally
74 | {
75 | CultureInfo.CurrentCulture = currentCulture;
76 | CultureInfo.CurrentUICulture = currentUICulture;
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Tests/Templates/happy-new-year.mjml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | New dreams, new hopes, new experiences and new joys, we wish you all the best for this New Year to come in 2018!
17 |
19 |
20 |
21 |
22 |
23 | Simply created on Mailjet Passport
24 |
25 |
26 |
27 |
28 | [[DELIVERY_INFO]]
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/Tests/Templates/include/about.mjml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | This is the About!
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/Tests/Templates/include/footer.mjml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Cheers,
7 | Me!
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | Something.com ©️ 2023
20 |
21 |
22 |
--------------------------------------------------------------------------------
/Tests/Templates/include/header.mjml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/Tests/Templates/include/styling.mjml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | .bg-dark {
6 | background: #2F5854 !important;
7 | }
8 | .bg-white {
9 | background: #FFFFFF !important;
10 | }
11 | p {
12 | font-size: 16px !important;
13 | font-weight: 400 !important;
14 | line-height: 24px !important;
15 | margin: 0;
16 | }
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/Tests/Templates/proof.mjml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Article Title
14 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sit amet ipsum consequat.
15 | READ MORE
16 |
17 |
18 |
19 |
20 |
21 | Article Title
22 | Lorem ipsum dolor sit amet, consectetur adipiscing elit.
23 | READ MORE
24 |
25 |
26 |
27 | Article Title
28 | Lorem ipsum dolor sit amet, consectetur adipiscing elit.
29 | READ MORE
30 |
31 |
32 |
33 | Article Title
34 | Lorem ipsum dolor sit amet, consectetur adipiscing elit.
35 | READ MORE
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/Tests/Templates/racoon.mjml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SebastianStehle/mjml-net/d04bfff1eeb72479553a3f949f9169ce0a8dcd63/Tests/Templates/racoon.mjml
--------------------------------------------------------------------------------
/Tests/Templates/referral-email.mjml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | Hey {{FirstName}}!
19 | Are you enjoying our weekly newsletter?
Then why not share it with your friends?
20 | You'll get a 15% discount
21 | on your next order when a friend uses the code {{ReferalCode}}!
22 | Refer a friend now
23 | Best,
The {{CompanyName}} Team
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/Tests/Templates/welcome-email.mjml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Welcome aboard
11 |
12 |
13 |
14 |
15 | Dear [[FirstName]]
Welcome to [[CompanyName]].
16 | We're really excited you've decided to give us a try. In case you have any questions, feel free to reach out to us at [[ContactEmail]]. You can login to your account with your username [[UserName]]
17 | Login
18 | Thanks,
The [[CompanyName]] Team
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/Tests/Types/ColorTypeTests.cs:
--------------------------------------------------------------------------------
1 | using Mjml.Net;
2 |
3 | namespace Tests.Types;
4 |
5 | public class ColorTypeTests
6 | {
7 | [Theory]
8 | [InlineData("rgba(100, 100, 100, 0.5)")]
9 | [InlineData("rgba(100, 100, 100, 0.5) ")]
10 | [InlineData("rgb(100, 100, 100)")]
11 | [InlineData("rgb(100, 100, 100) ")]
12 | [InlineData("#FF00FF")]
13 | [InlineData("#ff00ff")]
14 | [InlineData("#ff00ff ")]
15 | [InlineData("#f0f")]
16 | [InlineData("#f0f ")]
17 | [InlineData("red")]
18 | [InlineData("red ")]
19 | public void Should_validate_valid_values(string value)
20 | {
21 | var context = default(ValidationContext);
22 |
23 | var isValid = AttributeTypes.Color.Validate(value, ref context);
24 |
25 | Assert.True(isValid);
26 | }
27 |
28 | [Theory]
29 | [InlineData("#ff00bb", "#f0b")]
30 | [InlineData("#ff00bb", "#f0b ")]
31 | public void Should_coerce_value(string expected, string value)
32 | {
33 | var result = AttributeTypes.Color.Coerce(value);
34 |
35 | Assert.Equal(expected, result);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Tests/Types/EnumTypeTests.cs:
--------------------------------------------------------------------------------
1 | using Mjml.Net;
2 | using Mjml.Net.Types;
3 |
4 | namespace Tests.Types;
5 |
6 | public class EnumTypeTests
7 | {
8 | [Theory]
9 | [InlineData("A")]
10 | [InlineData("B")]
11 | [InlineData("a")]
12 | [InlineData("b")]
13 | public void Should_validate_valid_values(string value)
14 | {
15 | var context = default(ValidationContext);
16 |
17 | var isValid = new EnumType(false, "A", "B").Validate(value, ref context);
18 |
19 | Assert.True(isValid);
20 | }
21 |
22 | [Theory]
23 | [InlineData("")]
24 | [InlineData(" ")]
25 | [InlineData(null)]
26 | public void Should_allow_empty_string_when_optional(string? value)
27 | {
28 | var context = default(ValidationContext);
29 |
30 | var isValid = new EnumType(true, "A", "B").Validate(value!, ref context);
31 |
32 | Assert.True(isValid);
33 | }
34 |
35 | [Theory]
36 | [InlineData("")]
37 | [InlineData(" ")]
38 | [InlineData("A ")]
39 | [InlineData("C")]
40 | [InlineData("c")]
41 | public void Should_validate_invalid_values(string value)
42 | {
43 | var context = default(ValidationContext);
44 |
45 | var isValid = new EnumType(false, "A", "B").Validate(value, ref context);
46 |
47 | Assert.False(isValid);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Tests/Types/ManyTypeTests.cs:
--------------------------------------------------------------------------------
1 | using Mjml.Net;
2 | using Mjml.Net.Types;
3 |
4 | namespace Tests.Types;
5 |
6 | public class ManyTypeTests
7 | {
8 | [Theory]
9 | [InlineData("0")]
10 | [InlineData("0 ")]
11 | [InlineData("10%")]
12 | [InlineData("10px")]
13 | [InlineData("10px 20px")]
14 | [InlineData("10px 20px 30px")]
15 | [InlineData("10px 20px 30px 40px")]
16 | [InlineData("10px 20px 30px 40px")]
17 | public void Should_validate_valid_values(string value)
18 | {
19 | var context = default(ValidationContext);
20 |
21 | var isValid = new ManyType(new NumberType(Unit.Percent, Unit.Pixels), 1, 4).Validate(value, ref context);
22 |
23 | Assert.True(isValid);
24 | }
25 |
26 | [Theory]
27 | [InlineData("")]
28 | [InlineData("2 px")]
29 | [InlineData("2 %")]
30 | [InlineData("0 rem")]
31 | [InlineData("0rem")]
32 | [InlineData("10 px ")]
33 | [InlineData("1px 2px 3px 4px 5px")]
34 | public void Should_validate_invalid_values(string value)
35 | {
36 | var context = default(ValidationContext);
37 |
38 | var isValid = new ManyType(new NumberType(Unit.Percent, Unit.Pixels), 1, 4).Validate(value, ref context);
39 |
40 | Assert.False(isValid);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Tests/Types/NumberTypeTests.cs:
--------------------------------------------------------------------------------
1 | using Mjml.Net;
2 | using Mjml.Net.Types;
3 |
4 | namespace Tests.Types;
5 |
6 | public class NumberTypeTests
7 | {
8 | [Theory]
9 | [InlineData("0")]
10 | [InlineData("0 ")]
11 | [InlineData("10%")]
12 | [InlineData("10px")]
13 | [InlineData("10% ")]
14 | [InlineData("10px ")]
15 | [InlineData(" 10%")]
16 | [InlineData(" 10px")]
17 | public void Should_validate_valid_values(string value)
18 | {
19 | var context = default(ValidationContext);
20 |
21 | var isValid = new NumberType(Unit.Percent, Unit.Pixels).Validate(value, ref context);
22 |
23 | Assert.True(isValid);
24 | }
25 |
26 | [Theory]
27 | [InlineData("")]
28 | [InlineData("2 px")]
29 | [InlineData("2 %")]
30 | [InlineData("0 rem")]
31 | [InlineData("0rem")]
32 | [InlineData("10 px ")]
33 | public void Should_validate_invalid_values(string value)
34 | {
35 | var context = default(ValidationContext);
36 |
37 | var isValid = new NumberType(Unit.Percent, Unit.Pixels).Validate(value, ref context);
38 |
39 | Assert.False(isValid);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Tests/Types/UnitParserTests.cs:
--------------------------------------------------------------------------------
1 | using Mjml.Net;
2 |
3 | namespace Tests.Types;
4 |
5 | public class UnitParserTests
6 | {
7 | [Theory]
8 | [InlineData(null)]
9 | [InlineData("")]
10 | [InlineData(" ")]
11 | public void Should_parse_empty(string? value)
12 | {
13 | var result = UnitParser.Parse(value);
14 |
15 | Assert.Equal((0, Unit.Unknown), result);
16 | }
17 |
18 | [Fact]
19 | public void Should_parse_with_default_unit()
20 | {
21 | var result = UnitParser.Parse("42", Unit.Pixels);
22 |
23 | Assert.Equal((42, Unit.Pixels), result);
24 | }
25 |
26 | [Fact]
27 | public void Should_floor_pixels()
28 | {
29 | var result = UnitParser.Parse("60.5px", Unit.Pixels);
30 |
31 | Assert.Equal((60, Unit.Pixels), result);
32 | }
33 |
34 | [Fact]
35 | public void Should_floor_pixels_with_default_unit()
36 | {
37 | var result = UnitParser.Parse("60.5px", Unit.Pixels);
38 |
39 | Assert.Equal((60, Unit.Pixels), result);
40 | }
41 |
42 | [Fact]
43 | public void Should_parse_without_unit()
44 | {
45 | var result = UnitParser.Parse("60.5");
46 |
47 | Assert.Equal((60.5, Unit.None), result);
48 | }
49 |
50 | [Fact]
51 | public void Should_parse_without_unit2()
52 | {
53 | var result = UnitParser.Parse("60.5 ");
54 |
55 | Assert.Equal((60.5, Unit.None), result);
56 | }
57 |
58 | [Fact]
59 | public void Should_parse_without_value()
60 | {
61 | var result = UnitParser.Parse("px");
62 |
63 | Assert.Equal((0, Unit.Pixels), result);
64 | }
65 |
66 | [Fact]
67 | public void Should_parse_without_invalid()
68 | {
69 | var result = UnitParser.Parse("invalid");
70 |
71 | Assert.Equal((0, Unit.Unknown), result);
72 | }
73 |
74 | [Theory]
75 | [InlineData("100px")]
76 | [InlineData("100px ")]
77 | public void Should_parse_with_pixels(string value)
78 | {
79 | var result = UnitParser.Parse(value);
80 |
81 | Assert.Equal((100, Unit.Pixels), result);
82 | }
83 |
84 | [Theory]
85 | [InlineData("54.3%")]
86 | [InlineData("54.3% ")]
87 | public void Should_parse_with_percentage(string value)
88 | {
89 | var result = UnitParser.Parse(value);
90 |
91 | Assert.Equal((54.3, Unit.Percent), result);
92 | }
93 |
94 | [Theory]
95 | [InlineData("100px solid black")]
96 | [InlineData("100px solid black ")]
97 | public void Should_parse_as_border(string value)
98 | {
99 | var result = UnitParser.Parse(value);
100 |
101 | Assert.Equal((100, Unit.Pixels), result);
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/Tools/Program.cs:
--------------------------------------------------------------------------------
1 | namespace Tools;
2 |
3 | public static class Program
4 | {
5 | public static void Main(string[] args)
6 | {
7 | MigrateCS.Run();
8 |
9 | ConvertJS.Run();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Tools/Tools.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net9.0
6 | enable
7 | false
8 | enable
9 | latest
10 |
11 |
12 |
13 |
14 | all
15 | runtime; build; native; contentfiles; analyzers; buildtransitive
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/stylecop.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
3 | "settings": {
4 | "orderingRules": {
5 | "usingDirectivesPlacement": "outsideNamespace"
6 | },
7 | "documentationRules": {
8 | "variables": {
9 | "licenseName": "MIT"
10 | },
11 | "xmlHeader": false
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------