From 1f0fafc89d9dbb028e2b50c6ad795bf7b668d055 Mon Sep 17 00:00:00 2001 From: Toinedb Date: Fri, 25 Oct 2024 12:52:18 +0200 Subject: [PATCH] Innitial setup added tests --- .gitattributes | 2 + .github/workflows/ci-test.yml | 21 + .github/workflows/ci.yml | 19 + .github/workflows/release-nuget.yml | 29 ++ .gitignore | 399 ++++++++++++++++++ LICENSE => LICENSE.md | 0 README.md | 20 + nuget.png | Bin 0 -> 2205 bytes src/MarkdownParser.sln | 25 ++ src/MarkdownParser/IViewSupplier.cs | 76 ++++ src/MarkdownParser/LICENSE-CommonMark.NET.md | 27 ++ src/MarkdownParser/ListBulletFormatter.cs | 30 ++ src/MarkdownParser/MarkdownParseStream.cs | 78 ++++ src/MarkdownParser/MarkdownParser.cs | 45 ++ src/MarkdownParser/MarkdownParser.csproj | 52 +++ src/MarkdownParser/ViewFormatter.cs | 168 ++++++++ src/MarkdownParser/ViewWriter.cs | 231 ++++++++++ src/MarkdownParser/ViewWriterCache.cs | 80 ++++ test/MarkdownParser.Test.sln | 31 ++ .../MarkdownParseStreamSectionsSpecs.cs | 175 ++++++++ .../MarkdownParser.Test.csproj | 49 +++ .../MarkdownParserBaseSpecs.cs | 30 ++ .../MarkdownParserListComponentSpecs.cs | 85 ++++ .../MarkdownParserSectionsSpecs.cs | 101 +++++ .../MarkdownParserSingleComponentSpecs.cs | 249 +++++++++++ .../Mocks/StringComponentSupplier.cs | 54 +++ .../Examples/Combined/basic-example.md | 98 +++++ .../Examples/Combined/full-example.md | 157 +++++++ .../Examples/Combined/minimal-example.md | 43 ++ .../Resources/Examples/Sections/headers.md | 7 + .../Resources/Examples/Sections/list.md | 11 + .../Resources/Examples/Sections/nestedlist.md | 5 + .../Resources/Examples/Sections/paragraphs.md | 9 + .../Services/FileReader.cs | 17 + 34 files changed, 2423 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/workflows/ci-test.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release-nuget.yml create mode 100644 .gitignore rename LICENSE => LICENSE.md (100%) create mode 100644 README.md create mode 100644 nuget.png create mode 100644 src/MarkdownParser.sln create mode 100644 src/MarkdownParser/IViewSupplier.cs create mode 100644 src/MarkdownParser/LICENSE-CommonMark.NET.md create mode 100644 src/MarkdownParser/ListBulletFormatter.cs create mode 100644 src/MarkdownParser/MarkdownParseStream.cs create mode 100644 src/MarkdownParser/MarkdownParser.cs create mode 100644 src/MarkdownParser/MarkdownParser.csproj create mode 100644 src/MarkdownParser/ViewFormatter.cs create mode 100644 src/MarkdownParser/ViewWriter.cs create mode 100644 src/MarkdownParser/ViewWriterCache.cs create mode 100644 test/MarkdownParser.Test.sln create mode 100644 test/MarkdownParser.Test/MarkdownParseStreamSectionsSpecs.cs create mode 100644 test/MarkdownParser.Test/MarkdownParser.Test.csproj create mode 100644 test/MarkdownParser.Test/MarkdownParserBaseSpecs.cs create mode 100644 test/MarkdownParser.Test/MarkdownParserListComponentSpecs.cs create mode 100644 test/MarkdownParser.Test/MarkdownParserSectionsSpecs.cs create mode 100644 test/MarkdownParser.Test/MarkdownParserSingleComponentSpecs.cs create mode 100644 test/MarkdownParser.Test/Mocks/StringComponentSupplier.cs create mode 100644 test/MarkdownParser.Test/Resources/Examples/Combined/basic-example.md create mode 100644 test/MarkdownParser.Test/Resources/Examples/Combined/full-example.md create mode 100644 test/MarkdownParser.Test/Resources/Examples/Combined/minimal-example.md create mode 100644 test/MarkdownParser.Test/Resources/Examples/Sections/headers.md create mode 100644 test/MarkdownParser.Test/Resources/Examples/Sections/list.md create mode 100644 test/MarkdownParser.Test/Resources/Examples/Sections/nestedlist.md create mode 100644 test/MarkdownParser.Test/Resources/Examples/Sections/paragraphs.md create mode 100644 test/MarkdownParser.Test/Services/FileReader.cs diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml new file mode 100644 index 0000000..130e461 --- /dev/null +++ b/.github/workflows/ci-test.yml @@ -0,0 +1,21 @@ +name: Run Tests for CI + +on: + push: + branches: [ "main" ] + paths-ignore: + - "**.md" + pull_request: + branches: [ "main" ] + +jobs: + build-sample-ci: + + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + - name: Build + run: dotnet build test\MarkdownParser.Test.sln -c Release + - name: Run Tests + run: dotnet test test\MarkdownParser.Test\MarkdownParser.Test.csproj \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..233114d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,19 @@ +name: Build for CI + +on: + push: + branches: [ "main" ] + paths-ignore: + - "**.md" + pull_request: + branches: [ "main" ] + +jobs: + build-plugin-ci: + + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + - name: Build + run: dotnet build src\MarkdownParser.sln -c Release diff --git a/.github/workflows/release-nuget.yml b/.github/workflows/release-nuget.yml new file mode 100644 index 0000000..47a1d64 --- /dev/null +++ b/.github/workflows/release-nuget.yml @@ -0,0 +1,29 @@ +name: Create a (Pre)release on NuGet + +on: + push: + tags: + - "v[0-9]+.[0-9]+.[0-9]+" + - "v[0-9]+.[0-9]+.[0-9]+-preview[0-9]+" +jobs: + release-nuget: + + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + - name: Verify commit exists in origin/main + run: | + git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* + git branch --remote --contains | grep origin/main + - name: Get version information from tag + id: get_version + run: | + $version="${{github.ref_name}}".TrimStart("v") + "version-without-v=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + - name: Pack + run: dotnet pack src\MarkdownParser.sln -c Release -p:PackageVersion=${{ steps.get_version.outputs.version-without-v }} + - name: Push + run: dotnet nuget push src\MarkdownParser\bin\Release\MarkdownParser.${{ steps.get_version.outputs.version-without-v }}.nupkg -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_API_KEY }} + env: + GITHUB_TOKEN: ${{ secrets.NUGET_API_KEY }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f7fed0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,399 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +.DS_Store diff --git a/LICENSE b/LICENSE.md similarity index 100% rename from LICENSE rename to LICENSE.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..aad9c61 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +![nuget.png](https://raw.githubusercontent.com/Toine-db/MarkdownParser/main/nuget.png) +# MarkdownParser + +`MarkdownParser` provides the ability to parse Markdown text in to a nested UI structure. + +## Install Plugin + +[![NuGet](https://img.shields.io/nuget/v/MarkdownParser?label=NuGet)](https://www.nuget.org/packages/MarkdownParser/) + +Available on [NuGet](http://www.nuget.org/packages/MarkdownParser). + +Install with the dotnet CLI: `dotnet add package MarkdownParser`, or through the NuGet Package Manager in Visual Studio. + +## The Mechanism +1. CommonMark.NET is used to read Markdown and turn them into usable c# objects +2. A custom formatter is created to work with the created c# objects (formatter looks like a CommonMark.NET formatter) +3. A custom writer is created to control creation of ui components +4. You need to create: An UI component generator (IViewSupplier) must be created which supplies the ui components (one for every platform) + +Need any help on creating a IViewSupplier? check out [Plugin.Maui.MarkdownView](https://github.com/Toine-db/Plugin.Maui.MarkdownView). \ No newline at end of file diff --git a/nuget.png b/nuget.png new file mode 100644 index 0000000000000000000000000000000000000000..41255a1a183542b95de9fef9d1e7cb9b2bad73f8 GIT binary patch literal 2205 zcma)8c{J1w7ygYIV@9JvH1;LZNcOQC#y*O%E6SQ>=%o>|k9&F40}!x-bJEuhr5fUtNG@S_ z1ynm@M$-Z=Ti6I;o^s%rqecya+Gpipqk6{CuS%N)uk$lCyaY#B@WHC z(!^9yH`@~1J^lqlV&_ovL(>8_lCcUCOUrbM)%{r#SitEVgQaDOF|^Q*O!%s^f5ZCY z?^qx$T5n|G>W69iYqM8nW5?E1Qq~{AvE8B8{HET8)h{P8RN&%t6K8Fc-?eXg4K$N< z;~UB$W5C^nEXs1CIv35f!1Jb5P3kxl$4EEY;=^!g-}gT6!MgV44egY`r_pL@m2w1` zVP(*hsN(C(en)Z6Fsv7{I_#;-DG0=VJ&fUXhN&$}+MZyz4Db>qQ%MqX$+$5Iz2jJ@ zAG5cYvTBo5F-g?i1vK{}f?7cS9~6_gC|K&-OBLWVM`}hb-0A8m%;!gsM}aNL@b7sh zT323fnPfoq#oAk=XS}?J)I8`)t6{ zX+jb~Z-L}8C|*PjH(v?jHRH zxd!l!>wKnZR&7<^o|f$58JbYF4l6`Wp@JOqhqK_gnZfW2_DJ>_&~`N?j*$J;6F3g5 zDQJ)(W=6D%=NMv@sGjISulownaPE1p*V%TMT={h^&4k$#G-$4Sl;gZXEr^y>&@pe3 zv{@P4BNLtVDSAn;67}@7Jl6qE)Ag#VMfXtSh0nA2X&_BFmt#1AGxUNtC0;wRBV!95 z{jqI(u!>H$R|Z<4TCOk z_DsAkOVqI&QgE^R0Rt)lgFd`u@?btb_8sF8$#Kv1YC~X>Kz9RTZ^$b28{+ui1RRUx(iX zMhFN?vZ*dCIrO-Uwde@;SraHO#;UtKRvJ8+5xUvrr!Wp>wCq!)1R*Ht6=dtk!xC`| zb#Y~|dm>Bl!RoMW_-+5XON%fkD@K58%68ZGp7bp;N3ory<83MzN{%IJ`1VZQa6
xq? zm)V0nY%O5BZ`596$Jx_;3=*T?B#@WNxITPdm$b~QrI}R$lsgk2S(TV^v*ex3fzl#M zwk;`~H9&g_i(-EIdRMc(@w z2jczX_fdwLaQ2Gw%Tqb3D$t+3nHLJ)h4b)v@BD=vSB!6>0&X}B)SoV#Kl@QGTJbeg zWw|R%Q|m6v)j>L~sg!!=`}M~LHjC;CaJKG(t(ml8x#l2lNn2fOJavTm>8??Jw6y&UM6 zRIbjI+^78~V>!9^oAHnukd`)!UkM4>@yu_gM=8}2T-TKr-I+z(t!WAZgf9u>7L~_GA=3ID7ZXQ6i;{gxhj;T#oUYsg@)*I=7yh;_ z*R+be6z3i+e5(!;(D8AxKS-UNFgxC8osEtClF74BDa_U^OTS-1C-SKP1rdNVdF`~d z!#hY9-JWeI6lwOoU#(f5KF!x(LPTc7-u<#n-Uf95{p}`SKGWT=enx`ciAKS`D={6@ z1H$!YFwQOd+?(P;uPpPFdCfKYmOZuS2T}}soRnE*qav8M&DF-RHDrwTV!$RJdZ$ky ss(JMCa9U)Y{(MX1e;;g@qc{ZJ$gz5E9jWo;=%oP6O$f%dhVF6y0p^V9(*OVf literal 0 HcmV?d00001 diff --git a/src/MarkdownParser.sln b/src/MarkdownParser.sln new file mode 100644 index 0000000..ccf2219 --- /dev/null +++ b/src/MarkdownParser.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35312.102 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MarkdownParser", "MarkdownParser\MarkdownParser.csproj", "{C97AA01D-DDE0-4246-96D3-6FAA37FA7947}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C97AA01D-DDE0-4246-96D3-6FAA37FA7947}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C97AA01D-DDE0-4246-96D3-6FAA37FA7947}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C97AA01D-DDE0-4246-96D3-6FAA37FA7947}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C97AA01D-DDE0-4246-96D3-6FAA37FA7947}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {BCACAFBD-7377-4A34-B26D-EB532079B453} + EndGlobalSection +EndGlobal diff --git a/src/MarkdownParser/IViewSupplier.cs b/src/MarkdownParser/IViewSupplier.cs new file mode 100644 index 0000000..2c7e231 --- /dev/null +++ b/src/MarkdownParser/IViewSupplier.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; + +namespace MarkdownParser +{ + public interface IViewSupplier + { + /// + /// a default text view + /// + /// + /// + T GetTextView(string content); + + /// + /// a block quote view, where other views can be inserted + /// + /// + /// + T GetBlockquotesView(T childView); + + /// + /// a title, subtitle or header view + /// + /// + /// + /// + T GetHeaderView(string content, int headerLevel); + + /// + /// a image view with a optional subscription text view + /// + /// image location + /// (optional) null or empty when not used + /// (optional) id for image + /// + T GetImageView(string url, string subscription, string imageId); + + /// + /// a view that shows a list of listitems + /// + /// + /// + T GetListView(List items); + + /// + /// a view that shows a single item for a ListView (return View can be used in GetListView) + /// + /// view as childview (or use the content parameter) + /// does the item belong to a ordered (numbered) list + /// number of sequence + /// level depth of the list, root level starting at 1 + /// + T GetListItemView(T childView, bool isOrderedList, int sequenceNumber, int listLevel); + + /// + /// a layout that shows a collection of views + /// + /// collection of views + /// + T GetStackLayoutView(List childViews); + + /// + /// a image view that separates content + /// + /// + T GetThematicBreak(); + + + /// + /// a placeholder for views or other objects + /// + /// placeholder string + /// + T GetPlaceholder(string placeholderName); + } +} diff --git a/src/MarkdownParser/LICENSE-CommonMark.NET.md b/src/MarkdownParser/LICENSE-CommonMark.NET.md new file mode 100644 index 0000000..956e71f --- /dev/null +++ b/src/MarkdownParser/LICENSE-CommonMark.NET.md @@ -0,0 +1,27 @@ +Copyright (c) 2014, Kārlis Gaņģis +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of Kārlis Gaņģis nor the names of other contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/src/MarkdownParser/ListBulletFormatter.cs b/src/MarkdownParser/ListBulletFormatter.cs new file mode 100644 index 0000000..6a96d6a --- /dev/null +++ b/src/MarkdownParser/ListBulletFormatter.cs @@ -0,0 +1,30 @@ +using System; +using System.Globalization; + +namespace MarkdownParser +{ + public static class ListBulletFormatter + { + public static string GetListItemBullet(bool isOrderedList, int sequenceNumber, int listLevel, string listItemBulletOddCharacter, string listItemBulletEvenCharacter) + { + // 'listlevel' even or odd (list start at level 1 == odd) + var isListOddLeveled = (listLevel % 2) != 0; + + if (!isOrderedList) + { + return (isListOddLeveled) + ? listItemBulletOddCharacter + : listItemBulletEvenCharacter; + } + + const int aCharacterPosition = 97; // character 'a' position in ASCI table + var sequenceNumberCharacterPosition = -1 + aCharacterPosition + sequenceNumber; + + var bullet = (isListOddLeveled) + ? sequenceNumber.ToString() + : Convert.ToChar(sequenceNumberCharacterPosition, CultureInfo.InvariantCulture).ToString(); + + return $"{bullet}."; + } + } +} diff --git a/src/MarkdownParser/MarkdownParseStream.cs b/src/MarkdownParser/MarkdownParseStream.cs new file mode 100644 index 0000000..005cb4b --- /dev/null +++ b/src/MarkdownParser/MarkdownParseStream.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.IO; +using CommonMark.Syntax; + +namespace MarkdownParser +{ + public class MarkdownParseStream : IDisposable + { + private readonly TextReader _markdownSource; + + private Block _markdownDocument; + private Block _workingBlock; + + // Format\Convert c# objects into UI Components + private ViewFormatter _formatter; + + private readonly Queue _readCache = new Queue(); + + public MarkdownParseStream(IViewSupplier viewSupplier, TextReader markdownSource) + { + _markdownSource = markdownSource; + _formatter = new ViewFormatter(viewSupplier); + } + + /// + /// Read 1 element + /// + /// 1 element or default(T)/null when finished + public T Read() + { + if (_markdownDocument == null) + { + _markdownDocument = MarkdownParser.GetMarkdownDocument(_markdownSource); + + // Set first block to parse + _workingBlock = _markdownDocument.FirstChild; + } + + if (_readCache.Count != 0) + { + return _readCache.Dequeue(); + } + + if (_workingBlock == null) + { + return default(T); + } + + var uiComponents = _formatter.FormatSingleBlock(_workingBlock); + _workingBlock = _workingBlock.NextSibling; + + AddComponentsToCache(uiComponents); + + if (_readCache.Count != 0) + { + return _readCache.Dequeue(); + } + + return Read(); + } + + private void AddComponentsToCache(List components) + { + foreach (var uiComponent in components) + { + _readCache.Enqueue(uiComponent); + } + } + + public void Dispose() + { + _markdownDocument = null; + _formatter = null; + _workingBlock = null; + } + } +} diff --git a/src/MarkdownParser/MarkdownParser.cs b/src/MarkdownParser/MarkdownParser.cs new file mode 100644 index 0000000..4a3d25b --- /dev/null +++ b/src/MarkdownParser/MarkdownParser.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.IO; +using CommonMark; +using CommonMark.Syntax; + +namespace MarkdownParser +{ + public class MarkdownParser + { + private readonly IViewSupplier _viewSupplier; + + public MarkdownParser(IViewSupplier viewSupplier) + { + _viewSupplier = viewSupplier; + } + + public List Parse(string markdownSource) + { + using (var reader = new StringReader(markdownSource)) + { + return Parse(reader); + } + } + + public List Parse(TextReader markdownSource) + { + // Parse to usable c# objects + var markdownDocument = GetMarkdownDocument(markdownSource); + + // Format\Convert c# objects into UI Components + var formatter = new ViewFormatter(_viewSupplier); + var uiComponents = formatter.Format(markdownDocument); + + return uiComponents; + } + + public static Block GetMarkdownDocument(TextReader markdownSource) + { + // Parse to usable c# objects + var settings = CommonMarkSettings.Default.Clone(); + settings.AdditionalFeatures |= CommonMarkAdditionalFeatures.PlaceholderBracket; + return CommonMarkConverter.Parse(markdownSource, settings); + } + } +} diff --git a/src/MarkdownParser/MarkdownParser.csproj b/src/MarkdownParser/MarkdownParser.csproj new file mode 100644 index 0000000..dc60480 --- /dev/null +++ b/src/MarkdownParser/MarkdownParser.csproj @@ -0,0 +1,52 @@ + + + + + netstandard2.0;net8.0 + + + Toine de Boer + Copyright © Toine de Boer and contributors + True + https://github.com/Toine-db/MarkdownParser + https://github.com/Toine-db/MarkdownParser + git + dotnet;markdown; + True + true + true + snupkg + .NET Markdown Parser + Parser for Markdown text input and a Hierarchical UI structure output. + MIT + True + portable + icon.png + + + + + + + + + + PreserveNewest + + + + + + PreserveNewest + + + PreserveNewest + + + + + + + + + diff --git a/src/MarkdownParser/ViewFormatter.cs b/src/MarkdownParser/ViewFormatter.cs new file mode 100644 index 0000000..af12363 --- /dev/null +++ b/src/MarkdownParser/ViewFormatter.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using CommonMark; +using CommonMark.Syntax; + +namespace MarkdownParser +{ + public class ViewFormatter + { + private readonly ViewWriter _writer; + + public ViewFormatter(IViewSupplier viewSupplier) + { + _writer = new ViewWriter(viewSupplier); + } + + public List Format(Block markdownBlock) + { + WriteBlockToView(markdownBlock, _writer); + return _writer.Flush(); + } + + public List FormatSingleBlock(Block markdownBlock) + { + WriteBlockToView(markdownBlock, _writer, false); + return _writer.Flush(); + } + + private void WriteBlockToView(Block block, ViewWriter writer, bool continueWithNextSibling = true) + { + if (block == null) + { + return; + } + + switch (block.Tag) + { + case BlockTag.Document: + WriteBlockToView(block.FirstChild, writer); + break; + case BlockTag.Paragraph: + writer.StartBlock(block.Tag); + WriteInlineToView(block.InlineContent, writer); + writer.FinalizeParagraphBlock(); + break; + case BlockTag.BlockQuote: + writer.StartBlock(BlockTag.BlockQuote); + WriteBlockToView(block.FirstChild, writer); + writer.FinalizeBlockquoteBlock(); + break; + case BlockTag.List: + writer.StartBlock(block.Tag); + WriteBlockToView(block.FirstChild, writer); + writer.FinalizeListBlock(); + break; + case BlockTag.ListItem: + writer.StartBlock(block.Tag); + WriteBlockToView(block.FirstChild, writer); + writer.FinalizeListItemBlock(block.ListData); + break; + case BlockTag.AtxHeading: + case BlockTag.SetextHeading: + writer.StartBlock(block.Tag); + WriteInlineToView(block.InlineContent, writer); // needs to be tested + WriteBlockToView(block.FirstChild, writer); + writer.FinalizeHeaderBlock(block.Heading.Level); + break; + case BlockTag.ThematicBreak: + writer.StartAndFinalizeThematicBreak(); + break; + case BlockTag.FencedCode: + case BlockTag.IndentedCode: + // not supported + break; + case BlockTag.HtmlBlock: + // TODO.....if needed + //writer.StartBlock(BlockTag.Paragraph, block.StringContent.ToString()); + //WriteBlockToView(block.FirstChild, writer); + //writer.FinalizeParagraphBlock(); + break; + case BlockTag.ReferenceDefinition: + // not supported + break; + default: + throw new CommonMarkException("Block type " + block.Tag + " is not supported.", block); + } + + if (continueWithNextSibling && block.NextSibling != null) + { + WriteBlockToView(block.NextSibling, writer); + } + } + + private void WriteInlineToView(Inline inline, ViewWriter writer) + { + if (inline == null) + { + return; + } + + switch (inline.Tag) + { + case InlineTag.String: + case InlineTag.Code: + case InlineTag.RawHtml: + writer.AddText(inline.LiteralContent); + break; + case InlineTag.Link: + // TODO; maybe + WriteInlineToView(inline.FirstChild, writer); + break; + case InlineTag.Image: + writer.StartAndFinalizeImageBlock(inline.TargetUrl, inline.LiteralContent, inline.FirstChild?.LiteralContent); + break; + case InlineTag.SoftBreak: + case InlineTag.LineBreak: + writer.AddText(Environment.NewLine); + break; + case InlineTag.Placeholder: + writer.StartAndFinalizePlaceholderBlock(inline.TargetUrl); + break; + case InlineTag.Strikethrough: + case InlineTag.Emphasis: + case InlineTag.Strong: + WriteInlineToView(inline.FirstChild, writer); + break; + + default: + throw new ArgumentOutOfRangeException(); + } + + if (inline.NextSibling != null) + { + WriteInlineToView(inline.NextSibling, writer); + } + } + + // Helper for bug CommonMark nested listing + private List TrimNestedListBlocks(Block listBlock) + { + var listBlockChildTree = listBlock.AsEnumerable(); + + var cleanedChildTree = new List(); + var captureEnabled = true; + foreach (var child in listBlockChildTree) + { + if (child.Block == null) + { + continue; + } + + if (child.Block != listBlock + && child.Block.Tag == BlockTag.List) + { + captureEnabled = child.IsClosing; + } + + if (captureEnabled + && child.IsOpening) + { + cleanedChildTree.Add(child.Block); + } + } + + return cleanedChildTree; + } + } +} \ No newline at end of file diff --git a/src/MarkdownParser/ViewWriter.cs b/src/MarkdownParser/ViewWriter.cs new file mode 100644 index 0000000..6eb147d --- /dev/null +++ b/src/MarkdownParser/ViewWriter.cs @@ -0,0 +1,231 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using CommonMark.Syntax; + +namespace MarkdownParser +{ + public class ViewWriter + { + private IViewSupplier ViewSupplier { get; } + private List WrittenViews { get; set; } = new List(); + + private Stack> Workbench { get; } = new Stack>(); + private ViewWriterCache GetWorkbenchItem() + { + if (Workbench.Count == 0) + { + return null; + } + + return Workbench.Peek(); + } + + public ViewWriter(IViewSupplier viewSupplier) + { + ViewSupplier = viewSupplier; + } + + public List Flush() + { + var collectedViews = WrittenViews; + WrittenViews = new List(); + + return collectedViews; + } + + public void StartBlock(BlockTag blockType, string content = "") + { + Workbench.Push(new ViewWriterCache { ComponentType = blockType }); + GetWorkbenchItem().Add(content); + } + + public void FinalizeParagraphBlock() + { + var wbi = GetWorkbenchItem(); + if (wbi.ComponentType != BlockTag.Paragraph) + { + Debug.WriteLine($"Finalizing Paragraph can not finalize {wbi.ComponentType}"); + return; + } + + var views = new List(); + + var topWorkbenchItem = Workbench.Pop(); + var itemsCache = topWorkbenchItem.GetGroupedCachedValues(); + + foreach (var itemsCacheTuple in itemsCache) + { + var view = !string.IsNullOrEmpty(itemsCacheTuple.Item1) ? + ViewSupplier.GetTextView(itemsCacheTuple.Item1) : itemsCacheTuple.Item2; + + if (view != null) + { + views.Add(view); + } + } + + StoreView(StackViews(views)); + } + + public void FinalizeBlockquoteBlock() + { + var wbi = GetWorkbenchItem(); + if (wbi.ComponentType != BlockTag.BlockQuote) + { + Debug.WriteLine($"Finalizing BlockQuote can not finalize {wbi.ComponentType}"); + return; + } + + var topWorkbenchItem = Workbench.Pop(); + var itemsCache = topWorkbenchItem.GetGroupedCachedValues(); + + var childViews = itemsCache.Select(itemsCacheTuple => itemsCacheTuple.Item2).ToList(); + var childView = StackViews(childViews); + + var blockView = ViewSupplier.GetBlockquotesView(childView); + + StoreView(blockView); + } + + public void FinalizeHeaderBlock(int headerLevel) + { + var wbi = GetWorkbenchItem(); + if (wbi.ComponentType != BlockTag.AtxHeading + && wbi.ComponentType != BlockTag.SetextHeading) + + { + Debug.WriteLine($"Finalizing Header can not finalize {wbi.ComponentType}"); + return; + } + + var views = new List(); + + var topWorkbenchItem = Workbench.Pop(); + var itemsCache = topWorkbenchItem.GetGroupedCachedValues(); + + foreach (var itemsCacheTuple in itemsCache) + { + var view = !string.IsNullOrEmpty(itemsCacheTuple.Item1) ? + ViewSupplier.GetHeaderView(itemsCacheTuple.Item1, headerLevel) : itemsCacheTuple.Item2; + + views.Add(view); + } + + StoreView(StackViews(views)); + } + + public void FinalizeListBlock() + { + var wbi = GetWorkbenchItem(); + if (wbi.ComponentType != BlockTag.List) + { + Debug.WriteLine($"Finalizing List can not finalize {wbi.ComponentType}"); + return; + } + + var topWorkbenchItem = Workbench.Pop(); + var itemsCache = topWorkbenchItem.GetGroupedCachedValues(); + + var listItems = itemsCache.Select(itemsCacheTuple => itemsCacheTuple.Item2).ToList(); + var listView = ViewSupplier.GetListView(listItems); + + StoreView(listView); + } + + public void FinalizeListItemBlock(ListData listData) + { + var wbi = GetWorkbenchItem(); + if (wbi.ComponentType != BlockTag.ListItem) + { + Debug.WriteLine($"Finalizing ListItem can not finalize {wbi.ComponentType}"); + return; + } + + var views = new List(); + + var isOrderedList = listData.ListType == ListType.Ordered; + var sequenceNumber = listData.Start; + var depthLevel = Workbench.Count(wbItem => wbItem.ComponentType == BlockTag.List); + + var topWorkbenchItem = Workbench.Pop(); + var itemsCache = topWorkbenchItem.GetGroupedCachedValues(); + + + foreach (var itemsCacheTuple in itemsCache) + { + var view = !string.IsNullOrEmpty(itemsCacheTuple.Item1) ? + ViewSupplier.GetTextView(itemsCacheTuple.Item1) : itemsCacheTuple.Item2; + + if (view != null) + { + views.Add(view); + } + } + + var flattendView = StackViews(views); + + var listItemView = ViewSupplier.GetListItemView(flattendView, isOrderedList, sequenceNumber, depthLevel); + + StoreView(listItemView); + } + + public void AddText(string content) + { + GetWorkbenchItem().Add(content); + } + + public void StartAndFinalizeImageBlock(string targetUrl, string subscription, string imageId) + { + var imageView = ViewSupplier.GetImageView(targetUrl, subscription, imageId); + StoreView(imageView); + } + + public void StartAndFinalizeThematicBreak() + { + var seperator = ViewSupplier.GetThematicBreak(); + StoreView(seperator); + } + + public void StartAndFinalizePlaceholderBlock(string placeholderName) + { + var placeholderView = ViewSupplier.GetPlaceholder(placeholderName); + StoreView(placeholderView); + } + + private T StackViews(List views) + { + if (views == null || views.Count == 0) + { + return default(T); + } + + // multiple views combine a single stack layout + var viewToStore = views.Count == 1 ? + views[0] : ViewSupplier.GetStackLayoutView(views); + + return viewToStore; + } + + private void StoreView(T view) + { + if (view == null) + { + return; + } + + // Check if Workbench has an item where its working on + var wbi = GetWorkbenchItem(); + if (wbi != null) // add the new View to the WorkbenchItem + { + wbi.Add(view); + } + else // otherwise add the new View to finalized views collection + { + WrittenViews.Add(view); + } + } + + } +} diff --git a/src/MarkdownParser/ViewWriterCache.cs b/src/MarkdownParser/ViewWriterCache.cs new file mode 100644 index 0000000..4f1ce4f --- /dev/null +++ b/src/MarkdownParser/ViewWriterCache.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using CommonMark.Syntax; + +namespace MarkdownParser +{ + public class ViewWriterCache + { + public BlockTag? ComponentType { get; set; } + + private readonly Stack> _valuesStack = new Stack>(); + private readonly T _defaultT = default(T); + + public void Add(T item) + { + if (EqualityComparer.Default.Equals(item, _defaultT)) + { + return; + } + + _valuesStack.Push(new Tuple(null, item)); + } + + public void Add(string item) + { + if (string.IsNullOrEmpty(item)) + { + return; + } + + _valuesStack.Push(new Tuple(item, _defaultT)); + } + + /// + /// Get cached items in order, each Tuple has a text or T value (never both in the same Tuple) + /// + /// collection that contain a string of T (never both in the same Tuple) + public List> GetGroupedCachedValues() + { + var groupedCache = new List>(); + + var workbenchItemTextCacheBuilder = new StringBuilder(); + var values = _valuesStack.Reverse().ToArray(); + + foreach (var value in values) + { + // Check for text + if (!string.IsNullOrEmpty(value.Item1)) + { + workbenchItemTextCacheBuilder.Append(value.Item1); + continue; + } + + // No text anymore: Store workbenchItemTextCache if any to groupedCache + if (workbenchItemTextCacheBuilder.Length != 0) + { + groupedCache.Add(new Tuple(workbenchItemTextCacheBuilder.ToString(), _defaultT)); + workbenchItemTextCacheBuilder.Clear(); + } + + // If item2 is not null + if (!EqualityComparer.Default.Equals(value.Item2, _defaultT)) + { + groupedCache.Add(new Tuple(null, value.Item2)); + } + } + + // Store leftovers workbenchItemTextCache + if (workbenchItemTextCacheBuilder.Length != 0) + { + groupedCache.Add(new Tuple(workbenchItemTextCacheBuilder.ToString(), _defaultT)); + workbenchItemTextCacheBuilder.Clear(); + } + + return groupedCache; + } + } +} \ No newline at end of file diff --git a/test/MarkdownParser.Test.sln b/test/MarkdownParser.Test.sln new file mode 100644 index 0000000..8b86ee2 --- /dev/null +++ b/test/MarkdownParser.Test.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35312.102 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MarkdownParser.Test", "MarkdownParser.Test\MarkdownParser.Test.csproj", "{9AF00332-0247-495C-B708-C59D1EE9028B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MarkdownParser", "..\src\MarkdownParser\MarkdownParser.csproj", "{6FF59D32-432F-404B-A8E5-7EAECEA3BE8C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9AF00332-0247-495C-B708-C59D1EE9028B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9AF00332-0247-495C-B708-C59D1EE9028B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9AF00332-0247-495C-B708-C59D1EE9028B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9AF00332-0247-495C-B708-C59D1EE9028B}.Release|Any CPU.Build.0 = Release|Any CPU + {6FF59D32-432F-404B-A8E5-7EAECEA3BE8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6FF59D32-432F-404B-A8E5-7EAECEA3BE8C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6FF59D32-432F-404B-A8E5-7EAECEA3BE8C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6FF59D32-432F-404B-A8E5-7EAECEA3BE8C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6511C5AC-C79D-493E-9C8B-34B846285390} + EndGlobalSection +EndGlobal diff --git a/test/MarkdownParser.Test/MarkdownParseStreamSectionsSpecs.cs b/test/MarkdownParser.Test/MarkdownParseStreamSectionsSpecs.cs new file mode 100644 index 0000000..00e45b3 --- /dev/null +++ b/test/MarkdownParser.Test/MarkdownParseStreamSectionsSpecs.cs @@ -0,0 +1,175 @@ +using System.Text.RegularExpressions; +using FluentAssertions; +using MarkdownParser.Test.Mocks; +using MarkdownParser.Test.Services; + +namespace MarkdownParser.Test; + +[TestClass] +public class MarkdownParseStreamSectionsSpecs +{ + [TestMethod] + public void When_stream_parsing_paragraphs_it_should_output_multiple_text_views_in_good_order() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + List parseResult = new List(); + var markdown = FileReader.ReadFile("Sections.paragraphs.md"); + + var mockComponentSupplier = new StringComponentSupplier(); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + using (var reader = new StringReader(markdown)) + { + using (var markdownParseStream = new MarkdownParseStream(mockComponentSupplier, reader)) + { + var output = markdownParseStream.Read(); ; + while (output != null) // default(string) is NULL + { + parseResult.Add(output); + output = markdownParseStream.Read(); + } + } + } + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult.Count.Should().Be(3); + parseResult[0].Should().StartWith("textview:Paragraphs are"); + parseResult[1].Should().StartWith("textview:2nd paragraph."); + parseResult[2].Should().StartWith("textview:Note that"); + } + + [TestMethod] + public void When_stream_parsing_headers_it_should_output_header_views() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + List parseResult = new List(); + var markdown = FileReader.ReadFile("Sections.headers.md"); + + var mockComponentSupplier = new StringComponentSupplier(); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + using (var reader = new StringReader(markdown)) + { + using (var markdownParseStream = new MarkdownParseStream(mockComponentSupplier, reader)) + { + var output = markdownParseStream.Read(); ; + while (output != null) // default(string) is NULL + { + parseResult.Add(output); + output = markdownParseStream.Read(); + } + } + } + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult.Count.Should().Be(3); + parseResult[0].Should().StartWith("headerview:"); + parseResult[1].Should().StartWith("headerview:"); + parseResult[1].Should().StartWith("headerview:"); + } + + [TestMethod] + public void When_stream_parsing_nested_list_it_should_output_nesting_by_level() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + List parseResult = new List(); + var markdown = FileReader.ReadFile("Sections.nestedlist.md"); + + var mockComponentSupplier = new StringComponentSupplier(); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + using (var reader = new StringReader(markdown)) + { + using (var markdownParseStream = new MarkdownParseStream(mockComponentSupplier, reader)) + { + var output = markdownParseStream.Read(); ; + while (output != null) // default(string) is NULL + { + parseResult.Add(output); + output = markdownParseStream.Read(); + } + } + } + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult.Count.Should().Be(1); // just becuase it only outputs a single string + + var listviewCount = Regex.Matches(parseResult[0], "listview>:").Cast().Count(); + listviewCount.Should().Be(2); + + var splittedViews = parseResult[0].Split(':'); + splittedViews[0].Should().Be("listview>"); + splittedViews[1].Should().Be("-listitemview"); + splittedViews[2].Should().Be("_False.1.1_textview"); + splittedViews[3].Should().Be("item1-listitemview"); + splittedViews[4].Should().Be("_False.1.1_stackview>"); + splittedViews[5].Should().Be("+textview"); + splittedViews[6].Should().Be("item2+listview>"); + splittedViews[7].Should().Be("-listitemview"); + splittedViews[8].Should().Be("_False.2.1_textview"); + splittedViews[9].Should().Be("item2-1-listitemview"); + splittedViews[10].Should().Be("_False.2.1_textview"); + splittedViews[11].Should().Be("item2-2-listitemview"); + splittedViews[12].Should().Be("_False.2.1_textview"); + splittedViews[13].Should().Be("item2-3 parseResult = new List(); + + var mockComponentSupplier = new StringComponentSupplier(); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + using (var reader = new StringReader(markdown)) + { + using (var markdownParseStream = new MarkdownParseStream(mockComponentSupplier, reader)) + { + var output = markdownParseStream.Read(); ; + while (output != null) // default(string) is NULL + { + parseResult.Add(output); + output = markdownParseStream.Read(); + } + } + } + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult.Count.Should().Be(1); + parseResult.First().Should().StartWith("listview>:"); + + var listitems = parseResult.First().Substring(parseResult.First().IndexOf("-listitemview") + 1).Split('-'); + listitems.Length.Should().Be(3); + } +} diff --git a/test/MarkdownParser.Test/MarkdownParser.Test.csproj b/test/MarkdownParser.Test/MarkdownParser.Test.csproj new file mode 100644 index 0000000..f93842d --- /dev/null +++ b/test/MarkdownParser.Test/MarkdownParser.Test.csproj @@ -0,0 +1,49 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/MarkdownParser.Test/MarkdownParserBaseSpecs.cs b/test/MarkdownParser.Test/MarkdownParserBaseSpecs.cs new file mode 100644 index 0000000..4e2d0ad --- /dev/null +++ b/test/MarkdownParser.Test/MarkdownParserBaseSpecs.cs @@ -0,0 +1,30 @@ +using FluentAssertions; +using MarkdownParser.Test.Mocks; + +namespace MarkdownParser.Test; + +[TestClass] +public class MarkdownParserBaseSpecs +{ + [TestMethod] + public void When_starting_a_parse_it_should_minimal_output_one_item() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var markdown = "hello"; + + var mockComponentSupplier = new StringComponentSupplier(); + var parser = new MarkdownParser(mockComponentSupplier); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult.Count.Should().BeGreaterThan(0); + } +} diff --git a/test/MarkdownParser.Test/MarkdownParserListComponentSpecs.cs b/test/MarkdownParser.Test/MarkdownParserListComponentSpecs.cs new file mode 100644 index 0000000..7df6698 --- /dev/null +++ b/test/MarkdownParser.Test/MarkdownParserListComponentSpecs.cs @@ -0,0 +1,85 @@ +using FluentAssertions; +using MarkdownParser.Test.Mocks; + +namespace MarkdownParser.Test; + +[TestClass] +public class MarkdownParserListComponentSpecs +{ + [TestMethod] + public void When_parsing_a_list_it_should_output_a_specific_bullet() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var markdown = @" + * item1 + * item2 + * item3 +"; + var mockComponentSupplier = new StringComponentSupplier(); + var parser = new MarkdownParser(mockComponentSupplier); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult.Count.Should().Be(1); + parseResult.First().Should().StartWith("listview>:"); + + var listitems = parseResult.First().Substring(parseResult.First().IndexOf("-listitemview") + 1).Split('-'); + listitems.Length.Should().Be(3); + } + + [TestMethod] + public void When_parsing_a_nested_list_it_should_output_a_specific_nesting() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var markdown = @" +1. Dog + 1. German Shepherd + 2. Belgian Shepherd +"; + var mockComponentSupplier = new StringComponentSupplier(); + var parser = new MarkdownParser(mockComponentSupplier); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult.Count.Should().Be(1); + var view = parseResult.First(); + ContainsCount(view, "listview>:").Should().Be(2); + ContainsCount(view, ""); + splittedViews[1].Should().Be("-listitemview"); + splittedViews[2].Should().Be("_True.1.1_stackview>"); + splittedViews[3].Should().Be("+textview"); + splittedViews[4].Should().Be("Dog+listview>"); + splittedViews[5].Should().Be("-listitemview"); + splittedViews[6].Should().Be("_True.2.1_textview"); + splittedViews[7].Should().Be("German Shepherd-listitemview"); + splittedViews[8].Should().Be("_True.2.2_textview"); + splittedViews[9].Should().Be("Belgian Shepherd withinView.Substring(i)).Count(sub => sub.StartsWith(searchValue)); + } + +} diff --git a/test/MarkdownParser.Test/MarkdownParserSectionsSpecs.cs b/test/MarkdownParser.Test/MarkdownParserSectionsSpecs.cs new file mode 100644 index 0000000..5617526 --- /dev/null +++ b/test/MarkdownParser.Test/MarkdownParserSectionsSpecs.cs @@ -0,0 +1,101 @@ +using System.Text.RegularExpressions; +using FluentAssertions; +using MarkdownParser.Test.Mocks; +using MarkdownParser.Test.Services; + +namespace MarkdownParser.Test; + +[TestClass] +public class MarkdownParserSectionsSpecs +{ + [TestMethod] + public void When_parsing_paragraphs_it_should_output_multiple_text_views_in_good_order() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var markdown = FileReader.ReadFile("Sections.paragraphs.md"); + + var mockComponentSupplier = new StringComponentSupplier(); + var parser = new MarkdownParser(mockComponentSupplier); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult.Count.Should().Be(3); + parseResult[0].Should().StartWith("textview:Paragraphs are"); + parseResult[1].Should().StartWith("textview:2nd paragraph."); + parseResult[2].Should().StartWith("textview:Note that"); + } + + [TestMethod] + public void When_parsing_headers_it_should_output_header_views() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var markdown = FileReader.ReadFile("Sections.headers.md"); + + var mockComponentSupplier = new StringComponentSupplier(); + var parser = new MarkdownParser(mockComponentSupplier); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult.Count.Should().Be(3); + parseResult[0].Should().StartWith("headerview:"); + parseResult[1].Should().StartWith("headerview:"); + parseResult[1].Should().StartWith("headerview:"); + } + + [TestMethod] + public void When_parsing_nested_list_it_should_output_nesting_by_level() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var markdown = FileReader.ReadFile("Sections.nestedlist.md"); + + var mockComponentSupplier = new StringComponentSupplier(); + var parser = new MarkdownParser(mockComponentSupplier); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult.Count.Should().Be(1); // just becuase it only outputs a single string + + var listviewCount = Regex.Matches(parseResult[0], "listview>:").Cast().Count(); + listviewCount.Should().Be(2); + + var splittedViews = parseResult[0].Split(':'); + splittedViews[0].Should().Be("listview>"); + splittedViews[1].Should().Be("-listitemview"); + splittedViews[2].Should().Be("_False.1.1_textview"); + splittedViews[3].Should().Be("item1-listitemview"); + splittedViews[4].Should().Be("_False.1.1_stackview>"); + splittedViews[5].Should().Be("+textview"); + splittedViews[6].Should().Be("item2+listview>"); + splittedViews[7].Should().Be("-listitemview"); + splittedViews[8].Should().Be("_False.2.1_textview"); + splittedViews[9].Should().Be("item2-1-listitemview"); + splittedViews[10].Should().Be("_False.2.1_textview"); + splittedViews[11].Should().Be("item2-2-listitemview"); + splittedViews[12].Should().Be("_False.2.1_textview"); + splittedViews[13].Should().Be("item2-3(mockComponentSupplier); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult.Count.Should().Be(1); + parseResult.First().Should().StartWith("textview:hello"); + } + + [TestMethod] + public void When_parsing_header_it_should_output_a_headerview() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var markdown = @"## An h2 header ##"; + + var mockComponentSupplier = new StringComponentSupplier(); + var parser = new MarkdownParser(mockComponentSupplier); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult.Count.Should().Be(1); + parseResult[0].Should().Be("headerview:2:An h2 header"); + } + + [TestMethod] + public void When_parsing_image_it_should_output_a_imageview() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var markdown = "![example image](http://Example.jpg )"; + + var mockComponentSupplier = new StringComponentSupplier(); + var parser = new MarkdownParser(mockComponentSupplier); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult.Count.Should().Be(1); + parseResult.First().Should().Be("imageview:http://Example.jpg:"); + } + + [TestMethod] + public void When_parsing_image_with_subtitle_it_should_output_a_imageview_and_textview() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var markdown = "![example image](http://Example.jpg \"some comment\")"; + + var mockComponentSupplier = new StringComponentSupplier(); + var parser = new MarkdownParser(mockComponentSupplier); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult.Count.Should().Be(1); + parseResult.First().Should().Be("imageview:http://Example.jpg:some comment"); + } + + [TestMethod] + public void When_parsing_a_listitem_it_should_output_a_listview_with_single_item() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var markdown = @" * item9 "; + + var mockComponentSupplier = new StringComponentSupplier(); + var parser = new MarkdownParser(mockComponentSupplier); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult.Count.Should().Be(1); + parseResult.First().Should().StartWith("listview>:"); + parseResult.First().Should().EndWith(":".Length + 1); // +1 to remove the '-' before 'listitemview' + listitemview = listitemview.Substring(0, listitemview.Length - "(mockComponentSupplier); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult.Count.Should().Be(1); + parseResult.First().Should().StartWith("listview>:"); + + var listitems = parseResult.First().Substring(parseResult.First().IndexOf("-listitemview") + 1).Split('-'); + listitems.Length.Should().Be(3); + } + + [TestMethod] + public void When_parsing_a_block_it_should_output_a_blockview() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var markdown = @" +> Blockquotes are very handy in email to emulate reply text. +> This line is part of the same quote. + +"; + + var expectedResult = "Blockquotes are very handy in email to emulate reply text."; + expectedResult += Environment.NewLine; + expectedResult += "This line is part of the same quote."; + + var mockComponentSupplier = new StringComponentSupplier(); + var parser = new MarkdownParser(mockComponentSupplier); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult.Count.Should().Be(1); + parseResult.First().Should().StartWith("blockquoteview>:"); + parseResult.First().Should().EndWith(":".Length); + content = content.Substring(0, content.Length - "(mockComponentSupplier); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult.Count.Should().Be(1); + parseResult.First().Should().Be("thematicbreakview"); + } + + [TestMethod] + public void When_parsing_a_placeholder_it_should_output_a_PlaceHolderView() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var markdown = @"[my-placeholder] "; + + var mockComponentSupplier = new StringComponentSupplier(); + var parser = new MarkdownParser(mockComponentSupplier); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + var parseResult = parser.Parse(markdown); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + parseResult.Count.Should().Be(1); + parseResult.First().Should().Be("placeholderview:my-placeholder"); + } +} diff --git a/test/MarkdownParser.Test/Mocks/StringComponentSupplier.cs b/test/MarkdownParser.Test/Mocks/StringComponentSupplier.cs new file mode 100644 index 0000000..1f7dd6d --- /dev/null +++ b/test/MarkdownParser.Test/Mocks/StringComponentSupplier.cs @@ -0,0 +1,54 @@ +namespace MarkdownParser.Test.Mocks; + +internal class StringComponentSupplier : IViewSupplier +{ + public string GetTextView(string content) + { + return $"textview:{content}"; + } + + public string GetBlockquotesView(string content) + { + return $"blockquoteview>:{content} items) + { + // Each item will start with a '-' + var listItems = items.Aggregate(string.Empty, (current, item) => current + $"-{item}"); + + return $"listview>:{listItems} childViews) + { + var listItems = childViews.Aggregate(string.Empty, (current, item) => current + $"+{item}"); + + return $"stackview>:{listItems} Block quotes are +> written like so. +> +> They can span multiple paragraphs, +> if you like. + +Use 3 dashes for an em-dash. Use 2 dashes for ranges (ex., "it's all +in chapters 12--14"). Three dots ... will be converted to an ellipsis. +Unicode is supported. ☺ + + + +An h2 header +------------ + +Here's a numbered list: + + 1. first item + 2. second item + 3. third item + +Note again how the actual text starts at 4 columns in (4 characters +from the left side). Here's a code sample: + + # Let me re-iterate ... + for i in 1 .. 10 { do-something(i) } + +As you probably guessed, indented 4 spaces. By the way, instead of +indenting the block, you can use delimited blocks, if you like: + +~~~ +define foobar() { + print "Welcome to flavor country!"; +} +~~~ + +(which makes copying & pasting easier). You can optionally mark the +delimited block for Pandoc to syntax highlight it: + +~~~python +import time +# Quick, count to ten! +for i in range(10): + # (but not *too* quick) + time.sleep(0.5) + print i +~~~ + + + +### An h3 header ### + +A horizontal rule follows. + +*** + +Here's a definition list: + +apples + : Good for making applesauce. +oranges + : Citrus! +tomatoes + : There's no "e" in tomatoe. + +Again, text is indented 4 spaces. (Put a blank line between each +term/definition pair to spread things out more.) + +Here's a "line block": + +| Line one +| Line too +| Line tree + +and images can be specified like so: + +![example image](example-image.jpg "An exemplary image") + +Inline math equations go in like so: $\omega = d\phi / dt$. Display +math should get its own line and be put in in double-dollarsigns: + +And note that you can backslash-escape any punctuation characters +which you wish to be displayed literally, ex.: \`foo\`, \*bar\*, etc. \ No newline at end of file diff --git a/test/MarkdownParser.Test/Resources/Examples/Combined/full-example.md b/test/MarkdownParser.Test/Resources/Examples/Combined/full-example.md new file mode 100644 index 0000000..4f1f876 --- /dev/null +++ b/test/MarkdownParser.Test/Resources/Examples/Combined/full-example.md @@ -0,0 +1,157 @@ +An h1 header +============ + +Paragraphs are separated by a blank line. + +2nd paragraph. *Italic*, **bold**, and `monospace`. Itemized lists +look like: + + * this one + * that one + * the other one + +Note that --- not considering the asterisk --- the actual text +content starts at 4-columns in. + +> Block quotes are +> written like so. +> +> They can span multiple paragraphs, +> if you like. + +Use 3 dashes for an em-dash. Use 2 dashes for ranges (ex., "it's all +in chapters 12--14"). Three dots ... will be converted to an ellipsis. +Unicode is supported. ☺ + + + +An h2 header +------------ + +Here's a numbered list: + + 1. first item + 2. second item + 3. third item + +Note again how the actual text starts at 4 columns in (4 characters +from the left side). Here's a code sample: + + # Let me re-iterate ... + for i in 1 .. 10 { do-something(i) } + +As you probably guessed, indented 4 spaces. By the way, instead of +indenting the block, you can use delimited blocks, if you like: + +~~~ +define foobar() { + print "Welcome to flavor country!"; +} +~~~ + +(which makes copying & pasting easier). You can optionally mark the +delimited block for Pandoc to syntax highlight it: + +~~~python +import time +# Quick, count to ten! +for i in range(10): + # (but not *too* quick) + time.sleep(0.5) + print i +~~~ + + + +### An h3 header ### + +Now a nested list: + + 1. First, get these ingredients: + + * carrots + * celery + * lentils + + 2. Boil some water. + + 3. Dump everything in the pot and follow + this algorithm: + + find wooden spoon + uncover pot + stir + cover pot + balance wooden spoon precariously on pot handle + wait 10 minutes + goto first step (or shut off burner when done) + + Do not bump wooden spoon or it will fall. + +Notice again how text always lines up on 4-space indents (including +that last line which continues item 3 above). + +Here's a link to [a website](http://foo.bar), to a [local +doc](local-doc.html), and to a [section heading in the current +doc](#an-h2-header). Here's a footnote [^1]. + +[^1]: Footnote text goes here. + +Tables can look like this: + +size material color +---- ------------ ------------ +9 leather brown +10 hemp canvas natural +11 glass transparent + +Table: Shoes, their sizes, and what they're made of + +(The above is the caption for the table.) Pandoc also supports +multi-line tables: + +-------- ----------------------- +keyword text +-------- ----------------------- +red Sunsets, apples, and + other red or reddish + things. + +green Leaves, grass, frogs + and other things it's + not easy being. +-------- ----------------------- + +A horizontal rule follows. + +*** + +Here's a definition list: + +apples + : Good for making applesauce. +oranges + : Citrus! +tomatoes + : There's no "e" in tomatoe. + +Again, text is indented 4 spaces. (Put a blank line between each +term/definition pair to spread things out more.) + +Here's a "line block": + +| Line one +| Line too +| Line tree + +and images can be specified like so: + +![example image](example-image.jpg "An exemplary image") + +Inline math equations go in like so: $\omega = d\phi / dt$. Display +math should get its own line and be put in in double-dollarsigns: + +$$I = \int \rho R^{2} dV$$ + +And note that you can backslash-escape any punctuation characters +which you wish to be displayed literally, ex.: \`foo\`, \*bar\*, etc. \ No newline at end of file diff --git a/test/MarkdownParser.Test/Resources/Examples/Combined/minimal-example.md b/test/MarkdownParser.Test/Resources/Examples/Combined/minimal-example.md new file mode 100644 index 0000000..929db7f --- /dev/null +++ b/test/MarkdownParser.Test/Resources/Examples/Combined/minimal-example.md @@ -0,0 +1,43 @@ +An h1 header +============ + +Paragraphs are separated by a blank line. + +Itemized lists +look like: + + * item1 + * item2 + * item3 + +Note that --- not considering the asterisk --- the actual text +content starts at 4-columns in. + +> Block quotes are +> written like so. +> +> They can span multiple paragraphs, +> if you like. + + +An h2 header +------------ + +Here's a numbered list: + + 1. first item + 2. second item + 3. third item + +Note again how the actual text starts at 4 columns in (4 characters +from the left side). Here's a code sample: + + +### An h3 header ### + + +and images can be specified like so: + +![example image](https://upload.wikimedia.org/wikipedia/mediawiki/a/a9/Example.jpg "An exemplary image") + +#[id]: url/to/image "Optional title attribute" \ No newline at end of file diff --git a/test/MarkdownParser.Test/Resources/Examples/Sections/headers.md b/test/MarkdownParser.Test/Resources/Examples/Sections/headers.md new file mode 100644 index 0000000..8ac6c62 --- /dev/null +++ b/test/MarkdownParser.Test/Resources/Examples/Sections/headers.md @@ -0,0 +1,7 @@ +An h1 header +============ + +An h2 header +------------ + +### An h3 header ### diff --git a/test/MarkdownParser.Test/Resources/Examples/Sections/list.md b/test/MarkdownParser.Test/Resources/Examples/Sections/list.md new file mode 100644 index 0000000..367b289 --- /dev/null +++ b/test/MarkdownParser.Test/Resources/Examples/Sections/list.md @@ -0,0 +1,11 @@ +List1 + + * item1 + * item2 + * item3 + +List2 + + * item4 + * item5 + * item6 diff --git a/test/MarkdownParser.Test/Resources/Examples/Sections/nestedlist.md b/test/MarkdownParser.Test/Resources/Examples/Sections/nestedlist.md new file mode 100644 index 0000000..a8b496b --- /dev/null +++ b/test/MarkdownParser.Test/Resources/Examples/Sections/nestedlist.md @@ -0,0 +1,5 @@ +* item1 +* item2 + * item2-1 + * item2-2 + * item2-3 \ No newline at end of file diff --git a/test/MarkdownParser.Test/Resources/Examples/Sections/paragraphs.md b/test/MarkdownParser.Test/Resources/Examples/Sections/paragraphs.md new file mode 100644 index 0000000..dec6d66 --- /dev/null +++ b/test/MarkdownParser.Test/Resources/Examples/Sections/paragraphs.md @@ -0,0 +1,9 @@ +Paragraphs are separated by a blank line. + +2nd paragraph. *Italic*, **bold**, and `monospace`. Itemized lists +look like: (removed) + + + +Note that --- not considering the asterisk --- the actual text +content starts at 4-columns in. diff --git a/test/MarkdownParser.Test/Services/FileReader.cs b/test/MarkdownParser.Test/Services/FileReader.cs new file mode 100644 index 0000000..3c16a9a --- /dev/null +++ b/test/MarkdownParser.Test/Services/FileReader.cs @@ -0,0 +1,17 @@ +using System.Reflection; + +namespace MarkdownParser.Test.Services; + +static class FileReader +{ + public static string ReadFile(string filepath) + { + using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MarkdownParser.Test.Resources.Examples." + filepath)) + { + using (StreamReader reader = new StreamReader(stream)) + { + return reader.ReadToEnd(); + } + } + } +}