├── .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 | /// <summary> 4 | /// Providers values from MJML. 5 | /// </summary> 6 | public interface IBinder 7 | { 8 | /// <summary> 9 | /// Get the attribute of the node with the given name. 10 | /// </summary> 11 | /// <param name="name">The name of the attribute.</param> 12 | /// <returns> 13 | /// The attribute of the node or null if not found. 14 | /// </returns> 15 | string? GetAttribute(string name); 16 | 17 | /// <summary> 18 | /// Gets the class names. 19 | /// </summary> 20 | string[] ClassNames { get; } 21 | 22 | /// <summary> 23 | /// Get the text content of the node. 24 | /// </summary> 25 | /// <returns>The content of the node or null if not found.</returns> 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<IComponent> 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 | /// <summary> 4 | /// Provides files for mj-include components. 5 | /// </summary> 6 | public interface IFileLoader 7 | { 8 | /// <summary> 9 | /// Loads the file as text from the specified path and usigng context object from the parent file. 10 | /// </summary> 11 | /// <param name="path">The path to the file.</param> 12 | /// <returns> 13 | /// The text of the file or null, if not found and an optional context. 14 | /// </returns> 15 | string? LoadText(string path); 16 | } 17 | -------------------------------------------------------------------------------- /Mjml.Net/IHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Mjml.Net; 2 | 3 | /// <summary> 4 | /// A helper is responsible to set global data to tag, which will be mainly the head tag. 5 | /// </summary> 6 | /// <remarks> 7 | /// Used for fonts and styles. 8 | /// </remarks> 9 | public interface IHelper 10 | { 11 | /// <summary> 12 | /// Renders the global data. 13 | /// </summary> 14 | /// <param name="renderer">The renderer.</param> 15 | /// <param name="target">The target where the helpers are rendered.</param> 16 | /// <param name="context">The context to render.</param> 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<HtmlError>? 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<char> NameAsSpan { get; } 20 | 21 | ReadOnlySpan<char> 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 | /// <summary> 4 | /// Generates IDs for HTML tags. 5 | /// </summary> 6 | public interface IIdGenerator 7 | { 8 | /// <summary> 9 | /// Generates a new ID and returns the result. 10 | /// </summary> 11 | /// <returns>The created ID.</returns> 12 | string Next(); 13 | } 14 | -------------------------------------------------------------------------------- /Mjml.Net/IMjmlReader.cs: -------------------------------------------------------------------------------- 1 | namespace Mjml.Net; 2 | 3 | /// <summary> 4 | /// Reads MJML fragments. 5 | /// </summary> 6 | public interface IMjmlReader 7 | { 8 | /// <summary> 9 | /// Read a xml fragment from a string. 10 | /// </summary> 11 | /// <param name="mjml">The mjml fragment reader.</param> 12 | /// <param name="file">The fle for debugging purposes.</param> 13 | /// <param name="parent">The parent component.</param> 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<string> 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 | /// <summary> 6 | /// Provides the files from an in memory store. 7 | /// </summary> 8 | /// <remarks> 9 | /// Useful for preloading. 10 | /// </remarks> 11 | public sealed class InMemoryFileLoader : IFileLoader 12 | { 13 | private readonly Stack<string> pathStack = new Stack<string>(); 14 | private readonly IReadOnlyDictionary<string, string> content; 15 | 16 | public InMemoryFileLoader(IReadOnlyDictionary<string, string> content) 17 | { 18 | this.content = content; 19 | } 20 | 21 | /// <inheritdoc /> 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<T1, T2, TReturn> CreateFactory<T1, T2, TReturn>(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<Func<T1, T2, TReturn>>(constructorExpression, parameters).Compile(); 20 | } 21 | 22 | public static Func<object, TReturn> CreateILDelegate<TReturn>(this MethodInfo methodInfo) 23 | { 24 | var parameterTypes = new Type[] 25 | { 26 | typeof(object) 27 | }; 28 | 29 | var method = new DynamicMethod(methodInfo.Name, typeof(TReturn), parameterTypes); 30 | 31 | var il = method.GetILGenerator(); 32 | 33 | il.Emit(OpCodes.Ldarg_S, 0); 34 | il.Emit(OpCodes.Call, methodInfo); 35 | il.Emit(OpCodes.Ret); 36 | 37 | return (Func<object, TReturn>)method.CreateDelegate(typeof(Func<object, TReturn>)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Mjml.Net/Internal/RenderStack.cs: -------------------------------------------------------------------------------- 1 | namespace Mjml.Net.Internal; 2 | 3 | internal sealed class RenderStack<T> 4 | { 5 | private readonly Stack<T> stack = new Stack<T>(10); 6 | 7 | public T? Current; 8 | 9 | public IEnumerable<T> Elements => stack; 10 | 11 | public void Push(T element) 12 | { 13 | Current = element; 14 | 15 | stack.Push(element); 16 | } 17 | 18 | public T? Pop() 19 | { 20 | if (stack.Count == 0) 21 | { 22 | return default; 23 | } 24 | 25 | var top = stack.Pop(); 26 | 27 | if (stack.Count > 0) 28 | { 29 | Current = stack.Peek(); 30 | } 31 | else 32 | { 33 | Current = default; 34 | } 35 | 36 | return top; 37 | } 38 | 39 | public void Clear() 40 | { 41 | stack.Clear(); 42 | 43 | Current = default; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Mjml.Net/Internal/SubtreeReader.cs: -------------------------------------------------------------------------------- 1 | using HtmlPerformanceKit; 2 | 3 | namespace Mjml.Net.Internal; 4 | 5 | internal sealed class SubtreeReader : HtmlReaderWrapper 6 | { 7 | private static readonly HashSet<string> VoidTags = new HashSet<string>(StringComparer.OrdinalIgnoreCase) 8 | { 9 | "area", 10 | "base", 11 | "br", 12 | "col", 13 | "embed", 14 | "hr", 15 | "img", 16 | "input", 17 | "link", 18 | "meta", 19 | "param", 20 | "source", 21 | "track", 22 | "wbr", 23 | }; 24 | 25 | private readonly HtmlReaderWrapper inner; 26 | private int depth = 1; 27 | 28 | public SubtreeReader(HtmlReaderWrapper inner) 29 | : base(inner.Impl) 30 | { 31 | this.inner = inner; 32 | } 33 | 34 | public override bool Read() 35 | { 36 | if (depth == 0) 37 | { 38 | return false; 39 | } 40 | 41 | var hasRead = inner.Read(); 42 | 43 | if (hasRead) 44 | { 45 | if (TokenKind == HtmlTokenKind.Tag && !VoidTags.Contains(inner.Name) && !inner.SelfClosingElement) 46 | { 47 | depth++; 48 | } 49 | else if (TokenKind == HtmlTokenKind.EndTag && !VoidTags.Contains(inner.Name)) 50 | { 51 | depth--; 52 | } 53 | } 54 | else 55 | { 56 | depth = 0; 57 | } 58 | 59 | return depth > 0; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Mjml.Net/Mjml.Net.csproj: -------------------------------------------------------------------------------- 1 | <Project Sdk="Microsoft.NET.Sdk"> 2 | 3 | <PropertyGroup> 4 | <TargetFrameworks>net6.0;net7.0;net8.0;net9.0</TargetFrameworks> 5 | <ImplicitUsings>enable</ImplicitUsings> 6 | <Nullable>enable</Nullable> 7 | <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> 8 | <GenerateDocumentationFile>true</GenerateDocumentationFile> 9 | <NoWarn>1591</NoWarn> 10 | <NeutralLanguage>en</NeutralLanguage> 11 | <LangVersion>latest</LangVersion> 12 | <PackageReadmeFile>README.md</PackageReadmeFile> 13 | </PropertyGroup> 14 | 15 | <ItemGroup> 16 | <PackageReference Include="HtmlPerformanceKit" Version="1.0.0" /> 17 | <PackageReference Include="Meziantou.Analyzer" Version="2.0.185"> 18 | <PrivateAssets>all</PrivateAssets> 19 | <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> 20 | </PackageReference> 21 | <PackageReference Include="Microsoft.Extensions.ObjectPool" Version="9.0.0" Condition="$(TargetFramework) == 'net9.0'" /> 22 | <PackageReference Include="Microsoft.Extensions.ObjectPool" Version="8.0.11" Condition="$(TargetFramework) != 'net9.0'" /> 23 | <PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0"> 24 | <PrivateAssets>all</PrivateAssets> 25 | <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> 26 | </PackageReference> 27 | <PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> 28 | <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" PrivateAssets="all" /> 29 | </ItemGroup> 30 | 31 | <ItemGroup> 32 | <ProjectReference Include="..\Mjml.Net.Generator\Mjml.Net.Generator.csproj" OutputItemType="Analyzer" SetTargetFramework="TargetFramework=netstandard2.0" ReferenceOutputAssembly="false" /> 33 | </ItemGroup> 34 | 35 | <ItemGroup> 36 | <AdditionalFiles Include="..\stylecop.json" Link="stylecop.json" /> 37 | </ItemGroup> 38 | 39 | <ItemGroup> 40 | <None Include="..\README.md"> 41 | <Pack>True</Pack> 42 | <PackagePath></PackagePath> 43 | </None> 44 | 45 | <None Include="icon.png"> 46 | <Pack>True</Pack> 47 | <PackagePath></PackagePath> 48 | </None> 49 | </ItemGroup> 50 | 51 | <ItemGroup> 52 | <Compile Update="Properties\Resources.Designer.cs"> 53 | <DesignTime>True</DesignTime> 54 | <AutoGen>True</AutoGen> 55 | <DependentUpon>Resources.resx</DependentUpon> 56 | </Compile> 57 | </ItemGroup> 58 | 59 | <ItemGroup> 60 | <EmbeddedResource Update="Properties\Resources.resx"> 61 | <Generator>ResXFileCodeGenerator</Generator> 62 | <LastGenOutput>Resources.Designer.cs</LastGenOutput> 63 | </EmbeddedResource> 64 | </ItemGroup> 65 | 66 | </Project> 67 | -------------------------------------------------------------------------------- /Mjml.Net/Properties/Assembly.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("Tests")] 4 | -------------------------------------------------------------------------------- /Mjml.Net/RenderResult.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable SA1313 // Parameter names should begin with lower-case letter 2 | 3 | namespace Mjml.Net; 4 | 5 | public sealed record RenderResult(string Html, ValidationErrors Errors) 6 | { 7 | } 8 | -------------------------------------------------------------------------------- /Mjml.Net/SourcePosition.cs: -------------------------------------------------------------------------------- 1 | namespace Mjml.Net; 2 | 3 | #pragma warning disable SA1313 // Parameter names should begin with lower-case letter 4 | 5 | public record struct SourcePosition( 6 | int LineNumber, 7 | int LinePosition, 8 | string? File); 9 | -------------------------------------------------------------------------------- /Mjml.Net/Types/EnumType.cs: -------------------------------------------------------------------------------- 1 | namespace Mjml.Net.Types; 2 | 3 | public class EnumType : IType 4 | { 5 | private readonly HashSet<string> allowedValues; 6 | private readonly bool isOptional; 7 | 8 | public bool IsOptional => isOptional; 9 | 10 | public IReadOnlySet<string> AllowedValues => allowedValues; 11 | 12 | public EnumType(bool isOptional, params string[] values) 13 | { 14 | allowedValues = new HashSet<string>(values, StringComparer.OrdinalIgnoreCase); 15 | 16 | this.isOptional = isOptional; 17 | } 18 | 19 | public bool Validate(string value, ref ValidationContext context) 20 | { 21 | if (string.IsNullOrWhiteSpace(value) && isOptional) 22 | { 23 | return true; 24 | } 25 | 26 | return allowedValues.Contains(value); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Mjml.Net/Types/ManyType.cs: -------------------------------------------------------------------------------- 1 | namespace Mjml.Net.Types; 2 | 3 | public sealed class ManyType : IType 4 | { 5 | private readonly IType unit; 6 | private readonly int min; 7 | private readonly int max; 8 | 9 | public IType Unit => unit; 10 | 11 | public int Min => min; 12 | 13 | public int Max => max; 14 | 15 | public ManyType(IType unit, int min, int max) 16 | { 17 | this.unit = unit; 18 | this.min = min; 19 | this.max = max; 20 | } 21 | 22 | public bool Validate(string value, ref ValidationContext context) 23 | { 24 | var parts = value.Split(' ', StringSplitOptions.RemoveEmptyEntries); 25 | 26 | if (parts.Length < min || parts.Length > max) 27 | { 28 | return false; 29 | } 30 | 31 | foreach (var part in parts) 32 | { 33 | if (!unit.Validate(part, ref context)) 34 | { 35 | return false; 36 | } 37 | } 38 | 39 | return true; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Mjml.Net/Types/NumberType.cs: -------------------------------------------------------------------------------- 1 | namespace Mjml.Net.Types; 2 | 3 | public sealed class NumberType : IType 4 | { 5 | private readonly Unit[] units; 6 | 7 | public IReadOnlyCollection<Unit> Units => units; 8 | 9 | public NumberType(params Unit[] units) 10 | { 11 | this.units = units; 12 | } 13 | 14 | public bool Validate(string value, ref ValidationContext context) 15 | { 16 | var trimmed = value.AsSpan().Trim(); 17 | 18 | if (trimmed.Length == 1 && trimmed[0] == '0') 19 | { 20 | return true; 21 | } 22 | 23 | var (_, unit) = UnitParser.Parse(value); 24 | 25 | return units.Contains(unit); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Mjml.Net/Types/OneOfType.cs: -------------------------------------------------------------------------------- 1 | namespace Mjml.Net.Types; 2 | 3 | public sealed class OneOfType : IType 4 | { 5 | private readonly List<IType> units; 6 | 7 | public IReadOnlyCollection<IType> Units => units; 8 | 9 | public OneOfType(params IType[] units) 10 | { 11 | this.units = [..units]; 12 | } 13 | 14 | public bool Validate(string value, ref ValidationContext context) 15 | { 16 | foreach (var unit in units) 17 | { 18 | if (unit.Validate(value, ref context)) 19 | { 20 | return true; 21 | } 22 | } 23 | 24 | return false; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Mjml.Net/Types/StringType.cs: -------------------------------------------------------------------------------- 1 | namespace Mjml.Net.Types; 2 | 3 | public sealed class StringType : IType 4 | { 5 | private readonly bool isRequired; 6 | 7 | public StringType(bool isRequired) 8 | { 9 | this.isRequired = isRequired; 10 | } 11 | 12 | public bool Validate(string value, ref ValidationContext context) 13 | { 14 | return !isRequired || !string.IsNullOrEmpty(value); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Mjml.Net/Unit.cs: -------------------------------------------------------------------------------- 1 | namespace Mjml.Net; 2 | 3 | public enum Unit 4 | { 5 | Unknown, 6 | Em, 7 | Pixels, 8 | Percent, 9 | Rem, 10 | None 11 | } 12 | -------------------------------------------------------------------------------- /Mjml.Net/UnitParser.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace Mjml.Net; 4 | 5 | public static class UnitParser 6 | { 7 | public static (double Value, Unit Unit) Parse(string? rawValue, Unit defaultUnit = Unit.None) 8 | { 9 | if (string.IsNullOrWhiteSpace(rawValue)) 10 | { 11 | return (0, Unit.Unknown); 12 | } 13 | 14 | var span = rawValue.AsSpan().Trim(); 15 | 16 | var hasSeparator = false; 17 | 18 | int i; 19 | for (i = 0; i < span.Length; i++) 20 | { 21 | var c = span[i]; 22 | 23 | if (c == '.' || c == ',') 24 | { 25 | hasSeparator = true; 26 | continue; 27 | } 28 | 29 | if (!char.IsNumber(c)) 30 | { 31 | break; 32 | } 33 | } 34 | 35 | var unitSpan = span[i..]; 36 | var unitType = Unit.Unknown; 37 | 38 | if (unitSpan.StartsWith("px", StringComparison.OrdinalIgnoreCase)) 39 | { 40 | unitType = Unit.Pixels; 41 | } 42 | else if (unitSpan.StartsWith("%", StringComparison.OrdinalIgnoreCase)) 43 | { 44 | unitType = Unit.Percent; 45 | } 46 | else if (unitSpan.StartsWith("em", StringComparison.OrdinalIgnoreCase)) 47 | { 48 | unitType = Unit.Em; 49 | } 50 | else if (unitSpan.StartsWith("rem", StringComparison.OrdinalIgnoreCase)) 51 | { 52 | unitType = Unit.Rem; 53 | } 54 | else if (unitSpan.Length == 0) 55 | { 56 | unitType = defaultUnit; 57 | } 58 | 59 | var valueSpan = span[..i]; 60 | 61 | if (valueSpan.Length == 0) 62 | { 63 | return (0, unitType); 64 | } 65 | 66 | if (hasSeparator) 67 | { 68 | double.TryParse(valueSpan, NumberStyles.Any, CultureInfo.InvariantCulture, out var temp); 69 | 70 | if (unitType == Unit.Pixels) 71 | { 72 | return ((int)temp, unitType); 73 | } 74 | 75 | return (temp, unitType); 76 | } 77 | else 78 | { 79 | int.TryParse(valueSpan, NumberStyles.Any, CultureInfo.InvariantCulture, out var temp); 80 | 81 | return (temp, unitType); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Mjml.Net/ValidationContext.cs: -------------------------------------------------------------------------------- 1 | namespace Mjml.Net; 2 | 3 | public struct ValidationContext 4 | { 5 | public MjmlOptions Options { get; set; } 6 | 7 | public SourcePosition Position { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /Mjml.Net/ValidationErrors.cs: -------------------------------------------------------------------------------- 1 | namespace Mjml.Net; 2 | 3 | #pragma warning disable SA1313 // Parameter names should begin with lower-case letter 4 | public sealed record ValidationError(string Error, ValidationErrorType Type, SourcePosition Position); 5 | #pragma warning restore SA1313 // Parameter names should begin with lower-case letter 6 | 7 | public enum ValidationErrorType 8 | { 9 | Other, 10 | InvalidAttribute, 11 | InvalidParent, 12 | UnknownAttribute, 13 | UnknownElement, 14 | UnexpectedText, 15 | InvalidHtml 16 | } 17 | 18 | public sealed class ValidationErrors : List<ValidationError> 19 | { 20 | public ValidationErrors() 21 | { 22 | } 23 | 24 | public ValidationErrors(IEnumerable<ValidationError> source) 25 | : base(source) 26 | { 27 | } 28 | 29 | public void Add(string error, ValidationErrorType type, SourcePosition position = default) 30 | { 31 | Add(new ValidationError(error, type, position)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Mjml.Net/Validators/SoftValidator.cs: -------------------------------------------------------------------------------- 1 | namespace Mjml.Net.Validators; 2 | 3 | public sealed class SoftValidator : ValidatorBase 4 | { 5 | public static readonly SoftValidator Instance = new SoftValidator(); 6 | 7 | private SoftValidator() 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Mjml.Net/Validators/StrictValidator.cs: -------------------------------------------------------------------------------- 1 | namespace Mjml.Net.Validators; 2 | 3 | public sealed class StrictValidator : ValidatorBase 4 | { 5 | public static readonly StrictValidator Instance = new StrictValidator(); 6 | 7 | private StrictValidator() 8 | { 9 | } 10 | 11 | public override void AttributeValue(string name, string value, IComponent component, IType type, ValidationErrors errors, ref ValidationContext context) 12 | { 13 | if (!type.Validate(value, ref context)) 14 | { 15 | errors.Add($"'{value}' is not a valid attribute '{name}' of '{component.ComponentName}'.", 16 | ValidationErrorType.InvalidAttribute, 17 | context.Position); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Mjml.Net/Validators/ValidatorBase.cs: -------------------------------------------------------------------------------- 1 | using Mjml.Net.Components; 2 | using Mjml.Net.Components.Body; 3 | 4 | namespace Mjml.Net.Validators; 5 | 6 | public abstract class ValidatorBase : IValidator 7 | { 8 | protected ValidatorBase() 9 | { 10 | } 11 | 12 | public void Attribute(string name, string value, IComponent component, ValidationErrors errors, ref ValidationContext context) 13 | { 14 | var allowedAttributes = component.AllowedFields; 15 | 16 | if (allowedAttributes == null) 17 | { 18 | return; 19 | } 20 | 21 | if (!allowedAttributes.TryGetValue(name, out var attribute)) 22 | { 23 | errors.Add($"'{name}' is not a valid attribute of '{component.ComponentName}'.", 24 | ValidationErrorType.UnknownAttribute, 25 | context.Position); 26 | } 27 | else 28 | { 29 | AttributeValue(name, value, component, attribute, errors, ref context); 30 | } 31 | } 32 | 33 | public virtual void AttributeValue(string name, string value, IComponent component, IType type, ValidationErrors errors, ref ValidationContext context) 34 | { 35 | } 36 | 37 | public void Components(IComponent root, ValidationErrors errors, ref ValidationContext context) 38 | { 39 | if (root is RootComponent rootComponent) 40 | { 41 | if (!rootComponent.ChildNodes.Any(x => x is BodyComponent)) 42 | { 43 | errors.Add("Document must have 'mj-body' tag.", ValidationErrorType.Other); 44 | } 45 | } 46 | else 47 | { 48 | errors.Add($"'{root.ComponentName}' cannot be the root tag.", 49 | ValidationErrorType.InvalidParent, 50 | root.Position); 51 | } 52 | 53 | void Validate(IComponent component, IComponent? parent) 54 | { 55 | if (parent != null && component.AllowedParents?.Contains(parent.ComponentName) == false) 56 | { 57 | errors.Add($"'{component.ComponentName}' must be child of '{string.Join(", ", component.AllowedParents)}', found '{parent.ComponentName}'.", 58 | ValidationErrorType.InvalidParent, 59 | component.Position); 60 | } 61 | 62 | foreach (var child in component.ChildNodes) 63 | { 64 | Validate(child, component); 65 | } 66 | } 67 | 68 | Validate(root, null); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Mjml.Net/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebastianStehle/mjml-net/d04bfff1eeb72479553a3f949f9169ce0a8dcd63/Mjml.Net/icon.png -------------------------------------------------------------------------------- /Tests/BugReportTests.cs: -------------------------------------------------------------------------------- 1 | using Mjml.Net; 2 | 3 | namespace Tests; 4 | 5 | public class BugReportTests 6 | { 7 | [Fact] 8 | public void Should_produce_deterministic_results() 9 | { 10 | var expected = RenderSample(); 11 | 12 | for (var i = 0; i < 10; i++) 13 | { 14 | var actual = RenderSample(); 15 | 16 | Assert.Equal(expected, actual); 17 | } 18 | 19 | static string RenderSample() 20 | { 21 | var source = @" 22 | <mj-raw> 23 | <!-- MJML-COMPONENT-START --> 24 | </mj-raw> 25 | <mj-section> 26 | <mj-column> 27 | <mj-button font-family=""Helvetica"" background-color=""#f45e43"" color=""white""> 28 | Don't click me! 29 | </mj-button> 30 | </mj-column> 31 | </mj-section> 32 | <mj-raw> 33 | <!-- MJML-COMPONENT-END --> 34 | </mj-raw> 35 | "; 36 | var (html, _) = new MjmlRenderer().Render(source); 37 | return html; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Tests/Components/AccordionTests.cs: -------------------------------------------------------------------------------- 1 | using Tests.Internal; 2 | 3 | namespace Tests.Components; 4 | 5 | public class AccordionTests 6 | { 7 | [Fact] 8 | public void Should_render_accordion() 9 | { 10 | var source = @" 11 | <mj-accordion> 12 | <mj-accordion-element> 13 | <mj-accordion-title>Why use an accordion?</mj-accordion-title> 14 | <mj-accordion-text> 15 | <span style=""line-height:20px""> 16 | Element1 17 | </span> 18 | </mj-accordion-text> 19 | </mj-accordion-element> 20 | <mj-accordion-element> 21 | <mj-accordion-title>How it works</mj-accordion-title> 22 | <mj-accordion-text> 23 | <span style=""line-height:20px""> 24 | Element2 25 | </span> 26 | </mj-accordion-text> 27 | </mj-accordion-element> 28 | </mj-accordion>"; 29 | 30 | var (result, _) = TestHelper.Render(source); 31 | 32 | AssertHelpers.HtmlFileAssert("Components.Outputs.Accordion.html", result); 33 | } 34 | 35 | [Fact] 36 | public void Should_render_accordion_with_empty_elements() 37 | { 38 | var source = @" 39 | <mj-accordion> 40 | <mj-accordion-element> 41 | </mj-accordion-element> 42 | <mj-accordion-element> 43 | </mj-accordion-element> 44 | </mj-accordion>"; 45 | 46 | var (result, _) = TestHelper.Render(source); 47 | 48 | AssertHelpers.HtmlFileAssert("Components.Outputs.AccordionEmptyElements.html", result); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Tests/Components/BodyTests.cs: -------------------------------------------------------------------------------- 1 | using Tests.Internal; 2 | 3 | namespace Tests.Components; 4 | 5 | public class BodyTests 6 | { 7 | [Fact] 8 | public void Should_render_body_only() 9 | { 10 | var source = @" 11 | <mjml> 12 | <mj-body> 13 | </mj-body> 14 | </mjml> 15 | "; 16 | 17 | var (result, _) = TestHelper.Render(source); 18 | 19 | Assert.Contains("</body>", result, StringComparison.OrdinalIgnoreCase); 20 | } 21 | 22 | [Fact] 23 | public void Should_add_background_to_body() 24 | { 25 | var source = @" 26 | <mjml> 27 | <mj-body background-color=""red""> 28 | </mj-body> 29 | </mjml> 30 | "; 31 | 32 | var (result, _) = TestHelper.Render(source); 33 | 34 | Assert.Contains(@"<body style=""background-color:red;word-spacing:normal;"">", 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 | <mj-button font-family=""Helvetica"" background-color=""#f45e43"" color=""white""> 12 | Button 13 | </mj-button> 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 | <mj-button href=""https://mjml.io/"" font-family=""Helvetica"" background-color=""#f45e43"" color=""white""> 26 | Button Link 27 | </mj-button> 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 | <mj-button href=""https://mjml.io/"" font-family=""Helvetica"" background-color=""#f45e43"" color=""white"" rel=""relly good""> 40 | Button Link 41 | </mj-button> 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 | <mj-button> 54 | <strong>Hello</strong> MJML 55 | </mj-button> 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 | <mj-button> 68 | Hello <strong>MJML</strong> 69 | </mj-button> 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 | <mj-button width=""500""> 82 | Button 83 | </mj-button> 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 | <mjml-test head=""false""> 13 | <!-- COMMENT 1 --> 14 | <mj-spacer /> 15 | <!-- COMMENT 2 --> 16 | <mj-spacer /> 17 | <!-- COMMENT 3 --> 18 | </mjml-test> 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 = @"<mj-divider />"; 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 = @"<mj-divider width=""500""></mj-divider>"; 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 | <mjml-test body=""false""> 13 | <mj-head> 14 | <mj-font name=""Raleway"" href=""https://fonts.googleapis.com/css?family=Raleway"" /> 15 | </mj-head> 16 | <mj-body> 17 | </mj-body> 18 | </mjml-test> 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 | <mjml-test body=""false""> 31 | <mj-body> 32 | <mj-text font-family=""Ubuntu""></mj-text> 33 | </mj-body> 34 | </mjml-test> 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 | <mjml-test body=""false""> 47 | <mj-head> 48 | <mj-font name=""Ubuntu"" href=""https://fonts.googleapis.com/css?family=Ubuntu:300,400"" /> 49 | </mj-head> 50 | <mj-body> 51 | <mj-text font-family=""Ubuntu""></mj-text> 52 | </mj-body> 53 | </mjml-test> 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 | <mjml-test head=""false""> 12 | <mj-group> 13 | <mj-spacer /> 14 | </mj-group> 15 | <mj-group> 16 | <mj-spacer /> 17 | </mj-group> 18 | </mjml-test> 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 | <mjml-test head=""false""> 31 | <mj-group> 32 | <mj-column width=""100px""></mj-column> 33 | <mj-column></mj-column> 34 | </mj-group> 35 | </mjml-test> 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 | <mj-hero> 12 | </mj-hero> 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 | <mj-hero width=""500""> 25 | </mj-hero> 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 | <mj-hero> 38 | <mj-divider /> 39 | </mj-hero> 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 | <mj-hero> 52 | <mj-divider /> 53 | <mj-divider /> 54 | </mj-hero> 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 = @"<mj-image width=""300px"" src=""https://www.online-image-editor.com//styles/2014/images/example_image.png"" />"; 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 = @"<mj-image width=""300px"" src=""https://www.online-image-editor.com//styles/2014/images/example_image.png"" href=""link/to/website"" />"; 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 | <mj-list> 12 | <mj-li>List item one.</mj-li> 13 | <mj-li>List item two.</mj-li> 14 | <mj-li>List item three.</mj-li> 15 | <mj-li>List item four.</mj-li> 16 | </mj-list>"; 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 | <mj-msobutton font-family=""Helvetica"" background-color=""#f45e43"" color=""white""> 12 | Button 13 | </mj-msobutton> 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 | <mj-msobutton mso-proof=""true"" font-family=""Helvetica"" background-color=""#f45e43"" color=""white""> 26 | Button 27 | </mj-msobutton> 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 | <mj-msobutton mso-proof=""true"" height=""48px"" width=""252px"" border=""2px dashed #1f2153"" background-color=""none"" border-radius=""22px"" color=""black""> 40 | Reset Password 41 | </mj-msobutton> 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 = @"<mj-navbar hamburger=""hamburger""></mj-navbar>"; 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 = @"<mj-navbar></mj-navbar>"; 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 | <mj-navbar base-url=""https://mjml.io"" hamburger=""hamburger"" ico-color=""black""> 39 | <mj-navbar-link href=""/link1"" color=""#ff00ff"">Link1</mj-navbar-link> 40 | <mj-navbar-link href=""/link2"" color=""#ff0000"">Link2</mj-navbar-link> 41 | <mj-navbar-link href=""/link3"" color=""#0000ff"">Link3</mj-navbar-link> 42 | </mj-navbar>"; 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 | <style type="text/css"> 2 | @media only screen and (min-width:300px) { 3 | .mj-column-per-100 { 4 | width: 100% !important; 5 | max-width: 100%; 6 | } 7 | } 8 | </style> 9 | <style media="screen and (min-width:300px)"> 10 | .moz-text-html .mj-column-per-100 { 11 | width: 100% !important; 12 | max-width: 100%; 13 | } 14 | </style> -------------------------------------------------------------------------------- /Tests/Components/Outputs/Button.html: -------------------------------------------------------------------------------- 1 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;"> 2 | <tbody> 3 | <tr> 4 | <td align="center" bgcolor="#f45e43" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#f45e43;" valign="middle"> 5 | <p style="display:inline-block;background:#f45e43;color:white;font-family:Helvetica;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"> Button </p> 6 | </td> 7 | </tr> 8 | </tbody> 9 | </table> -------------------------------------------------------------------------------- /Tests/Components/Outputs/ButtonLink.html: -------------------------------------------------------------------------------- 1 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;"> 2 | <tbody> 3 | <tr> 4 | <td align="center" bgcolor="#f45e43" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#f45e43;" valign="middle"> 5 | <a href="https://mjml.io/" style="display:inline-block;background:#f45e43;color:white;font-family:Helvetica;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;" target="_blank"> Button Link </a> 6 | </td> 7 | </tr> 8 | </tbody> 9 | </table> -------------------------------------------------------------------------------- /Tests/Components/Outputs/ButtonLinkWithRel.html: -------------------------------------------------------------------------------- 1 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;"> 2 | <tbody> 3 | <tr> 4 | <td align="center" bgcolor="#f45e43" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#f45e43;" valign="middle"> 5 | <a href="https://mjml.io/" rel="relly good" style="display:inline-block;background:#f45e43;color:white;font-family:Helvetica;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;" target="_blank"> Button Link </a> 6 | </td> 7 | </tr> 8 | </tbody> 9 | </table> -------------------------------------------------------------------------------- /Tests/Components/Outputs/ButtonMixedContent.html: -------------------------------------------------------------------------------- 1 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;"> 2 | <tbody> 3 | <tr> 4 | <td align="center" bgcolor="#414141" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#414141;" valign="middle"> 5 | <p style="display:inline-block;background:#414141;color:#ffffff;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"> 6 | <strong>Hello</strong> MJML 7 | </p> 8 | </td> 9 | </tr> 10 | </tbody> 11 | </table> -------------------------------------------------------------------------------- /Tests/Components/Outputs/ButtonMixedContent2.html: -------------------------------------------------------------------------------- 1 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;"> 2 | <tbody> 3 | <tr> 4 | <td align="center" bgcolor="#414141" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#414141;" valign="middle"> 5 | <p style="display:inline-block;background:#414141;color:#ffffff;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"> 6 | Hello <strong>MJML</strong> 7 | </p> 8 | </td> 9 | </tr> 10 | </tbody> 11 | </table> -------------------------------------------------------------------------------- /Tests/Components/Outputs/ButtonWithoutWidthUnit.html: -------------------------------------------------------------------------------- 1 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;width:500;line-height:100%;"> 2 | <tr> 3 | <td align="center" bgcolor="#414141" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#414141;" valign="middle"> 4 | <p style="display:inline-block;width:450px;background:#414141;color:#ffffff;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;"> Button </p> 5 | </td> 6 | </tr> 7 | </table> -------------------------------------------------------------------------------- /Tests/Components/Outputs/ChildClasses.html: -------------------------------------------------------------------------------- 1 | <div> 2 | <!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" ><tr><td><![endif]--> 3 | <table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;"> 4 | <tr> 5 | <td style="padding:0 10px 0 0;vertical-align:middle;"> 6 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:200px;"> 7 | <tr> 8 | <td style="font-size:0;height:200px;vertical-align:middle;width:200px;"> 9 | <a href="#" target="_blank"> 10 | <img alt="christmas tree" height="200" src="https://cdn.pixabay.com/photo/2023/12/14/20/24/christmas-balls-8449615_1280.jpg" style="border-radius:3px;display:block;" width="200" /> 11 | </a> 12 | </td> 13 | </tr> 14 | </table> 15 | </td> 16 | <td style="vertical-align:middle;padding:4px 4px 4px 0;"> 17 | <a href="#" style="color:#333333;font-size:13px;font-family:Ubuntu, Helvetica, Arial, sans-serif;line-height:22px;text-decoration:none;" target="_blank"> This image is not displayed when complied with mjml-net </a> 18 | </td> 19 | </tr> 20 | </table> 21 | <!--[if mso | IE]></td></tr></table><![endif]--> 22 | </div> -------------------------------------------------------------------------------- /Tests/Components/Outputs/ColumnClass.html: -------------------------------------------------------------------------------- 1 | <div class="mj-column-per-50 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"> 2 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"> 3 | <tbody> 4 | <tr> 5 | <td align="left" class="test" style="font-size:0px;padding:10px 25px;word-break:break-word;"> 6 | <table cellpadding="0" cellspacing="0" width="100%" border="0" style="color:#000000;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:22px;table-layout:auto;width:100%;border:none;"> 7 | </table> 8 | </td> 9 | </tr> 10 | </tbody> 11 | </table> 12 | </div> -------------------------------------------------------------------------------- /Tests/Components/Outputs/ColumnFour.html: -------------------------------------------------------------------------------- 1 | <div class="mj-column-per-25 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"> 2 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" 3 | style="background-color:red;vertical-align:top;" width="100%"> 4 | <tbody> 5 | </tbody> 6 | </table> 7 | </div> 8 | <div class="mj-column-per-25 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"> 9 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" 10 | style="background-color:green;vertical-align:top;" width="100%"> 11 | <tbody> 12 | </tbody> 13 | </table> 14 | </div> 15 | <div class="mj-column-per-25 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"> 16 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" 17 | style="background-color:blue;vertical-align:top;" width="100%"> 18 | <tbody> 19 | </tbody> 20 | </table> 21 | </div> 22 | <div class="mj-column-per-25 mj-outlook-group-fix" style="font-size: 0px; text-align: left; direction: ltr; display: inline-block; vertical-align: top; width: 100%;"> 23 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" 24 | style="background-color:yellow;vertical-align:top;" width="100%"> 25 | <tbody> 26 | </tbody> 27 | </table> 28 | </div> -------------------------------------------------------------------------------- /Tests/Components/Outputs/ColumnOne.html: -------------------------------------------------------------------------------- 1 | <div class="mj-column-per-100 mj-outlook-group-fix" 2 | style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"> 3 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" 4 | style="background-color:red;vertical-align:top;" width="100%"> 5 | <tbody> 6 | </tbody> 7 | </table> 8 | </div> 9 | -------------------------------------------------------------------------------- /Tests/Components/Outputs/ColumnOneWithInnerBorder.html: -------------------------------------------------------------------------------- 1 | <div class="mj-column-per-100 mj-outlook-group-fix" style="direction:ltr;display:inline-block;font-size:0px;text-align:left;vertical-align:top;width:100%;"> 2 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%"> 3 | <tbody> 4 | <tr> 5 | <td style="padding:0;padding-bottom:0;padding-left:0;padding-right:0;padding-top:0;vertical-align:top;"> 6 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%" style="border:1px solid red;border-bottom:1px solid red;border-left:1px solid red;border-right:1px solid red;border-top:1px solid red;"> 7 | <tbody> 8 | <tr> 9 | <td align="left" style="font-size:0px;padding:10px 25px;padding-bottom:10px;padding-left:25px;padding-right:25px;padding-top:10px;word-break:break-word;"> 10 | <div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1;text-align:left;color:#000000;"> 11 | Hello World! 12 | </div> 13 | </td> 14 | </tr> 15 | </tbody> 16 | </table> 17 | </td> 18 | </tr> 19 | </tbody> 20 | </table> 21 | </div> 22 | -------------------------------------------------------------------------------- /Tests/Components/Outputs/ColumnOneWithPadding.html: -------------------------------------------------------------------------------- 1 | <div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"> 2 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%"> 3 | <tbody> 4 | <tr> 5 | <td style="background-color:red;vertical-align:top;padding:20px 52px;"> 6 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="" width="100%"> 7 | <tbody> 8 | </tbody> 9 | </table> 10 | </td> 11 | </tr> 12 | </tbody> 13 | </table> 14 | </div> -------------------------------------------------------------------------------- /Tests/Components/Outputs/ColumnThree.html: -------------------------------------------------------------------------------- 1 | <div class="mj-column-per-33-333333333333336 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"> 2 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="background-color:red;vertical-align:top;" width="100%"> 3 | <tbody> 4 | </tbody> 5 | </table> 6 | </div> 7 | <div class="mj-column-per-33-333333333333336 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"> 8 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="background-color:green;vertical-align:top;" width="100%"> 9 | <tbody> 10 | </tbody> 11 | </table> 12 | </div> 13 | <div class="mj-column-per-33-333333333333336 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"> 14 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="background-color:blue;vertical-align:top;" width="100%"> 15 | <tbody> 16 | </tbody> 17 | </table> 18 | </div> -------------------------------------------------------------------------------- /Tests/Components/Outputs/ColumnTwo.html: -------------------------------------------------------------------------------- 1 | <div class="mj-column-per-50 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"> 2 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="background-color:red;vertical-align:top;" width="100%"> 3 | <tbody> 4 | </tbody> 5 | </table> 6 | </div> 7 | <div class="mj-column-per-50 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;"> 8 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="background-color:green;vertical-align:top;" width="100%"> 9 | <tbody> 10 | </tbody> 11 | </table> 12 | </div> -------------------------------------------------------------------------------- /Tests/Components/Outputs/Comments.html: -------------------------------------------------------------------------------- 1 | <!-- COMMENT 1 --> 2 | <div style="height:20px;line-height:20px;"> </div> 3 | <!-- COMMENT 2 --> 4 | <div style="height:20px;line-height:20px;"> </div> 5 | <!-- COMMENT 3 --> -------------------------------------------------------------------------------- /Tests/Components/Outputs/Divider.html: -------------------------------------------------------------------------------- 1 | <p style="border-top:solid 4px #000000;font-size:1px;margin:0px auto;width:100%;"> 2 | </p> 3 | <!--[if mso | IE]> 4 | <table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 4px #000000;font-size:1px;margin:0px auto;width:550px;" role="presentation" width="550px" > 5 | <tr> 6 | <td style="height:0;line-height:0;"> 7 |   8 | </td> 9 | </tr> 10 | </table> 11 | <![endif]--> 12 | -------------------------------------------------------------------------------- /Tests/Components/Outputs/DividerWithoutWidthUnit.html: -------------------------------------------------------------------------------- 1 | <p style="border-top:solid 4px #000000;font-size:1px;margin:0px auto;width:500;"></p> 2 | <!--[if mso | IE]> 3 | <table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 4px #000000;font-size:1px;margin:0px auto;width:500px;" role="presentation" width="500px" > 4 | <tr> 5 | <td style="height:0;line-height:0;">  </td> 6 | </tr> 7 | </table> 8 | <![endif]--> 9 | -------------------------------------------------------------------------------- /Tests/Components/Outputs/Font.html: -------------------------------------------------------------------------------- 1 | <!--[if !mso]><!--> 2 | <link href="https://fonts.googleapis.com/css?family=Raleway" rel="stylesheet" type="text/css"> 3 | <style type="text/css"> 4 | @import url(https://fonts.googleapis.com/css?family=Raleway); 5 | </style> 6 | <!--<![endif]--> -------------------------------------------------------------------------------- /Tests/Components/Outputs/FontUbuntu.html: -------------------------------------------------------------------------------- 1 | <!--[if !mso]><!--> 2 | <link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css"> 3 | <style type="text/css"> 4 | @import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700); 5 | </style> 6 | <!--<![endif]--> -------------------------------------------------------------------------------- /Tests/Components/Outputs/FontUbuntu2.html: -------------------------------------------------------------------------------- 1 | <!--[if !mso]><!--> 2 | <link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400" rel="stylesheet" type="text/css"> 3 | <style type="text/css"> 4 | @import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400); 5 | </style> 6 | <!--<![endif]--> -------------------------------------------------------------------------------- /Tests/Components/Outputs/Group.html: -------------------------------------------------------------------------------- 1 | <div class="mj-column-per-50 mj-outlook-group-fix" style="font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;"> 2 | <!--[if mso | IE]> 3 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" > 4 | <tr> 5 | <td style="width:300px;" > 6 | <![endif]--> 7 | <div style="height:20px;line-height:20px;"> </div> 8 | <!--[if mso | IE]> 9 | </td> 10 | </tr> 11 | </table> 12 | <![endif]--> 13 | </div> 14 | <div class="mj-column-per-50 mj-outlook-group-fix" style="font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;"> 15 | <!--[if mso | IE]> 16 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" > 17 | <tr> 18 | <td style="width:300px;" > 19 | <![endif]--> 20 | <div style="height:20px;line-height:20px;"> </div> 21 | <!--[if mso | IE]> 22 | </td> 23 | </tr> 24 | </table> 25 | <![endif]--> 26 | </div> -------------------------------------------------------------------------------- /Tests/Components/Outputs/GroupWithColumns.html: -------------------------------------------------------------------------------- 1 | <div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0;line-height:0;text-align:left;display:inline-block;width:100%;direction:ltr;"> 2 | <!--[if mso | IE]> 3 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" > 4 | <tr> 5 | <td style="vertical-align:top;width:100px;" > 6 | <![endif]--> 7 | <div class="mj-column-px-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100px;"> 8 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"> 9 | <tbody> 10 | </tbody> 11 | </table> 12 | </div> 13 | <!--[if mso | IE]> 14 | </td> 15 | <td style="vertical-align:top;width:300px;" > 16 | <![endif]--> 17 | <div class="mj-column-per-50 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:50%;"> 18 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%"> 19 | <tbody> 20 | </tbody> 21 | </table> 22 | </div> 23 | <!--[if mso | IE]> 24 | </td> 25 | </tr> 26 | </table> 27 | <![endif]--> 28 | </div> -------------------------------------------------------------------------------- /Tests/Components/Outputs/Hero.html: -------------------------------------------------------------------------------- 1 | <!--[if mso | IE]> 2 | <table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:600px;" width="600" > 3 | <tr> 4 | <td style="line-height:0;font-size:0;mso-line-height-rule:exactly;"> 5 | <v:image style="border:0;mso-position-horizontal:center;position:absolute;top:0;width:600px;z-index:-3;" xmlns:v="urn:schemas-microsoft-com:vml" /> 6 | <![endif]--> 7 | <div style="margin:0 auto;max-width:600px;"> 8 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;"> 9 | <tbody> 10 | <tr style="vertical-align:top;"> 11 | <td style="background:#ffffff;background-position:center center;background-repeat:no-repeat;padding:0px;vertical-align:top;" height="0"> 12 | <!--[if mso | IE]> 13 | <table border="0" cellpadding="0" cellspacing="0" style="width:600px;" width="600" > 14 | <tr> 15 | <td style=""> 16 | <![endif]--> 17 | <div class="mj-hero-content" style="margin:0px auto;"> 18 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;margin:0px;"> 19 | <tbody> 20 | <tr> 21 | <td style=""> 22 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;margin:0px;"> 23 | <tbody> 24 | </tbody> 25 | </table> 26 | </td> 27 | </tr> 28 | </tbody> 29 | </table> 30 | </div> 31 | <!--[if mso | IE]> 32 | </td> 33 | </tr> 34 | </table> 35 | <![endif]--> 36 | </td> 37 | </tr> 38 | </tbody> 39 | </table> 40 | </div> 41 | <!--[if mso | IE]> 42 | </td> 43 | </tr> 44 | </table> 45 | <![endif]--> 46 | -------------------------------------------------------------------------------- /Tests/Components/Outputs/HtmlAttributeInvalid.html: -------------------------------------------------------------------------------- 1 | <html> 2 | <head> 3 | </head> 4 | <body> 5 | 6 | <div> 7 | <div class="custom"> 8 | <div></div> 9 | </div> 10 | </div> 11 | 12 | </body> 13 | </html> -------------------------------------------------------------------------------- /Tests/Components/Outputs/HtmlAttributes.html: -------------------------------------------------------------------------------- 1 | <html> 2 | <head> 3 | </head> 4 | <body> 5 | 6 | <div> 7 | <div class="custom"> 8 | <div data-id="42"></div> 9 | </div> 10 | </div> 11 | 12 | </body> 13 | </html> -------------------------------------------------------------------------------- /Tests/Components/Outputs/HtmlAttributesNoProcessor.html: -------------------------------------------------------------------------------- 1 | <div> 2 | <div class="custom"> 3 | <div></div> 4 | </div> 5 | </div> 6 | 7 | -------------------------------------------------------------------------------- /Tests/Components/Outputs/Image.html: -------------------------------------------------------------------------------- 1 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;"> 2 | <tbody> 3 | <tr> 4 | <td style="width:300px;"> 5 | <img height="auto" src="https://www.online-image-editor.com//styles/2014/images/example_image.png" width="300" style="border:0;display:block;font-size:13px;height:auto;outline:none;text-decoration:none;width:100%;" /> 6 | </td> 7 | </tr> 8 | </tbody> 9 | </table> -------------------------------------------------------------------------------- /Tests/Components/Outputs/ImageWithLink.html: -------------------------------------------------------------------------------- 1 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;"> 2 | <tbody> 3 | <tr> 4 | <td style="width:300px;"> 5 | <a href="link/to/website" target="_blank"> 6 | <img height="auto" src="https://www.online-image-editor.com//styles/2014/images/example_image.png" width="300" style="border:0;display:block;font-size:13px;height:auto;outline:none;text-decoration:none;width:100%;" /> 7 | </a> 8 | </td> 9 | </tr> 10 | </tbody> 11 | </table> -------------------------------------------------------------------------------- /Tests/Components/Outputs/MsoButton.html: -------------------------------------------------------------------------------- 1 | <!--[if mso]> 2 | <div> 3 | <v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" fill="t" strokeweight="0pt" strokecolor="#000000" stroked="f" arcsize="8%" style="padding:10px 25px;v-text-anchor:middle;"> 4 | <v:fill color="#f45e43"/> 5 | <w:anchorlock/> 6 | <center> 7 | <p style="background:#f45e43;border-radius:3px;color:white;display:inline-block;font-family:Helvetica;font-size:13px;font-weight:normal;line-height:120%;margin:0;mso-padding-alt:0px;padding:10px 25px;text-decoration:none;text-transform:none;"> 8 | Button 9 | </p> 10 | </center> 11 | </v:roundrect> 12 | </div> 13 | <![endif]--> 14 | <!--[if !mso]><!--> 15 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;"> 16 | <tr> 17 | <td align="center" bgcolor="#f45e43" role="presentation" valign="middle" style="border:none;border-bottom:none;border-left:none;border-right:none;border-top:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#f45e43;"> 18 | <p style="background:#f45e43;border-radius:3px;color:white;display:inline-block;font-family:Helvetica;font-size:13px;font-weight:normal;line-height:120%;margin:0;mso-padding-alt:0px;padding:10px 25px;text-decoration:none;text-transform:none;"> 19 | Button 20 | </p> 21 | </td> 22 | </tr> 23 | </table> 24 | <!--<![endif]--> 25 | -------------------------------------------------------------------------------- /Tests/Components/Outputs/MsoButtonWithBorder.html: -------------------------------------------------------------------------------- 1 | <!--[if mso]> 2 | <div> 3 | <v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" fill="f" strokeweight="2px" strokecolor="#1f2153" stroked="t" arcsize="46%" style="height:48px;width:252px;padding:10px 25px;v-text-anchor:middle;"> 4 | <v:stroke dashstyle="Dash"/> 5 | <w:anchorlock/> 6 | <center> 7 | <p style="background:none;border-radius:22px;color:black;display:inline-block;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;mso-padding-alt:0px;padding:10px 25px;text-decoration:none;text-transform:none;width:198px;"> 8 | Reset Password 9 | </p> 10 | </center> 11 | </v:roundrect> 12 | </div> 13 | <![endif]--> 14 | <!--[if !mso]><!--> 15 | <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;width:252px;line-height:100%;"> 16 | <tr> 17 | <td align="center" bgcolor="none" role="presentation" valign="middle" style="border:2px dashed #1f2153;border-bottom:2px dashed #1f2153;border-left:2px dashed #1f2153;border-right:2px dashed #1f2153;border-top:2px dashed #1f2153;border-radius:22px;cursor:auto;height:48px;mso-padding-alt:10px 25px;background:none;"> 18 | <p style="background:none;border-radius:22px;color:black;display:inline-block;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;mso-padding-alt:0px;padding:10px 25px;text-decoration:none;text-transform:none;width:198px;"> 19 | Reset Password 20 | </p> 21 | </td> 22 | </tr> 23 | </table> 24 | <!--<![endif]--> 25 | -------------------------------------------------------------------------------- /Tests/Components/Outputs/Navbar.html: -------------------------------------------------------------------------------- 1 | <!--[if !mso]><!--> 2 | <input type="checkbox" id="4c48e6fe53b37010" class="mj-menu-checkbox" style="display:none !important; max-height:0; visibility:hidden;" /> 3 | <!--<![endif]--> 4 | <div class="mj-menu-trigger" style="display:none;max-height:0px;max-width:0px;font-size:0px;overflow:hidden;"> 5 | <label for="4c48e6fe53b37010" class="mj-menu-label" style="display:block;cursor:pointer;mso-hide:all;-moz-user-select:none;user-select:none;color:#000000;font-size:30px;font-family:Ubuntu, Helvetica, Arial, sans-serif;text-transform:uppercase;text-decoration:none;line-height:30px;padding:10px;" align="center"> 6 | <span class="mj-menu-icon-open" style="mso-hide:all;"> ☰ </span> 7 | <span class="mj-menu-icon-close" style="display:none;mso-hide:all;"> ⊗ </span> 8 | </label> 9 | </div> 10 | <div class="mj-inline-links" style=""> 11 | <!--[if mso | IE]> 12 | <table role="presentation" border="0" cellpadding="0" cellspacing="0" align="center"> 13 | <tr></tr> 14 | </table> 15 | <![endif]--> 16 | </div> -------------------------------------------------------------------------------- /Tests/Components/Outputs/NavbarWithLinks.html: -------------------------------------------------------------------------------- 1 | <!--[if !mso]><!--> 2 | <input type="checkbox" id="b5e7b1c2f1d5bc37" class="mj-menu-checkbox" style="display:none !important; max-height:0; visibility:hidden;" /> 3 | <!--<![endif]--> 4 | <div class="mj-menu-trigger" style="display:none;max-height:0px;max-width:0px;font-size:0px;overflow:hidden;"> 5 | <label for="b5e7b1c2f1d5bc37" class="mj-menu-label" style="display:block;cursor:pointer;mso-hide:all;-moz-user-select:none;user-select:none;color:black;font-size:30px;font-family:Ubuntu, Helvetica, Arial, sans-serif;text-transform:uppercase;text-decoration:none;line-height:30px;padding:10px;" align="center"> 6 | <span class="mj-menu-icon-open" style="mso-hide:all;"> ☰ </span> 7 | <span class="mj-menu-icon-close" style="display:none;mso-hide:all;"> ⊗ </span> 8 | </label> 9 | </div> 10 | <div class="mj-inline-links" style=""> 11 | <!--[if mso | IE]> 12 | <table role="presentation" border="0" cellpadding="0" cellspacing="0" align="center"> 13 | <tr> 14 | <td style="padding:15px 10px;" class="" > 15 | <![endif]--> 16 | <a class="mj-link" href="https://mjml.io/link1" target="_blank" style="display:inline-block;color:#ff00ff;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:22px;text-decoration:none;text-transform:uppercase;padding:15px 10px;"> Link1 </a> 17 | <!--[if mso | IE]> 18 | </td> 19 | <td style="padding:15px 10px;" class="" > 20 | <![endif]--> 21 | <a class="mj-link" href="https://mjml.io/link2" target="_blank" style="display:inline-block;color:#ff0000;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:22px;text-decoration:none;text-transform:uppercase;padding:15px 10px;"> Link2 </a> 22 | <!--[if mso | IE]> 23 | </td> 24 | <td style="padding:15px 10px;" class="" > 25 | <![endif]--> 26 | <a class="mj-link" href="https://mjml.io/link3" target="_blank" style="display:inline-block;color:#0000ff;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:22px;text-decoration:none;text-transform:uppercase;padding:15px 10px;"> Link3 </a> 27 | <!--[if mso | IE]> 28 | </td> 29 | </tr> 30 | </table> 31 | <![endif]--> 32 | -------------------------------------------------------------------------------- /Tests/Components/Outputs/NavbarWithoutHamburger.html: -------------------------------------------------------------------------------- 1 | <div class="mj-inline-links" style=""> 2 | <!--[if mso | IE]> 3 | <table role="presentation" border="0" cellpadding="0" cellspacing="0" align="center"> 4 | <tr></tr> 5 | </table> 6 | <![endif]--> 7 | </div> -------------------------------------------------------------------------------- /Tests/Components/Outputs/Preview.html: -------------------------------------------------------------------------------- 1 | <div style="display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;"> 2 | Hello MJML 3 | </div> 4 | <div></div> -------------------------------------------------------------------------------- /Tests/Components/Outputs/Section.html: -------------------------------------------------------------------------------- 1 | <!--[if mso | IE]> 2 | <table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" class="" style="width:600px;" width="600" > 3 | <tr> 4 | <td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"> 5 | <![endif]--> 6 | <div style="margin:0px auto;max-width:600px;"> 7 | <table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;"> 8 | <tbody> 9 | <tr> 10 | <td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;"> 11 | <!--[if mso | IE]> 12 | <table role="presentation" border="0" cellpadding="0" cellspacing="0"> 13 | <tr></tr> 14 | </table> 15 | <![endif]--> 16 | </td> 17 | </tr> 18 | </tbody> 19 | </table> 20 | </div> 21 | <!--[if mso | IE]> 22 | </td> 23 | </tr> 24 | </table> 25 | <![endif]--> 26 | -------------------------------------------------------------------------------- /Tests/Components/Outputs/SectionWithBackgroundColor.html: -------------------------------------------------------------------------------- 1 | <!--[if mso | IE]> 2 | <table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" class="" style="width:600px;" width="600" bgcolor="red" > 3 | <tr> 4 | <td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"> 5 | <![endif]--> 6 | <div style="background:red;background-color:red;margin:0px auto;max-width:600px;"> 7 | <table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:red;background-color:red;width:100%;"> 8 | <tbody> 9 | <tr> 10 | <td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;"> 11 | <!--[if mso | IE]> 12 | <table role="presentation" border="0" cellpadding="0" cellspacing="0"> 13 | <tr></tr> 14 | </table> 15 | <![endif]--> 16 | </td> 17 | </tr> 18 | </tbody> 19 | </table> 20 | </div> 21 | <!--[if mso | IE]> 22 | </td> 23 | </tr> 24 | </table><![endif]--> 25 | -------------------------------------------------------------------------------- /Tests/Components/Outputs/SectionWithBackgroundImage.html: -------------------------------------------------------------------------------- 1 | <!--[if mso | IE]> 2 | <table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" class="" style="width:600px;" width="600" > 3 | <tr> 4 | <td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"> 5 | <v:rect style="width:600px;" xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false"> 6 | <v:fill origin="0.5, 0" position="0.5, 0" src="https://picsum.photos/600/300" type="tile" /> 7 | <v:textbox style="mso-fit-shape-to-text:true" inset="0,0,0,0"> 8 | <![endif]--> 9 | <div style="background:url(https://picsum.photos/600/300) center top / auto repeat;background-position:center top;background-repeat:repeat;background-size:auto;margin:0px auto;max-width:600px;"> 10 | <div style="line-height:0;font-size:0;"> 11 | <table align="center" background="https://picsum.photos/600/300" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:url(https://picsum.photos/600/300) center top / auto repeat;background-position:center top;background-repeat:repeat;background-size:auto;width:100%;"> 12 | <tbody> 13 | <tr> 14 | <td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;"> 15 | <!--[if mso | IE]> 16 | <table role="presentation" border="0" cellpadding="0" cellspacing="0"> 17 | <tr></tr> 18 | </table> 19 | <![endif]--> 20 | </td> 21 | </tr> 22 | </tbody> 23 | </table> 24 | </div> 25 | </div> 26 | <!--[if mso | IE]> 27 | </v:textbox> 28 | </v:rect> 29 | </td> 30 | </tr> 31 | </table> 32 | <![endif]--> 33 | -------------------------------------------------------------------------------- /Tests/Components/Outputs/SocialEmpty.html: -------------------------------------------------------------------------------- 1 | <!--[if mso | IE]> 2 | <table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" > 3 | <tr> 4 | </tr> 5 | </table> 6 | <![endif]--> 7 | -------------------------------------------------------------------------------- /Tests/Components/Outputs/SocialRaw.html: -------------------------------------------------------------------------------- 1 | <!--[if mso | IE]> 2 | <table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" > 3 | <tr> 4 | <![endif]--> 5 | Hello MJML 6 | <!--[if mso | IE]> 7 | </tr> 8 | </table> 9 | <![endif]--> 10 | -------------------------------------------------------------------------------- /Tests/Components/Outputs/Spacer.html: -------------------------------------------------------------------------------- 1 | <div style="height:20px;line-height:20px;"> 2 |   3 | </div> -------------------------------------------------------------------------------- /Tests/Components/Outputs/SpacerWithHeight.html: -------------------------------------------------------------------------------- 1 | <div style="height:100px;line-height:100px;"> 2 |   3 | </div> -------------------------------------------------------------------------------- /Tests/Components/Outputs/Style.html: -------------------------------------------------------------------------------- 1 | <style type="text/css"> 2 | @media only screen and (min-width:480px) { 3 | } 4 | </style> 5 | 6 | <style media="screen and (min-width:480px)"> 7 | </style> 8 | 9 | <style type="text/css"> 10 | </style> 11 | 12 | <style type="text/css"> 13 | .red-text div { 14 | color: red !important; 15 | } 16 | </style> 17 | 18 | <div> 19 | <div class="red-text"> 20 | <div style="font-weight: bold"></div> 21 | </div> 22 | </div> 23 | -------------------------------------------------------------------------------- /Tests/Components/Outputs/StyleInclude.html: -------------------------------------------------------------------------------- 1 | <title> 2 | 6 | 7 | 9 | 10 | 12 | 13 | -------------------------------------------------------------------------------- /Tests/Components/Outputs/StyleInline.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 7 | 8 | 10 | 11 | 13 | 14 | 16 | 17 | 18 |
19 |
20 |
21 |
22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /Tests/Components/Outputs/StyleInline2.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 7 | 8 | 10 | 11 | 13 | 14 | 16 | 17 | 18 |
19 |
20 |
21 |
22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /Tests/Components/Outputs/StyleInline3.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 7 | 8 | 10 | 11 | 13 | 14 | 16 | 17 | 18 |
19 |
20 |
21 |
22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /Tests/Components/Outputs/StyleInlineFallback.html: -------------------------------------------------------------------------------- 1 |  5 | 6 | 8 | 9 | 11 | 12 | 14 | 15 | 20 | 21 |
22 |
23 |
24 |
25 |
26 | -------------------------------------------------------------------------------- /Tests/Components/Outputs/Table.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
YearLanguageInspired from
1995PHPC, Shell Unix
1995JavaScriptScheme, Self
18 | -------------------------------------------------------------------------------- /Tests/Components/Outputs/TablePercent.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
YearLanguageInspired from
1995PHPC, Shell Unix
1995JavaScriptScheme, Self
-------------------------------------------------------------------------------- /Tests/Components/Outputs/TablePixels.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
YearLanguageInspired from
1995PHPC, Shell Unix
1995JavaScriptScheme, Self
-------------------------------------------------------------------------------- /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 | 
2 |

Hello MJML

3 |
-------------------------------------------------------------------------------- /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 | 30 | 31 | 32 |
11 | 16 |
17 | 23 |
24 | 29 |
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 | --------------------------------------------------------------------------------