├── .github └── workflows │ └── dotnet-core.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── SiteConfig.yml ├── source │ ├── Installing │ │ └── index.md │ ├── apidocs │ │ └── index.md │ ├── assets │ │ └── main.css │ ├── cli │ │ └── index.md │ ├── index.md │ ├── menu.md │ └── templates │ │ └── index.md └── templates │ ├── Default.cshtml │ ├── SiteFrame.cshtml │ └── menu.cshtml └── toolsrc ├── Chloroplast.Core ├── Chloroplast.Core.csproj ├── ChloroplastException.cs ├── Config.cs ├── Config │ └── SiteConfigSource.cs ├── Content │ ├── ContentArea.cs │ ├── DiskFile.cs │ ├── Header.cs │ └── IFile.cs ├── ContentNode.cs ├── Extensions │ ├── CollectionExtensions.cs │ ├── PathExtensions.cs │ └── SiteConfigurationExtensions.cs ├── Loaders │ ├── EcmaXmlExtensions.cs │ ├── EcmaXmlLoader.cs │ └── monodoc-ecma.cs └── Rendering │ ├── ChloroplastTemplateBase.cs │ ├── ContentRenderer.cs │ ├── EcmaXmlRenderer.cs │ ├── MarkdownRenderer.cs │ ├── RazorRenderer.cs │ └── YamlRenderer.cs ├── Chloroplast.Test ├── Chloroplast.Test.csproj ├── EcmaXmlTests.cs ├── MarkdownTests.cs ├── PathTests.cs ├── RazorTests.cs ├── TreeTests.cs └── YamlTests.cs ├── Chloroplast.Tool ├── Chloroplast.Tool.csproj ├── Commands │ ├── FullBuildCommand.cs │ ├── HostCommand.cs │ ├── ICliCommand.cs │ ├── NewCommand.cs │ └── WatchCommand.cs ├── Constants.cs └── Program.cs ├── Chloroplast.Web ├── Chloroplast.Web.csproj ├── Controllers │ └── ContentController.cs ├── Program.cs ├── Startup.cs ├── appsettings.Development.json └── appsettings.json ├── Chloroplast.sln └── index.md /.github/workflows/dotnet-core.yml: -------------------------------------------------------------------------------- 1 | name: .NET Core - build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 11 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Setup .NET Core 21 | uses: actions/setup-dotnet@v1 22 | with: 23 | dotnet-version: 3.1.x 24 | - name: Install dependencies 25 | run: dotnet restore toolsrc 26 | - name: Build 27 | run: dotnet build --configuration Release --no-restore toolsrc 28 | - name: Test 29 | run: dotnet test --no-restore --verbosity normal toolsrc 30 | - name: Pack 31 | run: dotnet pack toolsrc/Chloroplast.Core/ 32 | - name: Pack Tool 33 | run: dotnet pack toolsrc/Chloroplast.Tool/ 34 | - name: Upload a Build Artifact 35 | uses: actions/upload-artifact@v2.1.3 36 | with: 37 | name: chloroplast_tool 38 | path: toolsrc/Chloroplast.Tool/nupkg/*.nupkg 39 | - name: Add private GitHub registry to NuGet 40 | if: github.ref == 'refs/heads/main' 41 | run: dotnet nuget add source https://nuget.pkg.github.com/WildernessLabs/index.json --name "github" --username WildernessLabs --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | - name: Tool Core generated package to GitHub registry 45 | if: github.ref == 'refs/heads/main' 46 | run: dotnet nuget push ./toolsrc/Chloroplast.Core/nupkg/*.nupkg --source "github" --skip-duplicate --no-symbols true 47 | env: 48 | DOTNET_SYSTEM_NET_HTTP_USESOCKETSHTTPHANDLER: 0 49 | - name: Tool Push generated package to GitHub registry 50 | if: github.ref == 'refs/heads/main' 51 | run: dotnet nuget push ./toolsrc/Chloroplast.Tool/nupkg/*.nupkg --source "github" --skip-duplicate --no-symbols true 52 | env: 53 | DOTNET_SYSTEM_NET_HTTP_USESOCKETSHTTPHANDLER: 0 54 | - name: Tool Push generated package to nuget.org registry 55 | if: github.ref == 'refs/heads/main' 56 | run: dotnet nuget push ./toolsrc/Chloroplast.Tool/nupkg/*.nupkg --source "nuget.org" --api-key ${{ secrets.NUGET_API_KEY }} --skip-duplicate --no-symbols true 57 | env: 58 | DOTNET_SYSTEM_NET_HTTP_USESOCKETSHTTPHANDLER: 0 59 | 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # Chloroplast stuff 7 | Temp 8 | out 9 | 10 | # User-specific files 11 | *.rsuser 12 | *.suo 13 | *.user 14 | *.userosscache 15 | *.sln.docstates 16 | launchSettings.json 17 | 18 | # User-specific files (MonoDevelop/Xamarin Studio) 19 | *.userprefs 20 | 21 | # Mono auto generated files 22 | mono_crash.* 23 | 24 | # Build results 25 | [Dd]ebug/ 26 | [Dd]ebugPublic/ 27 | [Rr]elease/ 28 | [Rr]eleases/ 29 | x64/ 30 | x86/ 31 | [Aa][Rr][Mm]/ 32 | [Aa][Rr][Mm]64/ 33 | bld/ 34 | [Bb]in/ 35 | [Oo]bj/ 36 | [Ll]og/ 37 | [Ll]ogs/ 38 | 39 | # Visual Studio 2015/2017 cache/options directory 40 | .vs/ 41 | # Uncomment if you have tasks that create the project's static files in wwwroot 42 | #wwwroot/ 43 | 44 | # Visual Studio 2017 auto generated files 45 | Generated\ Files/ 46 | 47 | # MSTest test Results 48 | [Tt]est[Rr]esult*/ 49 | [Bb]uild[Ll]og.* 50 | 51 | # NUnit 52 | *.VisualState.xml 53 | TestResult.xml 54 | nunit-*.xml 55 | 56 | # Build Results of an ATL Project 57 | [Dd]ebugPS/ 58 | [Rr]eleasePS/ 59 | dlldata.c 60 | 61 | # Benchmark Results 62 | BenchmarkDotNet.Artifacts/ 63 | 64 | # .NET Core 65 | project.lock.json 66 | project.fragment.lock.json 67 | artifacts/ 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Visual Studio code coverage results 146 | *.coverage 147 | *.coveragexml 148 | 149 | # NCrunch 150 | _NCrunch_* 151 | .*crunch*.local.xml 152 | nCrunchTemp_* 153 | 154 | # MightyMoose 155 | *.mm.* 156 | AutoTest.Net/ 157 | 158 | # Web workbench (sass) 159 | .sass-cache/ 160 | 161 | # Installshield output folder 162 | [Ee]xpress/ 163 | 164 | # DocProject is a documentation generator add-in 165 | DocProject/buildhelp/ 166 | DocProject/Help/*.HxT 167 | DocProject/Help/*.HxC 168 | DocProject/Help/*.hhc 169 | DocProject/Help/*.hhk 170 | DocProject/Help/*.hhp 171 | DocProject/Help/Html2 172 | DocProject/Help/html 173 | 174 | # Click-Once directory 175 | publish/ 176 | 177 | # Publish Web Output 178 | *.[Pp]ublish.xml 179 | *.azurePubxml 180 | # Note: Comment the next line if you want to checkin your web deploy settings, 181 | # but database connection strings (with potential passwords) will be unencrypted 182 | *.pubxml 183 | *.publishproj 184 | 185 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 186 | # checkin your Azure Web App publish settings, but sensitive information contained 187 | # in these scripts will be unencrypted 188 | PublishScripts/ 189 | 190 | # NuGet Packages 191 | *.nupkg 192 | # NuGet Symbol Packages 193 | *.snupkg 194 | # The packages folder can be ignored because of Package Restore 195 | **/[Pp]ackages/* 196 | # except build/, which is used as an MSBuild target. 197 | !**/[Pp]ackages/build/ 198 | # Uncomment if necessary however generally it will be regenerated when needed 199 | #!**/[Pp]ackages/repositories.config 200 | # NuGet v3's project.json files produces more ignorable files 201 | *.nuget.props 202 | *.nuget.targets 203 | 204 | # Microsoft Azure Build Output 205 | csx/ 206 | *.build.csdef 207 | 208 | # Microsoft Azure Emulator 209 | ecf/ 210 | rcf/ 211 | 212 | # Windows Store app package directories and files 213 | AppPackages/ 214 | BundleArtifacts/ 215 | Package.StoreAssociation.xml 216 | _pkginfo.txt 217 | *.appx 218 | *.appxbundle 219 | *.appxupload 220 | 221 | # Visual Studio cache files 222 | # files ending in .cache can be ignored 223 | *.[Cc]ache 224 | # but keep track of directories ending in .cache 225 | !?*.[Cc]ache/ 226 | 227 | # Others 228 | ClientBin/ 229 | ~$* 230 | *~ 231 | *.dbmdl 232 | *.dbproj.schemaview 233 | *.jfm 234 | *.pfx 235 | *.publishsettings 236 | orleans.codegen.cs 237 | 238 | # Including strong name files can present a security risk 239 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 240 | #*.snk 241 | 242 | # Since there are multiple workflows, uncomment next line to ignore bower_components 243 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 244 | #bower_components/ 245 | 246 | # RIA/Silverlight projects 247 | Generated_Code/ 248 | 249 | # Backup & report files from converting an old project file 250 | # to a newer Visual Studio version. Backup files are not needed, 251 | # because we have git ;-) 252 | _UpgradeReport_Files/ 253 | Backup*/ 254 | UpgradeLog*.XML 255 | UpgradeLog*.htm 256 | ServiceFabricBackup/ 257 | *.rptproj.bak 258 | 259 | # SQL Server files 260 | *.mdf 261 | *.ldf 262 | *.ndf 263 | 264 | # Business Intelligence projects 265 | *.rdl.data 266 | *.bim.layout 267 | *.bim_*.settings 268 | *.rptproj.rsuser 269 | *- [Bb]ackup.rdl 270 | *- [Bb]ackup ([0-9]).rdl 271 | *- [Bb]ackup ([0-9][0-9]).rdl 272 | 273 | # Microsoft Fakes 274 | FakesAssemblies/ 275 | 276 | # GhostDoc plugin setting file 277 | *.GhostDoc.xml 278 | 279 | # Node.js Tools for Visual Studio 280 | .ntvs_analysis.dat 281 | node_modules/ 282 | 283 | # Visual Studio 6 build log 284 | *.plg 285 | 286 | # Visual Studio 6 workspace options file 287 | *.opt 288 | 289 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 290 | *.vbw 291 | 292 | # Visual Studio LightSwitch build output 293 | **/*.HTMLClient/GeneratedArtifacts 294 | **/*.DesktopClient/GeneratedArtifacts 295 | **/*.DesktopClient/ModelManifest.xml 296 | **/*.Server/GeneratedArtifacts 297 | **/*.Server/ModelManifest.xml 298 | _Pvt_Extensions 299 | 300 | # Paket dependency manager 301 | .paket/paket.exe 302 | paket-files/ 303 | 304 | # FAKE - F# Make 305 | .fake/ 306 | 307 | # CodeRush personal settings 308 | .cr/personal 309 | 310 | # Python Tools for Visual Studio (PTVS) 311 | __pycache__/ 312 | *.pyc 313 | 314 | # Cake - Uncomment if you are using it 315 | # tools/** 316 | # !tools/packages.config 317 | 318 | # Tabs Studio 319 | *.tss 320 | 321 | # Telerik's JustMock configuration file 322 | *.jmconfig 323 | 324 | # BizTalk build output 325 | *.btp.cs 326 | *.btm.cs 327 | *.odx.cs 328 | *.xsd.cs 329 | 330 | # OpenCover UI analysis results 331 | OpenCover/ 332 | 333 | # Azure Stream Analytics local run output 334 | ASALocalRun/ 335 | 336 | # MSBuild Binary and Structured Log 337 | *.binlog 338 | 339 | # NVidia Nsight GPU debugger configuration file 340 | *.nvuser 341 | 342 | # MFractors (Xamarin productivity tool) working folder 343 | .mfractor/ 344 | 345 | # Local History for Visual Studio 346 | .localhistory/ 347 | 348 | # BeatPulse healthcheck temp database 349 | healthchecksdb 350 | 351 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 352 | MigrationBackup/ 353 | 354 | # Ionide (cross platform F# VS Code tools) working folder 355 | .ionide/ 356 | 357 | 358 | # MacOS shit 359 | **/.DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chloroplast 2 | Wilderness Labs docs engine that converts and hosts Markdown and (soon) XML API docs into HTML. 3 | 4 | ## Getting Started 5 | 6 | First [Install](/docs/source/Installing/index.md) the `choloroplast.tool` dotnet tool. 7 | 8 | Then run the following commands to: create a new Chloroplast project at `targetFolder`, then build it, and run the local Chloroplast webserver to preview the docs. 9 | 10 | ``` 11 | chloroplast new conceptual targetFolder 12 | 13 | cd targetFolder 14 | 15 | chloroplast build 16 | chloroplast host 17 | ``` 18 | 19 | Once you're done with that, you can modify that default template, and [read the docs](/docs/). 20 | 21 | ## Host the Chloroplast docs 22 | 23 | If you want to see Chloroplast in action, you can even host the Chloroplast docs themselves. 24 | 25 | 1. Clone the Chloroplast repo. 26 | 27 | ``` 28 | git clone https://github.com/WildernessLabs/Chloroplast.git 29 | cd Chloroplast 30 | ``` 31 | 32 | 1. Install Chloroplast. 33 | 34 | There are [several ways to install Chloroplast](https://github.com/WildernessLabs/Chloroplast/blob/main/docs/source/Installing/index.md#package-repository). This will install it from NuGet as a global tool. 35 | 36 | ``` 37 | dotnet tool install Chloroplast.Tool -g 38 | ``` 39 | 40 | 1. Navigate to the Chloroplast docs. 41 | 42 | ``` 43 | cd docs 44 | ``` 45 | 46 | 1. Build and host the docs. 47 | 48 | ``` 49 | chloroplast build 50 | chloroplast host 51 | ``` 52 | 53 | This will build and then start hosting the docs from `localhost:5000`, launching a browser to preview them. 54 | -------------------------------------------------------------------------------- /docs/SiteConfig.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Site configuration file sample for chloroplast 3 | 4 | # site basics 5 | title: Chloroplast Docs 6 | email: hello@wildernesslabs.co 7 | description: >- 8 | Chloroplast by Wilderness Labs docs. 9 | 10 | # razor templates 11 | templates_folder: templates 12 | 13 | # main site folder 14 | areas: 15 | - source_folder: /source 16 | output_folder: / 17 | -------------------------------------------------------------------------------- /docs/source/Installing/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | template: Default 3 | title: Installing Chloroplast 4 | --- 5 | 6 | # Dependencies 7 | 8 | You must have the `dotnet` CLI installed ... to do so, you need only install _.NET Core_ on your machine. The instructions for that can be found here: 9 | 10 | https://docs.microsoft.com/en-us/dotnet/core/install/ 11 | 12 | # Installing Chloroplast 13 | 14 | You can acquire Chloroplast by installing it as either a [local or global 15 | tool](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-tool-install). 16 | 17 | 18 | ## Wilderness Labs package registry 19 | 20 | ### Recommended 21 | Chloroplast can be found on the public nuget.org package registry at: 22 | https://www.nuget.org/packages/Chloroplast.Tool/ 23 | 24 | ### Package Repository 25 | However you can also choose to install it through the Wilderness Labs package 26 | registry. This means you'll need to add the authenticated NuGet source as follows if you wish to do so. 27 | 28 | ``` 29 | dotnet nuget add source https://nuget.pkg.github.com/WildernessLabs/index.json --name “wildernesslabs” --username << your github username >> --password << a personal access token >> 30 | ``` 31 | 32 | ## Global 33 | Regardless of which package registry you choose, you can install Chloroplast as a global tool. 34 | 35 | ``` 36 | dotnet tool install Chloroplast.Tool -g 37 | ``` 38 | 39 | If you install it as a global tool, you can invoke it with the tool name 40 | `chloroplast`. 41 | 42 | ## Local 43 | Alternatively, you can install it as a local tool by first creating a local 44 | tool manifest in the project folder, as follows: 45 | 46 | ``` 47 | dotnet new tool-manifest 48 | dotnet tool install Chloroplast.Tool 49 | ``` 50 | 51 | If you install it as a local tool, you must prefix the tool name, so you can invoke it with `dotnet chloroplast`. 52 | 53 | ## Updating 54 | 55 | You can update to the latest release at any point by running the following command (with or without the `-g` depending on your preference): 56 | 57 | ``` 58 | dotnet tool update Chloroplast.Tool -g 59 | ``` 60 | -------------------------------------------------------------------------------- /docs/source/apidocs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | template: Default 3 | title: API Docs 4 | --- 5 | 6 | Chloroplast supports documenting .NET APIs. 7 | 8 | **PLEASE NOTE:** This functionality is still in development and not fully released yet. 9 | 10 | ## Build Process 11 | The API build process is inherently a multi-step process, for which we'll use a few different tools: 12 | 13 | - [mdoc](https://github.com/mono/api-doc-tools/): This tool takes compiled .NET assemblies, and produces an XML format called EcmaXml, which represents all of the type and namespace metadata, and gives you an opportunity to document your APIs. 14 | - msbuild: We are going to use msbuild to fetch mdoc itself, along with all the APIs that you need documented. 15 | - Build Server: The glue that will drive and automate this process. In this document, we will use GitHub Actions to demonstrate the capabilities, but this can honestly be built with any other build pipeline such as Azure DevOps. 16 | 17 | ## Using MSBuild to Drive APIs 18 | 19 | ```xml 20 | 21 | 22 | 23 | netstandard2.1 24 | 5.8.2 25 | $(NuGetPackageRoot)mdoc\$(MdocVersion)\tools\mdoc.exe 26 | tmp/EcmaXml 27 | 28 | 0.5.0 29 | $(NuGetPackageRoot)chloroplast.core\$(ChloroplastCoreVersion)\lib\$(TargetFramework)\Chloroplast.Core.dll 30 | 31 | $(NuGetPackageRoot)minirazor\2.0.3\lib\$(TargetFramework)\ 32 | $(NuGetPackageRoot)Microsoft.Extensions.Configuration.Abstractions\5.0.0\lib\netstandard2.0\ 33 | 34 | $(ChloroplastCorePath) 35 | -L $(NuGetPackageRoot) 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | ``` 51 | 52 | ## Using GitHub Actions to Automate Builds 53 | 54 | ```yaml 55 | name: Content Build 56 | 57 | on: 58 | workflow_dispatch: 59 | push: 60 | branches: [ main ] 61 | pull_request: 62 | branches: [ main ] 63 | 64 | jobs: 65 | build: 66 | runs-on: ubuntu-latest 67 | 68 | steps: 69 | - uses: actions/checkout@v2 70 | 71 | - name: Setup .NET Core 72 | uses: actions/setup-dotnet@v1 73 | with: 74 | dotnet-version: 3.1.x 75 | 76 | - name: Add private GitHub registry to NuGet 77 | run: dotnet nuget add source https://nuget.pkg.github.com/WildernessLabs/index.json --name "wildernesslabs" --username wildlingdev --password ${{ secrets.WILDLINGDEV_PAT }} --store-password-in-clear-text 78 | 79 | - name: Build APIs with mdoc 80 | run: | 81 | cd $GITHUB_WORKSPACE 82 | dotnet restore mdocbuild.csproj 83 | msbuild mdocbuild.csproj /t:mdocupdate 84 | 85 | - name: Install Chloroplast 86 | run: | 87 | dotnet new tool-manifest 88 | dotnet tool install Chloroplast.Tool 89 | 90 | # Runs a set of commands using the runners shell 91 | - name: Run a multi-line script 92 | run: | 93 | cd $GITHUB_WORKSPACE 94 | ls . 95 | dotnet chloroplast 96 | ls . 97 | 98 | - name: Upload a Build Artifact 99 | uses: actions/upload-artifact@v2.1.3 100 | with: 101 | name: output 102 | path: out/* 103 | 104 | - name: Publish to Web 105 | if: github.ref == 'refs/heads/main' 106 | uses: bacongobbler/azure-blob-storage-upload@v1.2.0 107 | with: 108 | source_dir: out 109 | container_name: $web 110 | connection_string: ${{ secrets.STORAGE_CONNECTION }} 111 | sync: true 112 | 113 | ``` 114 | 115 | ## Supporting DocXML/Triple Slash docs 116 | 117 | WIP ... 118 | -------------------------------------------------------------------------------- /docs/source/assets/main.css: -------------------------------------------------------------------------------- 1 | h1 2 | { 3 | margin:0; 4 | padding:10px; 5 | width:100%; 6 | background-color: darkgray; 7 | } 8 | 9 | #menu 10 | { 11 | float:left; 12 | width: 300px; 13 | background-color: white; 14 | } 15 | 16 | #maincontent 17 | { 18 | padding: 5px; 19 | } 20 | 21 | #body 22 | { 23 | margin-left: 300px; 24 | } 25 | 26 | #footer 27 | { 28 | border-top: 1px solid black; 29 | } -------------------------------------------------------------------------------- /docs/source/cli/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | template: Default 3 | title: Command Line Interface 4 | --- 5 | 6 | Chloroplast supports various sub commands. It is recommended to first `cd` into your project's root folder, and run the commands with defaults as appropriate. 7 | 8 | ## `new` sub command 9 | 10 | The new subcommand allows you to bootstrap a chloroplast site by 11 | copying files to a destination folder. 12 | 13 | ``` 14 | chloroplast new conceptual targetFolder 15 | ``` 16 | 17 | This will create `targetFolder` (if it doesn't already exist), and 18 | copy files from the `conceptual` template. You can then run the 19 | following commands to build and preview the site locally: 20 | 21 | ``` 22 | cd targetFolder 23 | chloroplast build 24 | chloroplast host 25 | ``` 26 | 27 | ### parameters 28 | 29 | - `conceptual`: The second positional parameter after `new`. 30 | - This will currently only support `conceptual`, which will be a copy of these docs. 31 | - Eventually we will have more templates, and eventually open up to community contributions. 32 | - `targetFolder`: the third positional parameter after `new` is the name of the target folder where the template files will be placed. 33 | 34 | ## `build` sub command 35 | 36 | The build subcommand will build a Chloroplast site 37 | 38 | ``` 39 | chloroplast build --root path/to/SiteConfig/ --out path/to/out 40 | ``` 41 | 42 | ### parameters 43 | 44 | - `root`: the path to the directory that contains a `SiteConfig.yml` file 45 | - `out`: the path to the directory where the resulting HTML will be output. 46 | - `normalizePaths`: defaults to `true`, which normalizes all paths to lower case. Set this to false for output paths to match the source casing. 47 | 48 | The parameters above will default to the current working directory for the root path, and an `out/` directory in that same path if both parameters are omitted. 49 | 50 | The build command will render all `index.md` files into corresponding `index.html` files, and copy everything else to the same corresponding location (images, styles, etc). 51 | 52 | ## `host` sub command 53 | 54 | The host sub command starts a simple HTML web server. Useful for local preview during development or even authoring. If you update markdown files, the content will automatically be rebuilt so you can just refresh the browser. 55 | 56 | ``` 57 | chloroplast host --root path/to/root --out path/to/html 58 | ``` 59 | 60 | You can just press the enter key to end this task after stopping the web host with Ctrl+C at any time. 61 | 62 | If you are updating razor templates, you can also just run this command in a separate terminal window and leave it running. That way, as you run the full `build` subcommand, the content and front-end styles will update, and you can just refresh the browser after that build has completed. 63 | 64 | ### parameters 65 | 66 | - `out`: This should be the same value as the `out` parameter used in the `build` command. 67 | 68 | The parameter above will default to the `out/` folder in the current working directory, if omitted. 69 | -------------------------------------------------------------------------------- /docs/source/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | template: Default 3 | title: Chloroplast Home 4 | subtitle: Documentation site for Chloroplast. 5 | --- 6 | 7 | # Welcome! 8 | 9 | To the Chloroplast docs. Once you [install it](/Installing), you can learn how to [use it](/cli), and [template it](/templates). 10 | 11 | The source code can be found here: 12 | https://github.com/wildernesslabs/Chloroplast 13 | -------------------------------------------------------------------------------- /docs/source/menu.md: -------------------------------------------------------------------------------- 1 | --- 2 | template: menu 3 | navTree: 4 | - title: Installing 5 | path: Installing/ 6 | - title: Command Line 7 | path: cli/ 8 | - title: Templates 9 | path: templates/ 10 | --- 11 | Copyright 2020 Wilderness Labs -------------------------------------------------------------------------------- /docs/source/templates/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | template: Default 3 | title: Templating 4 | --- 5 | 6 | Chloroplast templates are built using ASP.NET's Razor templates. 7 | 8 | # Configuration 9 | 10 | The `SiteConfig.yml` file lets you configure the location of templates and the folders to find content files to process. 11 | 12 | ```yaml 13 | # razor templates 14 | templates_folder: templates 15 | 16 | areas: 17 | - source_folder: /source 18 | output_folder: / 19 | ``` 20 | 21 | # Markdown front matter 22 | 23 | The template will be defined by the content's front matter. 24 | 25 | ``` 26 | --- 27 | template: TemplateName 28 | title: Document title 29 | --- 30 | ``` 31 | 32 | If this value is either incorrect, or omitted, it will default to look for a template named `Default.cshtml`. These templates are assumed to be just for the content area itself. 33 | 34 | # SiteFrame.cshtml 35 | 36 | For the main site's chrome, Chloroplast will look for a template named `SiteFrame.cshtml` ... this will render 37 | 38 | ``` 39 | 40 | 41 | @Model.GetMeta("Title") 42 | 43 | 44 |
45 | @Raw(Model.Body) 46 |
47 | 48 | 49 | ``` 50 | 51 | # Template Properties and Methods 52 | 53 | Available to all templates are the following functions: 54 | 55 | - `@Raw("")` 56 | - the `Raw` method will avoid HTML encoding content (which is the default). 57 | - `@Model.Body` 58 | - This is the main HTML content being rendered in this template. You should output this through the `Raw` method since it will likely contain HTML. 59 | - `@Model.GetMeta("title")` 60 | - with this you can access any value in the front matter (for example, `title`). 61 | - `@await PartialAsync("TemplateName", "model value")` 62 | - This lets you render sub templates. 63 | - the template name parameter will match the razor fil.e name ... so in the example above, it would be looking for a file named `Templates/TemplateName.cshtml` 64 | - the second parameter can be any kind of object really, whatever the template in question is expecting. 65 | - `@Model.Headers` collection 66 | - Each `h1`-`h6` will be parsed and added to this headers collection. 67 | - Every `Header` object has the following properties: 68 | - Level: the numeric value of the header 69 | - Value: the text 70 | - Slug: a slug version of the `Value`, which corresponds to an anchor added to the HTML before the header. 71 | - `@await PartialAsync("Docs/area/menu.md")` 72 | - This overload of the `PartialAsync` method assumes you're rendering a markdown file. 73 | - The markdown file can define its own template (will default to `Default`), and will only render the contents of that specific template (ie. no `SiteFrame.cshtml`). 74 | -------------------------------------------------------------------------------- /docs/templates/Default.cshtml: -------------------------------------------------------------------------------- 1 | @inherits Chloroplast.Core.Rendering.ChloroplastTemplateBase 2 |
3 |
4 | @await PartialAsync("source/menu.md") 5 |
6 |

@Model.GetMeta("title")

7 | @if (@Model.HasMeta("subtitle")) 8 | { 9 |

@Model.GetMeta("subtitle")

10 | } 11 | 12 | @Raw(Model.Body) 13 |
14 |
15 |
-------------------------------------------------------------------------------- /docs/templates/SiteFrame.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | @Model.GetMeta("Title") 7 | 8 | 9 | 28 | 29 | 30 |
31 |
32 |

Chloroplast Docs

33 |
34 |
35 | 36 | @Raw(Model.Body) 37 | 38 |
39 | 46 | 49 |
50 | 51 | 52 | -------------------------------------------------------------------------------- /docs/templates/menu.cshtml: -------------------------------------------------------------------------------- 1 | @using System.Linq; 2 | @using System.Collections.Generic; 3 | @using Microsoft.Extensions.Configuration; 4 | @using Chloroplast.Core.Extensions; 5 | @{ 6 | string RenderNodes(IConfigurationSection section) 7 | { 8 | var children = section.GetChildren(); 9 | if (children.Any()) 10 | { 11 | 23 | } 24 | return ""; 25 | } 26 | } 27 | 28 |
29 | @RenderNodes(Model.Metadata.GetSection ("navTree")) 30 |
-------------------------------------------------------------------------------- /toolsrc/Chloroplast.Core/Chloroplast.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp8.0 5 | 6 | ./nupkg 7 | true 8 | 9 | 0.5.3 10 | Wilderness Labs 11 | Wilderness Labs 12 | Core library for Chloroplast.Tool 13 | https://github.com/WildernessLabs/Chloroplast 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /toolsrc/Chloroplast.Core/ChloroplastException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | namespace Chloroplast.Core 3 | { 4 | public class ChloroplastException : ApplicationException 5 | { 6 | public ChloroplastException (string message) : base (message) { } 7 | public ChloroplastException (string message, Exception inner) : base (message, inner) { } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /toolsrc/Chloroplast.Core/Config.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.Configuration; 3 | 4 | namespace Chloroplast.Core 5 | { 6 | public static class SiteConfig 7 | { 8 | public static IConfigurationRoot Instance { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /toolsrc/Chloroplast.Core/Config/SiteConfigSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.IO; 5 | using Microsoft.Extensions.Configuration; 6 | using YamlDotNet.RepresentationModel; 7 | 8 | namespace Chloroplast.Core.Config 9 | { 10 | public class FrontMatterConfigSource : StreamConfigurationSource 11 | { 12 | string yaml; 13 | public FrontMatterConfigSource (string yamlsource) => this.yaml = yamlsource; 14 | 15 | public override IConfigurationProvider Build (IConfigurationBuilder builder) 16 | { 17 | var mem = new MemoryStream (); 18 | var stringBytes = System.Text.Encoding.UTF8.GetBytes (yaml); 19 | mem.Write (stringBytes, 0, stringBytes.Length); 20 | mem.Seek (0, SeekOrigin.Begin); 21 | this.Stream = mem; 22 | return new FrontMatterConfigurationProvider (this); 23 | } 24 | } 25 | 26 | public class FrontMatterConfigurationProvider : StreamConfigurationProvider 27 | { 28 | public FrontMatterConfigurationProvider (FrontMatterConfigSource source) : base (source) { } 29 | 30 | public override void Load (Stream stream) 31 | { 32 | var parser = new SiteConfigurationFileParser (); 33 | 34 | Data = parser.Parse (stream); 35 | } 36 | } 37 | 38 | public class SiteConfigSource : FileConfigurationSource 39 | { 40 | public override IConfigurationProvider Build (IConfigurationBuilder builder) 41 | { 42 | FileProvider = FileProvider ?? builder.GetFileProvider (); 43 | return new SiteConfigurationProvider (this); 44 | } 45 | } 46 | 47 | public class SiteConfigurationProvider : FileConfigurationProvider 48 | { 49 | public SiteConfigurationProvider (FileConfigurationSource source) : base (source) { } 50 | 51 | public override void Load (Stream stream) 52 | { 53 | var parser = new SiteConfigurationFileParser (); 54 | 55 | Data = parser.Parse (stream); 56 | } 57 | } 58 | 59 | internal class SiteConfigurationFileParser 60 | { 61 | private readonly IDictionary _data = new SortedDictionary (StringComparer.OrdinalIgnoreCase); 62 | private readonly Stack _context = new Stack (); 63 | private string _currentPath; 64 | 65 | public IDictionary Parse (string input) 66 | { 67 | using (var stream = new MemoryStream ()) 68 | using (var writer = new StreamWriter (stream)) 69 | { 70 | writer.Write (input); 71 | writer.Flush (); 72 | stream.Position = 0; 73 | return Parse (stream); 74 | } 75 | } 76 | 77 | public IDictionary Parse (Stream input) 78 | { 79 | return Parse (new StreamReader (input, detectEncodingFromByteOrderMarks: true)); 80 | } 81 | 82 | public IDictionary Parse (StreamReader input) 83 | { 84 | _data.Clear (); 85 | _context.Clear (); 86 | 87 | // https://dotnetfiddle.net/rrR2Bb 88 | var yaml = new YamlStream (); 89 | yaml.Load (input); 90 | 91 | if (yaml.Documents.Any ()) 92 | { 93 | var mapping = (YamlMappingNode)yaml.Documents[0].RootNode; 94 | 95 | // The document node is a mapping node 96 | VisitYamlMappingNode (mapping); 97 | } 98 | 99 | return _data; 100 | } 101 | 102 | private void VisitYamlNodePair (KeyValuePair yamlNodePair) 103 | { 104 | var context = ((YamlScalarNode)yamlNodePair.Key).Value; 105 | VisitYamlNode (context, yamlNodePair.Value); 106 | } 107 | 108 | private void VisitYamlNode (string context, YamlNode node) 109 | { 110 | if (node is YamlScalarNode scalarNode) 111 | { 112 | VisitYamlScalarNode (context, scalarNode); 113 | } 114 | if (node is YamlMappingNode mappingNode) 115 | { 116 | VisitYamlMappingNode (context, mappingNode); 117 | } 118 | if (node is YamlSequenceNode sequenceNode) 119 | { 120 | VisitYamlSequenceNode (context, sequenceNode); 121 | } 122 | } 123 | 124 | private void VisitYamlScalarNode (string context, YamlScalarNode yamlValue) 125 | { 126 | //a node with a single 1-1 mapping 127 | EnterContext (context); 128 | var currentKey = _currentPath; 129 | 130 | if (_data.ContainsKey (currentKey)) 131 | { 132 | throw new FormatException ($"duplicate key: {currentKey}"); 133 | } 134 | 135 | _data[currentKey] = IsNullValue (yamlValue) ? null : yamlValue.Value; 136 | ExitContext (); 137 | } 138 | 139 | private void VisitYamlMappingNode (YamlMappingNode node) 140 | { 141 | foreach (var yamlNodePair in node.Children) 142 | { 143 | VisitYamlNodePair (yamlNodePair); 144 | } 145 | } 146 | 147 | private void VisitYamlMappingNode (string context, YamlMappingNode yamlValue) 148 | { 149 | //a node with an associated sub-document 150 | EnterContext (context); 151 | 152 | VisitYamlMappingNode (yamlValue); 153 | 154 | ExitContext (); 155 | } 156 | 157 | private void VisitYamlSequenceNode (string context, YamlSequenceNode yamlValue) 158 | { 159 | //a node with an associated list 160 | EnterContext (context); 161 | 162 | VisitYamlSequenceNode (yamlValue); 163 | 164 | ExitContext (); 165 | } 166 | 167 | private void VisitYamlSequenceNode (YamlSequenceNode node) 168 | { 169 | for (int i = 0; i < node.Children.Count; i++) 170 | { 171 | VisitYamlNode (i.ToString (), node.Children[i]); 172 | } 173 | } 174 | 175 | private void EnterContext (string context) 176 | { 177 | _context.Push (context); 178 | _currentPath = ConfigurationPath.Combine (_context.Reverse ()); 179 | } 180 | 181 | private void ExitContext () 182 | { 183 | _context.Pop (); 184 | _currentPath = ConfigurationPath.Combine (_context.Reverse ()); 185 | } 186 | 187 | private bool IsNullValue (YamlScalarNode yamlValue) 188 | { 189 | return yamlValue.Style == YamlDotNet.Core.ScalarStyle.Plain 190 | && ( 191 | yamlValue.Value == "~" 192 | || yamlValue.Value == "null" 193 | || yamlValue.Value == "Null" 194 | || yamlValue.Value == "NULL" 195 | ); 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /toolsrc/Chloroplast.Core/Content/ContentArea.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using Chloroplast.Core.Extensions; 5 | using Chloroplast.Core.Loaders; 6 | using Microsoft.Extensions.Configuration; 7 | 8 | namespace Chloroplast.Core.Content 9 | { 10 | public abstract class ContentArea 11 | { 12 | public string SourcePath { get; set; } 13 | public string TargetPath { get; set; } 14 | public string RootRelativePath { get; set; } 15 | public abstract IList ContentNodes { get; } 16 | 17 | public static IEnumerable LoadContentAreas (IConfigurationRoot config) 18 | { 19 | 20 | string rootDirectory = config["root"].NormalizePath (); 21 | string outDirectory = config["out"].NormalizePath (); 22 | bool normalizePaths = config.GetBool ("normalizePaths", defaultValue: true); 23 | 24 | // individual files 25 | 26 | var fileConfigs = config.GetSection ("files"); 27 | if (fileConfigs != null) 28 | { 29 | foreach (var fileConfig in fileConfigs.GetChildren ()) 30 | { 31 | var area = new IndividualContentArea 32 | { 33 | SourcePath = rootDirectory.CombinePath (fileConfig["source_file"]), 34 | TargetPath = outDirectory.CombinePath (fileConfig["output_folder"].NormalizePath (toLower: normalizePaths)), 35 | RootRelativePath = fileConfig["output_folder"].Replace ("index.html", "").NormalizePath (toLower: normalizePaths) 36 | }; 37 | 38 | yield return area; 39 | } 40 | } 41 | 42 | // areas 43 | var areaConfigs = config.GetSection ("areas"); 44 | 45 | if (areaConfigs != null) 46 | { 47 | foreach (var areaConfig in areaConfigs.GetChildren ()) 48 | { 49 | var area = new GroupContentArea 50 | { 51 | SourcePath = rootDirectory.CombinePath (areaConfig["source_folder"]), 52 | 53 | TargetPath = outDirectory.CombinePath (areaConfig["output_folder"].NormalizePath (toLower: normalizePaths)), 54 | RootRelativePath = areaConfig["output_folder"].Replace ("index.html", "").NormalizePath (toLower: normalizePaths), 55 | NormalizePaths = normalizePaths, 56 | AreaType = areaConfig["type"] 57 | }; 58 | 59 | // TODO: validate values 60 | 61 | yield return area; 62 | } 63 | } 64 | } 65 | } 66 | 67 | public class IndividualContentArea : ContentArea 68 | { 69 | public override IList ContentNodes 70 | { 71 | get 72 | { 73 | return new[] { 74 | new ContentNode 75 | { 76 | Slug = Path.GetDirectoryName (this.SourcePath), 77 | Source = new DiskFile (this.SourcePath, this.SourcePath), 78 | Target = new DiskFile (this.TargetPath, this.TargetPath), 79 | Area = this 80 | } 81 | }.ToList(); 82 | } 83 | } 84 | } 85 | 86 | public class GroupContentArea : ContentArea 87 | { 88 | List nodes; 89 | 90 | public bool NormalizePaths { get; set; } 91 | 92 | string areaType = "markdown"; 93 | public string AreaType 94 | { 95 | get => areaType; 96 | set 97 | { 98 | if (!string.IsNullOrWhiteSpace (value)) 99 | areaType = value; 100 | } 101 | } 102 | 103 | public GroupContentArea () 104 | { 105 | } 106 | 107 | public GroupContentArea (IEnumerable inputNodes) 108 | { 109 | this.nodes = new List (inputNodes); 110 | } 111 | 112 | public override IList ContentNodes 113 | { 114 | get 115 | { 116 | if (nodes == null) 117 | { 118 | string menuPath = string.Empty; 119 | 120 | nodes = Directory 121 | .GetFiles (this.SourcePath, "*.*", SearchOption.AllDirectories) 122 | .OrderBy (p => p) 123 | .Select (p => 124 | { 125 | var relative = p.RelativePath (SourcePath); 126 | var targetrelative = relative.NormalizePath (toLower: this.NormalizePaths); 127 | 128 | if (targetrelative.EndsWith (".md")) 129 | targetrelative = targetrelative.Substring (0, targetrelative.Length - 3) + ".html"; 130 | 131 | else if (targetrelative.EndsWith (".xml")) 132 | { 133 | targetrelative = targetrelative.Substring (0, targetrelative.Length - 4) + ".html"; 134 | if (targetrelative.Contains ("ns-")) 135 | { 136 | // this is a namespace, let's switch the target filename 137 | var filename = Path.GetFileNameWithoutExtension (targetrelative).Replace ("ns-", string.Empty); 138 | var folder = Path.GetDirectoryName (targetrelative); 139 | targetrelative = Path.Combine (folder, filename, "index.html"); 140 | } 141 | else if (targetrelative.EndsWith ("index.html")) 142 | { 143 | // let's parse this and pull out menu information 144 | var indexFile = new DiskFile (p, relative); 145 | var XmlForIndex = indexFile.ReadContentAsync ().Result; 146 | var index = EcmaXmlLoader.LoadXIndex (XmlForIndex); 147 | 148 | // TODO: 'api/' should be coming from configuration 149 | var apiRootPath = $"/api/{Path.GetFileName (Path.GetDirectoryName (p)).ToLower ()}"; 150 | string docSetMenu = index.ToMenu (apiRootPath); 151 | var indexMenuPath = TargetPath.CombinePath ("menu.md"); 152 | var indexMenuFile = new DiskFile (indexMenuPath, "menu.md"); 153 | menuPath = indexMenuPath; 154 | indexMenuFile.WriteContentAsync (docSetMenu).Wait (); 155 | } 156 | } 157 | 158 | var targetFile = TargetPath.CombinePath (targetrelative); 159 | var node = new ContentNode 160 | { 161 | Slug = Path.GetDirectoryName (relative), 162 | Source = new DiskFile (p, relative), 163 | Target = new DiskFile (targetFile, targetrelative), 164 | MenuPath = menuPath, 165 | Area = this 166 | }; 167 | 168 | return node; 169 | }).ToList (); 170 | } 171 | return nodes; 172 | } 173 | } 174 | 175 | 176 | 177 | /// 178 | /// Using heuristics to build the content tree 179 | /// 180 | /// The content nodes, nested as they are in the file system. 181 | public IEnumerable BuildHierarchy () 182 | { 183 | Dictionary items = new Dictionary (); 184 | 185 | ContentNode currentParent = null; 186 | ContentNode previous = null; 187 | int nestDepth = 0; 188 | int topDepth = 0; 189 | 190 | // sort by the path, and then go up and down the tree adding to the nest 191 | var nodes = this.ContentNodes 192 | .Where (n => n.Source.RootRelativePath.EndsWith(".md")) 193 | .OrderBy (n => Path.GetDirectoryName(n.Source.RootRelativePath)) 194 | .ToArray(); 195 | foreach (var node in nodes) 196 | { 197 | int thisDepth = node.Source.RootRelativePath.Length - node.Source.RootRelativePath.Replace (Path.DirectorySeparatorChar.ToString(), string.Empty).Length; 198 | 199 | bool wentDeeper = thisDepth > nestDepth; 200 | bool wentUp = thisDepth < nestDepth; 201 | bool sameDepth = thisDepth == nestDepth; 202 | 203 | // for the first iteration, and for when the depth doesn't change but it's top level 204 | if (currentParent == null || ((sameDepth || wentUp) && thisDepth == topDepth)) 205 | { 206 | currentParent = node; 207 | nestDepth = thisDepth; 208 | topDepth = thisDepth; 209 | items.Add (node.Source.RootRelativePath, node); 210 | } 211 | else 212 | { 213 | if (wentDeeper) 214 | { 215 | currentParent = previous; 216 | } 217 | if (wentUp) 218 | { 219 | currentParent = currentParent.Parent; 220 | } 221 | 222 | node.Parent = currentParent; 223 | currentParent.Children.Add (node); 224 | 225 | } 226 | 227 | nestDepth = thisDepth; 228 | previous = node; 229 | } 230 | 231 | return items.Values.Where(n => n.Parent == null); 232 | } 233 | } 234 | } -------------------------------------------------------------------------------- /toolsrc/Chloroplast.Core/Content/DiskFile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using Chloroplast.Core.Extensions; 5 | 6 | namespace Chloroplast.Core.Content 7 | { 8 | public class DiskFile : IFile 9 | { 10 | public DiskFile (string fullPath, string relativePath) 11 | { 12 | this.FullPath = fullPath; 13 | 14 | FileInfo info = new FileInfo (fullPath); 15 | if (info.Exists) 16 | this.LastUpdated = info.LastWriteTime; 17 | else 18 | this.LastUpdated = DateTime.MinValue; 19 | 20 | this.RootRelativePath = relativePath; 21 | } 22 | 23 | public DateTime LastUpdated { get; set; } 24 | public string RootRelativePath { get; set; } 25 | public string FullPath { get; } 26 | 27 | public void CopyTo (IFile target) 28 | { 29 | if (target is DiskFile) 30 | { 31 | try { 32 | var dtarget = target as DiskFile; 33 | dtarget.FullPath.EnsureFileDirectory(); 34 | File.Copy(this.FullPath, dtarget.FullPath, true); 35 | } 36 | catch(Exception ex) { 37 | Console.WriteLine(ex.Message); 38 | } 39 | } 40 | else 41 | { 42 | throw new NotImplementedException($"can't copy to a {target.GetType().Name}"); 43 | } 44 | } 45 | 46 | public Task ReadContentAsync () => 47 | File.ReadAllTextAsync (this.FullPath); 48 | 49 | public async Task WriteContentAsync (string content) 50 | { 51 | try 52 | { 53 | this.FullPath.EnsureFileDirectory (); 54 | 55 | // This is extremely strange, but I was getting 56 | // intermittent results (empty files) with the 57 | // await version. So this sync workaround 58 | // wfm 59 | if (true) 60 | File.WriteAllText (this.FullPath, content); 61 | else 62 | await File.WriteAllTextAsync (this.FullPath, content); 63 | } 64 | catch (Exception ex) 65 | { 66 | Console.ForegroundColor = ConsoleColor.Red; 67 | Console.WriteLine (ex.ToString()); 68 | Console.ResetColor (); 69 | } 70 | } 71 | 72 | public override string ToString () 73 | { 74 | return this.FullPath; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /toolsrc/Chloroplast.Core/Content/Header.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using HtmlAgilityPack; 3 | 4 | namespace Chloroplast.Core.Content 5 | { 6 | public class Header 7 | { 8 | public int Level { get; set; } 9 | public string Value { get; set; } 10 | public string Slug 11 | { 12 | get 13 | { 14 | return this.Value 15 | .Replace (" ", "_") 16 | .Replace ("!", "") 17 | .Replace ("?", "") 18 | .Replace ("#", "_") 19 | .Replace (".", "_") 20 | .Replace ("<", "_") 21 | .Replace (">", "_") 22 | .Replace ("\"", "_") 23 | .Replace("'", "_") 24 | .Replace ("&", "and"); 25 | } 26 | } 27 | 28 | internal static Header FromNode (HtmlNode n) 29 | { 30 | return new Header 31 | { 32 | Value = n.InnerText, 33 | Level = GetLevel (n) 34 | }; 35 | } 36 | 37 | private static int GetLevel (HtmlNode n) 38 | { 39 | string tagName = n.Name.ToLower (); 40 | switch (tagName) 41 | { 42 | case "h1": 43 | return 1; 44 | case "h2": 45 | return 2; 46 | case "h3": 47 | return 3; 48 | case "h4": 49 | return 4; 50 | case "h5": 51 | return 5; 52 | case "h6": 53 | return 6; 54 | default: 55 | return 1; 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /toolsrc/Chloroplast.Core/Content/IFile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Chloroplast.Core.Content 5 | { 6 | public interface IFile 7 | { 8 | DateTime LastUpdated { get; set; } 9 | string RootRelativePath { get; set; } 10 | void CopyTo (IFile target); 11 | Task ReadContentAsync (); 12 | Task WriteContentAsync (string content); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /toolsrc/Chloroplast.Core/ContentNode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Chloroplast.Core.Content; 4 | 5 | namespace Chloroplast.Core 6 | { 7 | public class MenuNode 8 | { 9 | public string Path { get; set; } 10 | public string Title { get; set; } 11 | public IEnumerable Items { get; set; } 12 | } 13 | 14 | public class ContentNode 15 | { 16 | public string Slug { get; set; } 17 | public string Title { get; set; } 18 | public IFile Source { get; set; } 19 | public IFile Target { get; set; } 20 | public ContentArea Area { get; set; } 21 | public string MenuPath { get; set; } 22 | 23 | public ContentNode () 24 | { 25 | } 26 | 27 | public ContentNode Parent { get; set; } 28 | public IList Children { get; } = new List (); 29 | 30 | public override string ToString () 31 | { 32 | return $"{Slug}, {Title}, {Source}->{Target}"; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /toolsrc/Chloroplast.Core/Extensions/CollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Chloroplast.Core.Extensions 6 | { 7 | public static class CollectionExtensions 8 | { 9 | public static T[] SubArray(this IEnumerable collection, int from, int to) 10 | { 11 | return collection 12 | .Skip (from) 13 | .Take (to - from) 14 | .ToArray (); 15 | } 16 | 17 | public static string StringJoinFromSubArray (this IEnumerable lines, string separator, int from, int to) 18 | { 19 | var sub = lines.SubArray (from, to); 20 | return String.Join (separator, sub).Trim (); 21 | } 22 | 23 | public static T Try(this IDictionary dict, string key) 24 | { 25 | T val; 26 | if (dict.TryGetValue (key, out val)) 27 | return val; 28 | 29 | return default (T); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /toolsrc/Chloroplast.Core/Extensions/PathExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Chloroplast.Core.Extensions 5 | { 6 | public static class PathExtensions 7 | { 8 | public static char OtherDirectorySeparator { get; } = Path.DirectorySeparatorChar == '/' ? '\\' : '/'; 9 | 10 | public static string NormalizePath(this string value, bool toLower = false) 11 | { 12 | if (string.IsNullOrWhiteSpace (value)) 13 | return string.Empty; 14 | 15 | var slashed = value.Replace (OtherDirectorySeparator, Path.DirectorySeparatorChar); 16 | 17 | if (slashed.StartsWith ('~')) 18 | slashed = slashed.Replace("~", Environment.GetFolderPath (Environment.SpecialFolder.UserProfile)); 19 | 20 | return toLower ? slashed.ToLower() : slashed; 21 | } 22 | 23 | public static string RelativePath(this string value, string rootPath) 24 | { 25 | var replaced = value.Normalize ().Replace (rootPath.Normalize (), string.Empty); 26 | if (replaced.StartsWith (Path.DirectorySeparatorChar)) 27 | replaced = replaced.Substring (1); 28 | 29 | return replaced; 30 | } 31 | 32 | public static string GetPathFileName(this string value) 33 | { 34 | return Path.GetFileName (value); 35 | } 36 | 37 | public static string CombinePath(this string value, params string[] paths) 38 | { 39 | string combinedPaths = Path.Combine (paths).NormalizePath (); 40 | if (combinedPaths.StartsWith (Path.DirectorySeparatorChar)) 41 | combinedPaths = combinedPaths.Substring (1); 42 | 43 | if (combinedPaths.StartsWith (Path.DirectorySeparatorChar)) 44 | combinedPaths = combinedPaths.Substring (1); 45 | 46 | return Path.Combine (value, combinedPaths); 47 | } 48 | 49 | /// The directory path 50 | public static string EnsureDirectory(this string dir) 51 | { 52 | if (!Directory.Exists (dir)) 53 | Directory.CreateDirectory (dir); 54 | 55 | return dir; 56 | } 57 | 58 | /// The directory path 59 | public static string EnsureFileDirectory(this string value) 60 | { 61 | string dir = Path.GetDirectoryName(value); 62 | return dir.EnsureDirectory (); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /toolsrc/Chloroplast.Core/Extensions/SiteConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Chloroplast.Core.Config; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.FileProviders; 5 | 6 | namespace Chloroplast.Core.Extensions 7 | { 8 | public static class SiteConfigurationExtensions 9 | { 10 | public static IConfigurationBuilder AddChloroplastConfig (this IConfigurationBuilder builder, string path) 11 | { 12 | return AddChloroplastConfig (builder, provider: null, path: path, optional: false, reloadOnChange: false); 13 | } 14 | 15 | public static IConfigurationBuilder AddChloroplastConfig (this IConfigurationBuilder builder, string path, bool optional) 16 | { 17 | return AddChloroplastConfig (builder, provider: null, path: path, optional: optional, reloadOnChange: false); 18 | } 19 | 20 | public static IConfigurationBuilder AddChloroplastConfig (this IConfigurationBuilder builder, string path, bool optional, bool reloadOnChange) 21 | { 22 | return AddChloroplastConfig (builder, provider: null, path: path, optional: optional, reloadOnChange: reloadOnChange); 23 | } 24 | 25 | public static IConfigurationBuilder AddChloroplastConfig (this IConfigurationBuilder builder, IFileProvider provider, string path, bool optional, bool reloadOnChange) 26 | { 27 | if (provider == null && Path.IsPathRooted (path)) 28 | { 29 | provider = new PhysicalFileProvider (Path.GetDirectoryName (path)); 30 | path = Path.GetFileName (path); 31 | } 32 | var source = new SiteConfigSource 33 | { 34 | FileProvider = provider, 35 | Path = path, 36 | Optional = optional, 37 | ReloadOnChange = reloadOnChange 38 | }; 39 | builder.Add (source); 40 | return builder; 41 | } 42 | 43 | public static IConfigurationBuilder AddChloroplastFrontMatter(this IConfigurationBuilder builder, string content) 44 | { 45 | var source = new FrontMatterConfigSource (content); 46 | builder.Add (source); 47 | return builder; 48 | } 49 | 50 | public static bool ContainsKey(this IConfigurationRoot config, string key) 51 | { 52 | if (config == null) return false; 53 | 54 | var value = config[key]; 55 | bool hasStringKey = !string.IsNullOrWhiteSpace (value); 56 | 57 | if (!hasStringKey) 58 | { 59 | var section = config.GetSection (key); 60 | return section.Exists(); 61 | } 62 | 63 | return true; 64 | } 65 | 66 | public static bool ContainsKey (this IConfigurationSection config, string key) 67 | { 68 | var value = config[key]; 69 | bool hasStringKey = !string.IsNullOrWhiteSpace (value); 70 | 71 | if (!hasStringKey) 72 | { 73 | var section = config.GetSection (key); 74 | return section != null; 75 | } 76 | 77 | return true; 78 | } 79 | 80 | public static bool GetBool(this IConfigurationRoot config, string key, bool defaultValue = false) 81 | { 82 | var value = config[key]; 83 | 84 | if (string.IsNullOrWhiteSpace (value)) 85 | return defaultValue; 86 | 87 | bool theValue; 88 | 89 | bool.TryParse (value, out theValue); 90 | 91 | return theValue; 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /toolsrc/Chloroplast.Core/Loaders/EcmaXmlExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Xml.Serialization; 6 | using EcmaXml = Chloroplast.Core.Loaders.EcmaXml; 7 | 8 | namespace Chloroplast.Core.Loaders.EcmaXml 9 | { 10 | public partial class Namespace 11 | { 12 | public string Summary 13 | { 14 | get { 15 | var sum = (EcmaXml.summary)this.Docs.Items.FirstOrDefault (i => i is EcmaXml.summary); 16 | return sum == null ? string.Empty : sum.Text.FirstOrDefault (); 17 | } 18 | } 19 | } 20 | 21 | [XmlType("Overview")] 22 | public class XIndex 23 | { 24 | [XmlElement] 25 | public string Title { get; set; } 26 | //types 27 | [XmlArray ("Types")] 28 | [XmlArrayItem ("Namespace", Type = typeof (XIndexNamespace))] 29 | public List Namespaces { get; set; } 30 | //assemblies 31 | //remarks 32 | //copyright 33 | //extension methods 34 | 35 | public string ToMenu(string rootPath) 36 | { 37 | StringBuilder sb = new StringBuilder (); 38 | sb.AppendLine (@"--- 39 | template: menu 40 | parent: 41 | - path: "+ rootPath +@" 42 | title: Home 43 | navTree:"); 44 | foreach(var ns in this.Namespaces) 45 | { 46 | sb.AppendLine (@"- path: "+ rootPath +@"/"+ ns.Name +@" 47 | title: "+ ns.Name +@""); 48 | if (ns.Types.Any ()) 49 | { 50 | sb.AppendLine (" items: "); 51 | foreach (var t in ns.Types) 52 | { 53 | sb.AppendLine ($" - path: {rootPath}/{ns.Name}/{t.Name}.html"); 54 | sb.AppendLine ($" title: {t.Name}"); 55 | } 56 | } 57 | } 58 | 59 | sb.AppendLine ("---"); 60 | 61 | return sb.ToString (); 62 | } 63 | } 64 | 65 | [XmlType("Namespace")] 66 | public class XIndexNamespace 67 | { 68 | [XmlAttribute] 69 | public string Name { get; set; } 70 | 71 | [XmlElement("Type")] 72 | public List Types { get; set; } 73 | } 74 | 75 | [XmlType("Type")] 76 | public class XIndexType 77 | { 78 | [XmlAttribute] 79 | public string Name { get; set; } 80 | 81 | private string displayName; 82 | 83 | [XmlAttribute] 84 | public string DisplayName 85 | { 86 | get 87 | { 88 | return string.IsNullOrWhiteSpace (this.displayName) ? this.Name : this.displayName; 89 | } 90 | set 91 | { 92 | this.displayName = value; 93 | } 94 | } 95 | 96 | [XmlAttribute] 97 | public string Kind { get; set; } 98 | } 99 | 100 | [Serializable] 101 | [XmlType("Type")] 102 | public class XType 103 | { 104 | [XmlAttribute] 105 | public string Name { get; set; } 106 | [XmlAttribute] 107 | public string FullName { get; set; } 108 | 109 | [XmlElement ("TypeSignature")] 110 | public List Signatures { get; set; } = new List (); 111 | 112 | [XmlElement ("AssemblyInfo")] 113 | public List AssemblyInfos { get; set; } = new List (); 114 | 115 | [XmlArray ("Members")] 116 | [XmlArrayItem ("Member", Type = typeof (XMemberItem))] 117 | public XMemberItem[] Members { get; set; } = new XMemberItem[0]; 118 | 119 | public XBase Base { get; set; } 120 | 121 | [XmlArray ("Parameters")] 122 | [XmlArrayItem ("Parameter", Type = typeof (XParameter))] 123 | public List Parameters { get; set; } = new List (); 124 | [XmlArray ("TypeParameters")] 125 | [XmlArrayItem ("TypeParameter", Type = typeof (XParameter))] 126 | public List TypeParameters { get; set; } = new List (); 127 | 128 | public XDocs Docs { get; set; } 129 | 130 | // class type 131 | // interfaces 132 | // attributes 133 | } 134 | 135 | [XmlType("Docs")] 136 | public class XDocs 137 | { 138 | [XmlElement ("summary")] 139 | public string Summary { get; set; } = string.Empty; 140 | [XmlElement ("remarks")] 141 | public string Remarks { get; set; } = string.Empty; 142 | 143 | [XmlElement ("param")] 144 | public List Params { get; set; } = new List (); 145 | [XmlElement ("typeparam")] 146 | public List TypeParams { get; set; } = new List (); 147 | } 148 | 149 | public class XParam 150 | { 151 | [XmlAttribute ("name")] 152 | public string Name { get; set; } = string.Empty; 153 | 154 | [XmlText()] 155 | public string Value { get; set; } = string.Empty; 156 | } 157 | 158 | public class XParameter 159 | { 160 | [XmlAttribute] 161 | public string Name { get; set; } 162 | [XmlAttribute] 163 | public string Type { get; set; } 164 | [XmlAttribute] 165 | public string RefType { get; set; } 166 | // attributes 167 | // default value 168 | } 169 | 170 | [XmlType ("Member")] 171 | public class XMemberItem 172 | { 173 | [XmlAttribute("MemberName")] 174 | public string Name { get; set; } 175 | 176 | public string FullName { get; set; } 177 | 178 | public string MemberType { get; set; } 179 | 180 | [XmlElement ("MemberSignature")] 181 | public List Signatures { get; set; } = new List (); 182 | 183 | [XmlElement ("AssemblyInfo")] 184 | public List AssemblyInfos { get; set; } = new List (); 185 | 186 | [XmlArray ("Parameters")] 187 | [XmlArrayItem ("Parameter", Type = typeof (XParameter))] 188 | public List Parameters { get; set; } = new List (); 189 | [XmlArray ("TypeParameters")] 190 | [XmlArrayItem ("TypeParameter", Type = typeof (XParameter))] 191 | public List TypeParameters { get; set; } = new List (); 192 | 193 | public XDocs Docs { get; set; } 194 | 195 | // parameter attributes 196 | } 197 | 198 | //[XmlType ("MemberSignature")] 199 | //[XmlType ("TypeSignature")] 200 | public class XSignature 201 | { 202 | [XmlAttribute] 203 | public string Language { get; set; } 204 | 205 | [XmlAttribute] 206 | public string Value { get; set; } 207 | 208 | } 209 | 210 | [XmlType("AssemblyInfo")] 211 | public class XAssemblyInfo 212 | { 213 | [XmlElement("AssemblyVersion")] 214 | public List AssemblyVersion { get; set; } 215 | public string AssemblyName { get; set; } 216 | } 217 | 218 | [XmlType("Base")] 219 | public class XBase 220 | { 221 | public string BaseTypeName { get; set; } 222 | 223 | // TODO: BaseTypeArguments 224 | // BaseTypeArgument TypeParamName="U">T LoadNamespace (GenerateStreamFromString (s)); 11 | public static EcmaXml.Namespace LoadNamespace (Stream s) => Deserialize (s); 12 | 13 | public static EcmaXml.Type LoadType (string s) => LoadType (GenerateStreamFromString (s)); 14 | public static EcmaXml.Type LoadType (Stream s) => Deserialize (s); 15 | 16 | public static EcmaXml.XType LoadXType (string s) => LoadXType (GenerateStreamFromString (s)); 17 | public static EcmaXml.XType LoadXType (Stream s) => Deserialize (s); 18 | 19 | public static EcmaXml.XIndex LoadXIndex (string s) => LoadXIndex (GenerateStreamFromString (s)); 20 | public static EcmaXml.XIndex LoadXIndex (Stream s) => Deserialize (s); 21 | 22 | public static Stream GenerateStreamFromString (string s) 23 | { 24 | var stream = new MemoryStream (); 25 | var writer = new StreamWriter (stream); 26 | writer.Write (s); 27 | writer.Flush (); 28 | stream.Position = 0; 29 | return stream; 30 | } 31 | 32 | private static T Deserialize (Stream s) 33 | { 34 | T ns; 35 | using (s) 36 | { 37 | var serializer = new XmlSerializer (typeof (T)); 38 | 39 | ns = (T)serializer.Deserialize (s); 40 | } 41 | 42 | return ns; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /toolsrc/Chloroplast.Core/Rendering/ChloroplastTemplateBase.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Chloroplast.Core.Content; 3 | using Chloroplast.Core.Extensions; 4 | using MiniRazor; 5 | 6 | namespace Chloroplast.Core.Rendering 7 | { 8 | public abstract class ChloroplastTemplateBase : TemplateBase where T : RenderedContent 9 | { 10 | public ChloroplastTemplateBase () 11 | { 12 | } 13 | 14 | protected Task PartialAsync(string templateName, K model) 15 | { 16 | return RazorRenderer.Instance.RenderTemplateContent (templateName, model); 17 | } 18 | 19 | protected async Task PartialAsync(string menuPath) 20 | { 21 | string fullMenuPath; 22 | 23 | if (!menuPath.Equals (this.Model.Node.MenuPath)) 24 | { 25 | fullMenuPath = SiteConfig.Instance["root"] 26 | .NormalizePath () 27 | .CombinePath (menuPath); 28 | } 29 | else 30 | { 31 | fullMenuPath = this.Model.Node.MenuPath; 32 | } 33 | 34 | // load the menu path 35 | var node = new ContentNode 36 | { 37 | Slug = "/" + Model.Node.Area.TargetPath.GetPathFileName ().CombinePath (Model.Node.Slug), 38 | Source = new DiskFile (fullMenuPath, menuPath), 39 | Target = new DiskFile (fullMenuPath, menuPath), 40 | Parent = this.Model.Node 41 | }; 42 | var r = await ContentRenderer.FromMarkdownAsync (node); 43 | r = await ContentRenderer.ToRazorAsync (r); 44 | 45 | return new RawString (r.Body); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /toolsrc/Chloroplast.Core/Rendering/ContentRenderer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.IO; 5 | using System.Threading.Tasks; 6 | using Microsoft.Extensions.Configuration; 7 | using Chloroplast.Core.Extensions; 8 | using Chloroplast.Core.Content; 9 | using EcmaXml = Chloroplast.Core.Loaders.EcmaXml; 10 | 11 | namespace Chloroplast.Core.Rendering 12 | { 13 | public class RenderedContent 14 | { 15 | public ContentNode Node { get; set; } 16 | public string Body { get; set; } 17 | public IConfigurationRoot Metadata { get; set; } 18 | public Header[] Headers { get; internal set; } 19 | 20 | public RenderedContent () 21 | { 22 | } 23 | 24 | public RenderedContent (RenderedContent content) 25 | { 26 | this.Node = content.Node; 27 | this.Body = content.Body; 28 | this.Metadata = content.Metadata; 29 | } 30 | 31 | public string GetMeta(string key) 32 | { 33 | if (Metadata == null) return string.Empty; 34 | 35 | string value = Metadata[key]; 36 | return value ?? string.Empty; 37 | } 38 | 39 | public bool HasMeta(string key) 40 | { 41 | if (Metadata == null) return false; 42 | 43 | string value = Metadata[key]; 44 | return !string.IsNullOrWhiteSpace(value); 45 | } 46 | } 47 | 48 | public class EcmaXmlContent : RenderedContent 49 | { 50 | public T Element { get; set; } 51 | } 52 | 53 | public class FrameRenderedContent : RenderedContent 54 | { 55 | public FrameRenderedContent (RenderedContent content, IEnumerable tree) 56 | : base (content) 57 | { 58 | Tree = tree; 59 | } 60 | 61 | public IEnumerable Tree { get; set; } 62 | } 63 | 64 | public static class ContentRenderer 65 | { 66 | private static RazorRenderer razorRenderer; 67 | 68 | public static async Task InitializeAsync(IConfigurationRoot config) 69 | { 70 | razorRenderer = new RazorRenderer (); 71 | await razorRenderer.InitializeAsync (config); 72 | } 73 | public static async Task FromMarkdownAsync(ContentNode node) 74 | { 75 | var content = node.Source.ReadContentAsync (); 76 | var parsed = new RenderedContent 77 | { 78 | Node = node, 79 | Body = content.Result 80 | }; 81 | 82 | // parse front-matter 83 | YamlRenderer yamlrenderer = new YamlRenderer (); 84 | (var yaml, string markdown) = yamlrenderer.ParseDoc (parsed.Body); 85 | parsed.Metadata = yaml; 86 | 87 | parsed.Body = markdown; 88 | parsed.Node.Title = yaml["title"] ?? yaml["Title"] ?? parsed.Node.Slug; 89 | 90 | // convert markdown to html 91 | MarkdownRenderer mdRenderer = new MarkdownRenderer (); 92 | parsed.Body = mdRenderer.Render (parsed.Body); 93 | 94 | // parse out headers 95 | HtmlAgilityPack.HtmlDocument doc = new HtmlAgilityPack.HtmlDocument (); 96 | doc.LoadHtml (parsed.Body); 97 | var nodes = doc.DocumentNode.SelectNodes ("//h1|//h2|//h3|//h4//h5//h6"); 98 | List
headers = null; 99 | if (nodes != null) 100 | { 101 | headers = new List
(nodes.Count); 102 | 103 | foreach (var n in nodes.Where(n=>!string.IsNullOrWhiteSpace(n.InnerText))) 104 | { 105 | var header = Header.FromNode (n); 106 | headers.Add (header); 107 | 108 | // insert the anchor before the header element 109 | var anchor = doc.CreateElement ("a"); 110 | anchor.Attributes.Add ("name", header.Slug); 111 | n.ParentNode.InsertBefore (anchor, n); 112 | } 113 | parsed.Body = doc.DocumentNode.OuterHtml; 114 | } 115 | parsed.Headers = headers != null ? headers.ToArray() : new Header[0]; 116 | 117 | return parsed; 118 | } 119 | 120 | public static async Task FromEcmaXmlAsync (ContentNode item, IConfigurationRoot config) 121 | { 122 | var content = item.Source.ReadContentAsync (); 123 | var parsed = new RenderedContent 124 | { 125 | Node = item, 126 | Body = content.Result 127 | }; 128 | 129 | var rendered = EcmaXmlRenderer.Render (item, content.Result, config); 130 | 131 | return await rendered; 132 | } 133 | 134 | public static async Task ToRazorAsync (RenderedContent content) 135 | { 136 | content.Body = await razorRenderer.RenderContentAsync (content); 137 | 138 | return content; 139 | } 140 | 141 | public static async Task ToRazorAsync (FrameRenderedContent content) 142 | { 143 | content.Body = await razorRenderer.RenderContentAsync (content); 144 | return content; 145 | } 146 | 147 | public static async Task ToRazorAsync (EcmaXmlContent nscontent) 148 | { 149 | var body = await razorRenderer.RenderContentAsync (nscontent); 150 | return body; 151 | } 152 | 153 | public static async Task ToRazorAsync (EcmaXmlContent nscontent) 154 | { 155 | var body = await razorRenderer.RenderContentAsync (nscontent); 156 | return body; 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /toolsrc/Chloroplast.Core/Rendering/EcmaXmlRenderer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Chloroplast.Core.Loaders; 4 | using Microsoft.Extensions.Configuration; 5 | using EcmaXml = Chloroplast.Core.Loaders.EcmaXml; 6 | 7 | namespace Chloroplast.Core.Rendering 8 | { 9 | public class EcmaXmlRenderer 10 | { 11 | public EcmaXmlRenderer () 12 | { 13 | } 14 | 15 | public static async Task Render (ContentNode item, string body, Microsoft.Extensions.Configuration.IConfigurationRoot config) 16 | { 17 | 18 | MarkdownRenderer md = new MarkdownRenderer (); 19 | 20 | if (body.StartsWith (" ToEcmaContent (ContentNode item, IConfigurationRoot config, T t) 68 | { 69 | return new EcmaXmlContent 70 | { 71 | Node = item, 72 | Element = t, 73 | Metadata = config 74 | }; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /toolsrc/Chloroplast.Core/Rendering/MarkdownRenderer.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Markdig; 3 | using Markdig.Parsers; 4 | using Markdig.Renderers; 5 | 6 | namespace Chloroplast.Core.Rendering 7 | { 8 | public class MarkdownRenderer 9 | { 10 | public string Render (string markdown) 11 | { 12 | var builder = new MarkdownPipelineBuilder () 13 | .UseEmphasisExtras () 14 | .UseGridTables () 15 | .UsePipeTables () 16 | .UseGenericAttributes () 17 | .UseAutoLinks (); 18 | 19 | var pipeline = builder.Build (); 20 | 21 | var writer = new StringWriter (); 22 | var renderer = new HtmlRenderer (writer); 23 | pipeline.Setup (renderer); 24 | 25 | var document = MarkdownParser.Parse (markdown, pipeline); 26 | renderer.Render (document); 27 | writer.Flush (); 28 | 29 | return writer.ToString (); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /toolsrc/Chloroplast.Core/Rendering/RazorRenderer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | using Chloroplast.Core.Extensions; 6 | //using Choroplast.Core.Loaders.EcmaXml; 7 | using Microsoft.Extensions.Configuration; 8 | using MiniRazor; 9 | 10 | namespace Chloroplast.Core.Rendering 11 | { 12 | public class RazorRenderer 13 | { 14 | public static RazorRenderer Instance; 15 | 16 | Dictionary templates = new Dictionary (); 17 | //TemplateEngine engine = new TemplateEngine (); 18 | 19 | public async Task AddTemplateAsync (string templatePath) 20 | { 21 | string fileName = Path.GetFileNameWithoutExtension (templatePath); 22 | if (!templates.ContainsKey (fileName)) 23 | { 24 | Chloroplast.Core.Loaders.EcmaXml.Namespace ns = new Chloroplast.Core.Loaders.EcmaXml.Namespace (); 25 | Console.WriteLine (ns.ToString ()); 26 | templates[fileName] = Razor.Compile (await File.ReadAllTextAsync (templatePath)); 27 | } 28 | } 29 | 30 | public async Task InitializeAsync (IConfigurationRoot config) 31 | { 32 | string rootPath = config["root"].NormalizePath (); 33 | string templatePath = config["templates_folder"].NormalizePath (); 34 | string fullTemplatePath = rootPath.CombinePath (templatePath); 35 | 36 | foreach (var razorPath in Directory.EnumerateFiles (fullTemplatePath, "*.cshtml", SearchOption.AllDirectories)) 37 | { 38 | await this.AddTemplateAsync (razorPath); 39 | } 40 | 41 | // danger will robinson ... 42 | // there should be only one ... big assumption here 43 | Instance = this; 44 | } 45 | 46 | public async Task RenderContentAsync (FrameRenderedContent parsed) 47 | { 48 | try 49 | { 50 | // now render into site frame 51 | var frame = templates["SiteFrame"]; 52 | 53 | var result = await frame.RenderAsync (parsed); 54 | return result; 55 | } 56 | catch (Exception ex) 57 | { 58 | Console.ForegroundColor = ConsoleColor.Red; 59 | Console.WriteLine (ex.ToString ()); 60 | Console.ResetColor (); 61 | return ex.ToString (); 62 | } 63 | } 64 | 65 | public async Task RenderTemplateContent (string templateName, T model) 66 | { 67 | var template = templates[templateName]; 68 | return new RawString (await template.RenderAsync (model)); 69 | } 70 | 71 | public async Task RenderContentAsync (RenderedContent parsed) 72 | { 73 | try 74 | { 75 | string defaultTemplateName = "Default"; 76 | string templateName = defaultTemplateName; 77 | 78 | if (parsed.Metadata.ContainsKey ("template")) 79 | templateName = parsed.Metadata["template"]; 80 | 81 | if (parsed.Metadata.ContainsKey ("layout")) 82 | templateName = parsed.Metadata["layout"]; 83 | 84 | TemplateDescriptor template; 85 | 86 | if (!templates.TryGetValue (templateName, out template)) 87 | template = templates[defaultTemplateName]; 88 | 89 | // Render template 90 | var result = await template.RenderAsync (parsed); 91 | 92 | return result; 93 | 94 | } 95 | catch (Exception ex) 96 | { 97 | Console.ForegroundColor = ConsoleColor.Red; 98 | Console.WriteLine (ex.ToString ()); 99 | Console.ResetColor (); 100 | return ex.ToString (); 101 | } 102 | } 103 | 104 | public async Task RenderContentAsync (EcmaXmlContent parsed) 105 | { 106 | try 107 | { 108 | string templateName = "Namespace"; 109 | 110 | TemplateDescriptor template; 111 | 112 | if (!templates.TryGetValue (templateName, out template)) 113 | template = templates[templateName]; 114 | 115 | // Render template 116 | var result = await template.RenderAsync (parsed); 117 | 118 | return result; 119 | 120 | } 121 | catch (Exception ex) 122 | { 123 | Console.ForegroundColor = ConsoleColor.Red; 124 | Console.WriteLine (ex.ToString ()); 125 | Console.ResetColor (); 126 | return ex.ToString (); 127 | } 128 | } 129 | 130 | public async Task RenderContentAsync (EcmaXmlContent parsed) 131 | { 132 | try 133 | { 134 | string templateName = "Type"; 135 | 136 | TemplateDescriptor template; 137 | 138 | if (!templates.TryGetValue (templateName, out template)) 139 | template = templates[templateName]; 140 | 141 | // Render template 142 | var result = await template.RenderAsync (parsed); 143 | 144 | return result; 145 | 146 | } 147 | catch (Exception ex) 148 | { 149 | Console.ForegroundColor = ConsoleColor.Red; 150 | Console.WriteLine (ex.ToString ()); 151 | Console.ResetColor (); 152 | return ex.ToString (); 153 | } 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /toolsrc/Chloroplast.Core/Rendering/YamlRenderer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using Chloroplast.Core.Config; 5 | using Chloroplast.Core.Extensions; 6 | using Microsoft.Extensions.Configuration; 7 | using YamlDotNet.RepresentationModel; 8 | using YamlDotNet.Serialization; 9 | 10 | namespace Chloroplast.Core.Rendering 11 | { 12 | public class YamlRenderer 13 | { 14 | 15 | public (IConfigurationRoot,string) ParseDoc(string content) 16 | { 17 | (string yaml, string markdown) = Split (content); 18 | SiteConfigurationFileParser configParser = new SiteConfigurationFileParser (); 19 | 20 | ConfigurationBuilder builder = new ConfigurationBuilder (); 21 | builder.AddChloroplastFrontMatter (yaml); 22 | return (builder.Build (), markdown); 23 | } 24 | 25 | private (string yaml, string markdown) Split (string content) 26 | { 27 | // first normalize to neutralize git derpery 28 | content = content.Replace ("\r\n", "\n"); 29 | var lines = content.Split ('\n'); 30 | 31 | bool markerStarter = content.StartsWith ("---"); 32 | int startdelimiter = markerStarter ? 1 : 0; 33 | int enddelimiter = 0; 34 | for (int i = startdelimiter; i < lines.Length; i++) 35 | { 36 | string line = lines[i].Trim(); 37 | 38 | if (markerStarter) 39 | { 40 | // just look for the closing `---` 41 | if (line.StartsWith ("---")) 42 | { 43 | enddelimiter = i; 44 | break; 45 | } 46 | } 47 | else 48 | { 49 | // there was no markerStarter ... just look for the 50 | // first empty line 51 | if (line.Length == 0) 52 | { 53 | enddelimiter = i; 54 | break; 55 | } 56 | } 57 | } 58 | 59 | // if no end delimiter was found, just return content as is 60 | // with no markdown 61 | if (enddelimiter == 0) 62 | return (string.Empty, content); 63 | 64 | string parsedYaml = lines.StringJoinFromSubArray (Environment.NewLine, startdelimiter, enddelimiter); 65 | string parsedMarkdown = lines.StringJoinFromSubArray (Environment.NewLine, enddelimiter+1, content.Length - enddelimiter+1); 66 | return (parsedYaml, parsedMarkdown); 67 | } 68 | 69 | /// 70 | /// Saves a menu markdown file 71 | /// 72 | /// the path, including markdown filename 73 | /// the menu nodes we want to be included 74 | public static void RenderAndSaveMenu(string filePath, IEnumerable nodes) 75 | { 76 | var serializer = new SerializerBuilder () 77 | .WithNamingConvention(new YamlDotNet.Serialization.NamingConventions.CamelCaseNamingConvention()) 78 | .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitNull) 79 | .Build(); 80 | string fileContent = serializer.Serialize (new 81 | { 82 | template = "menu", 83 | navTree = nodes 84 | }); 85 | 86 | string mdContent = $"---{Environment.NewLine}{fileContent}{Environment.NewLine}---{Environment.NewLine}"; 87 | File.WriteAllText (filePath, mdContent); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /toolsrc/Chloroplast.Test/Chloroplast.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp8.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | all 14 | 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | all 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /toolsrc/Chloroplast.Test/EcmaXmlTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using Chloroplast.Core.Loaders; 5 | using Chloroplast.Core.Loaders.EcmaXml; 6 | using Xunit; 7 | using EcmaXml = Chloroplast.Core.Loaders.EcmaXml; 8 | 9 | namespace Chloroplast.Test 10 | { 11 | public class EcmaXmlTests 12 | { 13 | [Fact] 14 | public void TestLoadIndex() 15 | { 16 | var index = EcmaXmlLoader.LoadXIndex (XmlForIndex); 17 | 18 | Assert.Single (index.Namespaces); 19 | Assert.Equal ("Meadow", index.Title); 20 | var ns = index.Namespaces.Single (); 21 | Assert.Equal ("Meadow", ns.Name); 22 | 23 | Assert.Equal (5, ns.Types.Count); 24 | 25 | Assert.Equal ("AnalogCapabilities", ns.Types[0].Name); 26 | Assert.Equal ("AnalogCapabilities", ns.Types[0].DisplayName); 27 | Assert.Equal ("Class", ns.Types[0].Kind); 28 | } 29 | 30 | [Fact] 31 | public void TestLoadNamespace() 32 | { 33 | var ns = EcmaXmlLoader.LoadNamespace (XmlForNS); 34 | 35 | Assert.Equal ("Meadow.Foundation.Audio", ns.Name); 36 | Assert.Equal ("To be added.", ns.Summary); 37 | } 38 | 39 | [Fact] 40 | public void TestLoadType () 41 | { 42 | var t = EcmaXmlLoader.LoadXType (XmlForTypeOnlyDetails); 43 | 44 | Assert.Equal ("App", t.Name); 45 | Assert.Equal ("Meadow.App", t.FullName); 46 | Assert.Equal (2, t.Signatures.Count); 47 | Assert.Single (t.AssemblyInfos); 48 | Assert.Equal (2, t.AssemblyInfos.First ().AssemblyVersion.Count); 49 | Assert.Equal ("0.21.0.0", t.AssemblyInfos.First ().AssemblyVersion.First()); 50 | Assert.Equal ("System.Object", t.Base.BaseTypeName); 51 | 52 | // docs 53 | var docs = t.Docs; 54 | Assert.NotNull (docs); 55 | Assert.Equal ("To be added.", docs.Summary); 56 | Assert.Equal ("To be added.", docs.Remarks); 57 | Assert.Equal (2, docs.TypeParams.Count); 58 | Assert.Equal ("D", docs.TypeParams.First ().Name); 59 | Assert.Equal ("To be added.", docs.TypeParams.First ().Value); 60 | } 61 | 62 | [Fact] 63 | public void TestLoadTypeMember() 64 | { 65 | var t = EcmaXmlLoader.LoadXType (XmlForMembers); 66 | 67 | Assert.Equal ("Test", t.Name); 68 | Assert.Single (t.Members); 69 | Assert.Equal (".ctor", t.Members.First ().Name); 70 | Assert.Equal (2, t.Members.First ().Signatures.Count); 71 | Assert.Equal ("protected App ();", t.Members.First ().Signatures.First ().Value); 72 | Assert.Equal ("Constructor", t.Members.First ().MemberType); 73 | Assert.Single (t.Members.First ().AssemblyInfos); 74 | Assert.Equal ("0.22.0.0", t.Members.First ().AssemblyInfos.First().AssemblyVersion.First()); 75 | 76 | // parameter lists 77 | var member = t.Members.First (); 78 | 79 | Assert.Equal (4, member.Parameters.Count); 80 | Assert.Equal ("a", member.Parameters[0].Name); 81 | Assert.Equal ("System.String", member.Parameters[1].Type); 82 | 83 | Assert.Single (member.TypeParameters); 84 | 85 | // docs 86 | var docs = t.Members.First().Docs; 87 | Assert.NotNull (docs); 88 | Assert.Equal ("To be added.", docs.Summary); 89 | Assert.Equal ("To be added.", docs.Remarks); 90 | Assert.Equal (4, docs.Params.Count); 91 | Assert.Equal ("a", docs.Params.First ().Name); 92 | Assert.Equal ("To be added.", docs.Params.First ().Value); 93 | } 94 | 95 | 96 | private static string XmlForIndex = @" 97 | 98 | 99 | 100 | 101 | System.Diagnostics.Debuggable(System.Diagnostics.DebuggableAttribute+DebuggingModes.IgnoreSymbolStoreSequencePoints) 102 | 103 | 104 | 105 | 106 | To be added. 107 | To be added. 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | Meadow 118 | 119 | 120 | 121 | "; 122 | private static string XmlForNS = @" 123 | 124 | To be added. 125 | To be added. 126 | 127 | 128 | "; 129 | private static string XmlForTypeOnlyDetails = @" 130 | 131 | 132 | 133 | Meadow 134 | 0.21.0.0 135 | 0.22.0.0 136 | 137 | 138 | 139 | 140 | ReferenceTypeConstraint 141 | Meadow.Hardware.IIODevice 142 | 143 | 144 | 145 | 146 | ReferenceTypeConstraint 147 | Meadow.IApp 148 | 149 | 150 | 151 | 152 | System.Object 153 | 154 | 155 | 156 | Meadow.IApp 157 | 158 | 159 | 160 | To be added. 161 | To be added. 162 | To be added. 163 | To be added. 164 | 165 | "; 166 | 167 | private static string XmlForMembers = @" 168 | 169 | 170 | 171 | 172 | Constructor 173 | 174 | 0.22.0.0 175 | 176 | 177 | 178 | 179 | 180 | [Mono.DocTest.Doc (""Type Parameter!"")] 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | To be added. 193 | To be added. 194 | To be added. 195 | To be added. 196 | To be added. 197 | To be added. 198 | 199 | 200 | 201 | 202 | "; 203 | 204 | private static string XmlForGenericType = @" 205 | 206 | 207 | 208 | Meadow 209 | 0.22.0.0 210 | 211 | 212 | 213 | 214 | ReferenceTypeConstraint 215 | Meadow.Hardware.IIODevice 216 | 217 | 218 | 219 | 220 | ReferenceTypeConstraint 221 | Meadow.IApp 222 | 223 | 224 | 225 | 226 | System.Object 227 | 228 | 229 | 230 | Meadow.IApp 231 | 232 | 233 | 234 | To be added. 235 | To be added. 236 | To be added. 237 | To be added. 238 | 239 | 240 | 241 | 242 | 243 | Constructor 244 | 245 | 0.22.0.0 246 | 247 | 248 | 249 | To be added. 250 | To be added. 251 | 252 | 253 | 254 | 255 | 256 | Property 257 | 258 | 0.22.0.0 259 | 260 | 261 | A 262 | 263 | 264 | To be added. 265 | To be added. 266 | To be added. 267 | 268 | 269 | 270 | 271 | 272 | Property 273 | 274 | 0.22.0.0 275 | 276 | 277 | D 278 | 279 | 280 | To be added. 281 | To be added. 282 | To be added. 283 | 284 | 285 | 286 | 287 | 288 | Method 289 | 290 | M:Meadow.IApp.OnWake 291 | 292 | 293 | 0.22.0.0 294 | 295 | 296 | System.Void 297 | 298 | 299 | 300 | To be added. 301 | To be added. 302 | 303 | 304 | 305 | 306 | 307 | Method 308 | 309 | M:Meadow.IApp.WillReset 310 | 311 | 312 | 0.22.0.0 313 | 314 | 315 | System.Void 316 | 317 | 318 | 319 | To be added. 320 | To be added. 321 | 322 | 323 | 324 | 325 | 326 | Method 327 | 328 | M:Meadow.IApp.WillSleep 329 | 330 | 331 | 0.22.0.0 332 | 333 | 334 | System.Void 335 | 336 | 337 | 338 | To be added. 339 | To be added. 340 | 341 | 342 | 343 | 344 | "; 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /toolsrc/Chloroplast.Test/MarkdownTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Chloroplast.Core.Rendering; 3 | using Xunit; 4 | 5 | namespace Chloroplast.Test 6 | { 7 | public class MarkdownTests 8 | { 9 | [Fact] 10 | public void SimpleMarkdown() 11 | { 12 | MarkdownRenderer renderer = new MarkdownRenderer (); 13 | var actual = renderer.Render (@"one paragraph 14 | 15 | another paragraph"); 16 | 17 | Assert.Contains ("

", actual); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /toolsrc/Chloroplast.Test/PathTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Chloroplast.Core.Extensions; 4 | using Xunit; 5 | 6 | namespace Chloroplast.Test 7 | { 8 | public class PathTests 9 | { 10 | [Fact] 11 | public void NormalizesDirCharToPlatform () 12 | { 13 | var mixedSlashPath = $"this{PathExtensions.OtherDirectorySeparator}path{Path.DirectorySeparatorChar}yes"; 14 | Assert.Equal ($"this{Path.DirectorySeparatorChar}path{Path.DirectorySeparatorChar}yes", mixedSlashPath.NormalizePath ()); 15 | } 16 | 17 | [Fact] 18 | public void ExpandsHomeDirectory () 19 | { 20 | var homePath = Path.Combine("~", "dev"); 21 | var actual = homePath.NormalizePath (); 22 | 23 | Assert.False (actual.Contains ('~'), $"didn't expand home path in {actual}"); 24 | Assert.True (actual.Length > 5, $"shorter than expected {actual}"); 25 | } 26 | 27 | [Fact] 28 | public void GetsRelativePath () 29 | { 30 | var rootPath = Path.Combine ("c:", "home", "dir", "site"); 31 | var fullPath = Path.Combine (rootPath, "area", "index.md"); 32 | var actual = fullPath.RelativePath (rootPath); 33 | Assert.DoesNotContain (rootPath, actual); 34 | Assert.False (actual.StartsWith (Path.DirectorySeparatorChar), $"Should not start with slash char, {actual}"); 35 | } 36 | 37 | [Fact] 38 | public void JoinsPaths () 39 | { 40 | var joined = "some".CombinePath ("\\path"); 41 | Assert.Equal (Path.Combine ("some", "path"), joined); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /toolsrc/Chloroplast.Test/RazorTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Chloroplast.Core.Rendering; 4 | using Xunit; 5 | 6 | namespace Chloroplast.Test 7 | { 8 | public class RazorTests 9 | { 10 | //[Fact] 11 | public async Task SimpleRender() 12 | { 13 | RazorRenderer renderer = new RazorRenderer (); 14 | 15 | var content = await renderer.RenderContentAsync (MakeContent ()); 16 | 17 | // TODO: need to mock up initialization 18 | } 19 | 20 | private RenderedContent MakeContent () 21 | { 22 | throw new NotImplementedException (); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /toolsrc/Chloroplast.Test/TreeTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using System.IO; 6 | using Chloroplast.Core; 7 | using Chloroplast.Core.Content; 8 | using Chloroplast.Core.Extensions; 9 | using Xunit; 10 | 11 | namespace Chloroplast.Test 12 | { 13 | public class TreeTests 14 | { 15 | [Fact] 16 | public void TopLevels () 17 | { 18 | List nodes = new List (); 19 | nodes.Add (MakeNode ("one")); 20 | nodes.Add (MakeNode ($"one".CombinePath ("two"))); 21 | nodes.Add (MakeNode ($"one".CombinePath ("three"))); 22 | nodes.Add (MakeNode ($"one".CombinePath ("four"))); 23 | 24 | ContentArea area = new ContentArea (nodes); 25 | var result = area.BuildHierarchy (); 26 | 27 | Assert.True (result.First ().Children.Count == 3); 28 | } 29 | 30 | [Fact] 31 | public void MultipleLevels () 32 | { 33 | List nodes = new List (); 34 | nodes.Add (MakeNode ("one")); 35 | nodes.Add (MakeNode ($"one".CombinePath ("two"))); 36 | nodes.Add (MakeNode ($"one".CombinePath ("two").CombinePath("one"))); 37 | nodes.Add (MakeNode ($"one".CombinePath ("three"))); 38 | nodes.Add (MakeNode ($"one".CombinePath ("three").CombinePath ("one"))); 39 | nodes.Add (MakeNode ($"one".CombinePath ("four"))); 40 | nodes.Add (MakeNode ($"one".CombinePath ("four").CombinePath ("one"))); 41 | nodes.Add (MakeNode ($"two")); 42 | 43 | ContentArea area = new ContentArea (nodes); 44 | var result = area.BuildHierarchy ().ToArray(); 45 | 46 | Assert.True (result[0].Children.Count == 3); //one/* 47 | Assert.True (result[1].Children.Count == 0); //two/* 48 | Assert.True (result[0].Children[0].Children.Count == 1); // one/two/* 49 | Assert.True (result[0].Children[1].Children.Count == 1); // one/three/* 50 | Assert.True (result[0].Children[2].Children.Count == 1); // one/four/* 51 | 52 | 53 | } 54 | 55 | private static ContentNode MakeNode (string path) 56 | { 57 | return new ContentNode 58 | { 59 | Slug = "one", 60 | Title = path + " title", 61 | Source = new TestSource (path) 62 | }; 63 | } 64 | } 65 | 66 | class TestSource : IFile 67 | { 68 | public DateTime LastUpdated { get; set; } 69 | public string RootRelativePath { get; set; } 70 | 71 | public TestSource(string rootPath) 72 | { 73 | this.RootRelativePath = rootPath; 74 | LastUpdated = DateTime.Now; 75 | } 76 | 77 | public void CopyTo (IFile target) 78 | { 79 | throw new NotImplementedException (); 80 | } 81 | 82 | public Task ReadContentAsync () 83 | { 84 | throw new NotImplementedException (); 85 | } 86 | 87 | public Task WriteContentAsync (string content) 88 | { 89 | throw new NotImplementedException (); 90 | } 91 | 92 | public override string ToString () 93 | { 94 | return this.RootRelativePath; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /toolsrc/Chloroplast.Test/YamlTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Chloroplast.Core.Rendering; 3 | using Chloroplast.Core.Extensions; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.FileProviders; 6 | using Dazinator.AspNet.Extensions.FileProviders; 7 | using Xunit; 8 | 9 | namespace Chloroplast.Test 10 | { 11 | public class YamlTests 12 | { 13 | [Fact] 14 | public void TestSimple() 15 | { 16 | YamlRenderer renderer = new YamlRenderer(); 17 | (var yaml, var markdown) = renderer.ParseDoc (@"--- 18 | template: Home 19 | title: Chloroplast Home 20 | subtitle: Documentation site for Chloroplast. 21 | --- 22 | 23 | # Welcome! 24 | 25 | To the chloroplast docs. yay!"); 26 | 27 | Assert.True (markdown.StartsWith ("# Welcome!"), "markdown parsed out"); 28 | Assert.Equal ("Home", yaml["template"]); 29 | Assert.Equal ("Chloroplast Home", yaml["title"]); 30 | Assert.Equal ("Documentation site for Chloroplast.", yaml["subtitle"]); 31 | } 32 | 33 | [Fact] 34 | public void TestConfig() 35 | { 36 | string yml = @"--- 37 | # Site configuration file sample for chloroplast 38 | 39 | # site basics 40 | title: Chloroplast Docs 41 | email: hello@wildernesslabs.co 42 | description: >- 43 | Chloroplast by Wilderness Labs docs. 44 | 45 | # razor templates 46 | templates_folder: Templates 47 | 48 | files: 49 | - source_file: /Docs/index.md 50 | output_folder: / 51 | 52 | # main site folder 53 | areas: 54 | - source_folder: /Docs 55 | output_folder: / 56 | "; 57 | 58 | InMemoryFileProvider s = new InMemoryFileProvider() ; 59 | s.Directory.AddFile ("/some/path/", new StringFileInfo (yml, "SiteConfig.yml")); 60 | 61 | var config = new ConfigurationBuilder () 62 | .AddChloroplastConfig (s, "/some/path/SiteConfig.yml", false, false) 63 | .Build(); 64 | 65 | Assert.Equal ("Chloroplast Docs", config["title"]); 66 | } 67 | 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /toolsrc/Chloroplast.Tool/Chloroplast.Tool.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp8.0 6 | 7 | true 8 | chloroplast 9 | ./nupkg 10 | true 11 | 12 | 0.5.5 13 | Wilderness Labs 14 | Wilderness Labs 15 | Markdown-based static site generator 16 | https://github.com/WildernessLabs/Chloroplast 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | ProjectTemplates\conceptual\SiteConfig.yml 39 | PreserveNewest 40 | 41 | 42 | ProjectTemplates\conceptual\source\Installing\index.md 43 | PreserveNewest 44 | 45 | 46 | 47 | 48 | ProjectTemplates\conceptual\source\menu.md 49 | PreserveNewest 50 | 51 | 52 | ProjectTemplates\conceptual\source\index.md 53 | PreserveNewest 54 | 55 | 56 | ProjectTemplates\conceptual\source\cli\index.md 57 | PreserveNewest 58 | 59 | 60 | ProjectTemplates\conceptual\source\templates\index.md 61 | PreserveNewest 62 | 63 | 64 | ProjectTemplates\conceptual\source\assets\main.css 65 | PreserveNewest 66 | 67 | 68 | ProjectTemplates\conceptual\source\apidocs\index.md 69 | PreserveNewest 70 | 71 | 72 | ProjectTemplates\conceptual\templates\menu.cshtml 73 | PreserveNewest 74 | 75 | 76 | ProjectTemplates\conceptual\templates\SiteFrame.cshtml 77 | PreserveNewest 78 | 79 | 80 | ProjectTemplates\conceptual\templates\Default.cshtml 81 | PreserveNewest 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /toolsrc/Chloroplast.Tool/Commands/FullBuildCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using System.Threading.Tasks.Dataflow; 6 | 7 | using Chloroplast.Core; 8 | using Chloroplast.Core.Content; 9 | using Chloroplast.Core.Extensions; 10 | using Chloroplast.Core.Rendering; 11 | using Microsoft.Extensions.Configuration; 12 | 13 | namespace Chloroplast.Tool.Commands 14 | { 15 | public class FullBuildCommand : ICliCommand 16 | { 17 | public FullBuildCommand () 18 | { 19 | } 20 | 21 | public string Name => "Build"; 22 | 23 | public async Task> RunAsync (IConfigurationRoot config) 24 | { 25 | await ContentRenderer.InitializeAsync (config); 26 | 27 | List>> tasks= new List>>(); 28 | 29 | // Start iterating over content 30 | foreach (var area in ContentArea.LoadContentAreas (config)) 31 | { 32 | Console.WriteLine ($"Processing area: {area.SourcePath}"); 33 | 34 | List>> firsttasks = new List>> (); 35 | foreach (var item in area.ContentNodes) 36 | { 37 | if (config["force"] == string.Empty && 38 | item.Source.LastUpdated <= item.Target.LastUpdated) { 39 | Console.WriteLine($"\tskipping: {item.Source.RootRelativePath}"); 40 | continue; 41 | } 42 | 43 | // TODO: refactor this out to a build queue 44 | firsttasks.Add(Task.Factory.StartNew(async () => 45 | { 46 | Console.WriteLine ($"\tdoc: {item.Source.RootRelativePath}"); 47 | 48 | if (item.Source.RootRelativePath.EndsWith(".md")) 49 | { 50 | var r = await ContentRenderer.FromMarkdownAsync(item); 51 | r = await ContentRenderer.ToRazorAsync(r); 52 | //await item.Target.WriteContentAsync(r.Body); 53 | 54 | return r; 55 | } 56 | else if (item.Source.RootRelativePath.EndsWith(".xml")) 57 | { 58 | var r = await ContentRenderer.FromEcmaXmlAsync (item, config); 59 | //r = await ContentRenderer.ToRazorAsync (r); 60 | 61 | return r; 62 | } 63 | else 64 | { 65 | item.Source.CopyTo(item.Target); 66 | return null; 67 | } 68 | })); 69 | } 70 | 71 | Task.WaitAll (firsttasks.ToArray ()); 72 | var rendered = firsttasks 73 | .Select (t => t.Result.Result) 74 | .Where(r => r != null); 75 | 76 | IEnumerable menutree; 77 | 78 | if (area is GroupContentArea) 79 | { 80 | menutree = ((GroupContentArea)area).BuildHierarchy ().ToArray (); 81 | } 82 | else // this isn't used for frame rendering anyways. TODO: refactor this out 83 | menutree = new ContentNode[0]; 84 | 85 | // this is an experimental feature that is, for now, disabled. 86 | // helpful for quickly bootstrapping menu files, but should be 87 | // fleshed out into a full subcommand at some point 88 | bool bootstrapMenu = false; 89 | if (bootstrapMenu) 90 | { 91 | var menus =  PrepareMenu (area.TargetPath, menutree); 92 | 93 | YamlRenderer.RenderAndSaveMenu (area.SourcePath.CombinePath("menu.md"), menus); 94 | } 95 | 96 | foreach (var item in rendered.Select(r => new FrameRenderedContent(r, menutree))) 97 | { 98 | tasks.Add (Task.Factory.StartNew (async () => 99 | { 100 | Console.WriteLine ($"\tframe rendering: {item.Node.Title}"); 101 | 102 | var result = await ContentRenderer.ToRazorAsync (item); 103 | await item.Node.Target.WriteContentAsync (result.Body); 104 | 105 | return result; 106 | 107 | })); 108 | } 109 | 110 | Task.WaitAll (tasks.ToArray ()); 111 | } 112 | 113 | return tasks; 114 | } 115 | 116 | private IEnumerable PrepareMenu (string areapath, IEnumerable nodes) 117 | { 118 | foreach (var node in nodes) 119 | { 120 | string title = node.Title; 121 | 122 | if (string.IsNullOrEmpty(node.Title)) 123 | { 124 | title = node.Slug.GetPathFileName ().Replace("_", " "); 125 | 126 | if (string.IsNullOrWhiteSpace (title)) 127 | continue; 128 | } 129 | 130 | var menu = new MenuNode 131 | { 132 | Title = title, 133 | Path = "/" + areapath.GetPathFileName().CombinePath(node.Slug), 134 | Items = PrepareMenu (areapath, node.Children) 135 | }; 136 | 137 | if (!menu.Items.Any ()) 138 | menu.Items = null; 139 | 140 | yield return menu; 141 | } 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /toolsrc/Chloroplast.Tool/Commands/HostCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.Configuration; 5 | using Chloroplast.Core.Extensions; 6 | using Microsoft.Extensions.Hosting; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.AspNetCore.Builder; 10 | using Microsoft.Extensions.FileProviders; 11 | using System.IO; 12 | using Chloroplast.Core; 13 | using Microsoft.Extensions.Primitives; 14 | using System.Linq; 15 | 16 | namespace Chloroplast.Tool.Commands 17 | { 18 | public class HostCommand : ICliCommand 19 | { 20 | string rootPath; 21 | string pathToUse; 22 | IConfigurationRoot rootconfig; 23 | List watchers = new List (); 24 | List areaSourcePaths = new List (); 25 | 26 | public HostCommand () 27 | { 28 | } 29 | 30 | public string Name => "Host"; 31 | 32 | public async Task> RunAsync (IConfigurationRoot config) 33 | { 34 | this.rootconfig = config; 35 | var outPath = config["out"].NormalizePath (); 36 | this.rootPath = config["root"].NormalizePath (); 37 | 38 | Console.WriteLine ($"out: " + outPath); 39 | Console.WriteLine ($"root: " + rootPath); 40 | 41 | if (!string.IsNullOrWhiteSpace(outPath)) 42 | { 43 | pathToUse = outPath; 44 | } 45 | else if (!string.IsNullOrWhiteSpace(rootPath)) 46 | { 47 | pathToUse = rootPath; 48 | } 49 | else 50 | { 51 | string potentialPath = Directory.GetCurrentDirectory ().CombinePath ("out"); 52 | if (Directory.Exists (potentialPath)) 53 | pathToUse = potentialPath; 54 | else 55 | { 56 | Console.WriteLine ("Can't start server, please provide `out` or `root` parameter to static files"); 57 | return new Task[0]; 58 | } 59 | } 60 | 61 | var host = Host.CreateDefaultBuilder () 62 | .UseContentRoot (pathToUse) 63 | .ConfigureWebHostDefaults (webBuilder => 64 | { 65 | webBuilder.CaptureStartupErrors (true); 66 | webBuilder.UseWebRoot ("/"); 67 | webBuilder.PreferHostingUrls (true); 68 | webBuilder.UseUrls ("http://localhost:5000"); 69 | webBuilder.UseStartup (); 70 | }) 71 | .UseConsoleLifetime (c => c.SuppressStatusMessages = true) 72 | .Build (); 73 | //using (host) 74 | { 75 | await host.StartAsync (); 76 | Console.WriteLine ($"started on http://localhost:5000 ... press any key to end"); 77 | 78 | try 79 | { 80 | System.Diagnostics.Process proc = new System.Diagnostics.Process (); 81 | proc.StartInfo.UseShellExecute = true; 82 | proc.StartInfo.FileName = "http://localhost:5000/"; 83 | proc.Start (); 84 | } 85 | catch (Exception ex) 86 | { 87 | Console.WriteLine ("Error starting browser, " + ex.Message); 88 | } 89 | 90 | return new[] {Task.Factory.StartNew(() => 91 | { 92 | 93 | var areaConfigs = this.rootconfig.GetSection ("areas"); 94 | 95 | if (areaConfigs != null) 96 | { 97 | foreach (var areaConfig in areaConfigs.GetChildren ()) 98 | { 99 | var areaSource = areaConfig["source_folder"]; 100 | this.areaSourcePaths.Add(areaSource); 101 | var areaSourcePath = this.rootPath.CombinePath (areaSource); 102 | Console.WriteLine ("watching: " + areaSourcePath); 103 | var watcher = new FileSystemWatcher(areaSourcePath); 104 | 105 | watcher.NotifyFilter = NotifyFilters.LastWrite 106 | | NotifyFilters.Size; 107 | 108 | watcher.Changed += Watcher_Changed; 109 | watcher.Error += Watcher_Error; 110 | 111 | // TODO: consider changing filter so we copy over static assets 112 | watcher.Filter = "*.md"; 113 | watcher.IncludeSubdirectories = true; 114 | watcher.EnableRaisingEvents = true; 115 | watchers.Add(watcher); 116 | } 117 | 118 | } 119 | else 120 | { 121 | Console.WriteLine("no source areas to watch for changes in SiteConfig.yml"); 122 | } 123 | 124 | Console.WriteLine("Press Enter to Quit Host"); 125 | Console.ReadLine(); 126 | // TODO: need a better story for disposing this in case of an error 127 | foreach(var w in watchers) 128 | { 129 | w.Changed -= Watcher_Changed; 130 | w.Error -= Watcher_Error; 131 | w.Dispose(); 132 | } 133 | watchers.Clear(); 134 | host.Dispose(); 135 | }) }; 136 | } 137 | } 138 | 139 | private static void Watcher_Error (object sender, ErrorEventArgs e) => 140 | PrintException (e.GetException ()); 141 | 142 | private static void PrintException (Exception? ex) 143 | { 144 | if (ex != null) 145 | { 146 | Console.WriteLine ($"Message: {ex.Message}"); 147 | Console.WriteLine ("Stacktrace:"); 148 | Console.WriteLine (ex.StackTrace); 149 | Console.WriteLine (); 150 | PrintException (ex.InnerException); 151 | } 152 | } 153 | 154 | bool running = false; 155 | 156 | private void Watcher_Changed (object sender, FileSystemEventArgs e) 157 | { 158 | if (e.ChangeType != WatcherChangeTypes.Changed) 159 | { 160 | return; 161 | } 162 | if (this.running) 163 | { 164 | Console.WriteLine ($"File changed during build, not rebuilt: {e.FullPath}"); 165 | // TODO: add to waiting queue 166 | return; 167 | } 168 | 169 | // TODO: implement a custom IConfigurationRoot that points to this one changed file, and call FullBuildCommand 170 | WatcherConfig config = new WatcherConfig (); 171 | config["out"] = this.pathToUse; 172 | config["root"] = this.rootPath; 173 | config["force"] = "true"; 174 | 175 | 176 | if (string.IsNullOrEmpty (rootconfig["templates_folder"])) 177 | { 178 | string[] siteFramePaths = Directory.GetFiles (this.rootPath, "SiteFrame.cshtml", SearchOption.AllDirectories); 179 | if (!siteFramePaths.Any ()) 180 | { 181 | throw new ChloroplastException ($":( Could not find 'SiteFrame.cshtml' in {this.rootPath}"); 182 | } 183 | 184 | config["templates_folder"] = Path.GetDirectoryName (siteFramePaths.First ()); 185 | } 186 | else 187 | config["templates_folder"] = rootconfig["templates_folder"]; 188 | 189 | string relativePath = e.FullPath.Replace (this.rootPath, string.Empty); 190 | 191 | string relativeOut = relativePath; 192 | if (relativeOut.EndsWith (".md")) 193 | relativeOut = Path.Combine ( 194 | Path.GetDirectoryName (relativePath), 195 | "index.html" 196 | ); 197 | foreach (var areaSourcePath in this.areaSourcePaths) 198 | { 199 | relativeOut = relativeOut.Replace (areaSourcePath, string.Empty); 200 | } 201 | 202 | config.AddSection ("files", new WatcherConfig (new Dictionary 203 | { 204 | { "source_file", relativePath }, 205 | { "output_folder", relativeOut } 206 | })); 207 | 208 | FullBuildCommand fullBuild = new FullBuildCommand (); 209 | 210 | Console.WriteLine ($"Changed: {e.FullPath}"); 211 | this.running = true; 212 | var tasks = fullBuild.RunAsync (config); 213 | tasks.Wait (); 214 | this.running = false; 215 | } 216 | 217 | internal class WatcherConfig : IConfigurationRoot, IConfigurationSection 218 | { 219 | Dictionary rootValues; 220 | Dictionary sections = new Dictionary (); 221 | 222 | public WatcherConfig () 223 | { 224 | this.rootValues = new Dictionary (); 225 | } 226 | 227 | public WatcherConfig (Dictionary values) 228 | { 229 | this.rootValues = values; 230 | } 231 | 232 | public string this[string key] 233 | { 234 | get => this.rootValues.ContainsKey(key) ? this.rootValues[key] : string.Empty; 235 | set => this.rootValues[key] = value; 236 | } 237 | 238 | public void AddSection(string key, WatcherConfig value) => this.sections[key] = value; 239 | 240 | public IConfigurationSection GetSection (string key) => this.sections.ContainsKey(key) ? this.sections[key] : null; 241 | 242 | public IEnumerable GetChildren () 243 | { 244 | return (new[] { this }).Union (this.sections.Values); 245 | } 246 | 247 | #region unused interface members 248 | 249 | public IEnumerable Providers => throw new NotImplementedException (); 250 | 251 | string IConfigurationSection.Key => throw new NotImplementedException (); 252 | 253 | string IConfigurationSection.Path => throw new NotImplementedException (); 254 | 255 | string IConfigurationSection.Value { get => throw new NotImplementedException (); set => throw new NotImplementedException (); } 256 | 257 | 258 | public IChangeToken GetReloadToken () 259 | { 260 | throw new NotImplementedException (); 261 | } 262 | 263 | public void Reload () 264 | { 265 | throw new NotImplementedException (); 266 | } 267 | #endregion 268 | } 269 | 270 | public class Startup 271 | { 272 | public Startup (IConfiguration configuration) 273 | { 274 | Configuration = configuration; 275 | } 276 | 277 | public IConfiguration Configuration { get; } 278 | 279 | public void ConfigureServices (IServiceCollection services) 280 | { 281 | } 282 | 283 | public void Configure (IApplicationBuilder app, IWebHostEnvironment env) 284 | { 285 | app.UseDeveloperExceptionPage (); 286 | 287 | 288 | app.UseFileServer (new FileServerOptions 289 | { 290 | FileProvider = new PhysicalFileProvider (env.ContentRootPath), 291 | RequestPath = "" 292 | }); 293 | 294 | app.UseRouting (); 295 | 296 | } 297 | } 298 | 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /toolsrc/Chloroplast.Tool/Commands/ICliCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.Configuration; 5 | 6 | namespace Chloroplast.Tool.Commands 7 | { 8 | public interface ICliCommand 9 | { 10 | public string Name { get; } 11 | public Task> RunAsync (IConfigurationRoot config); 12 | } 13 | 14 | public class ConfigCommand : ICliCommand 15 | { 16 | public string Name => "Config"; 17 | 18 | public Task> RunAsync (IConfigurationRoot config) => throw new NotImplementedException (); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /toolsrc/Chloroplast.Tool/Commands/NewCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Chloroplast.Core; 7 | using Chloroplast.Core.Extensions; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.Configuration.CommandLine; 10 | 11 | namespace Chloroplast.Tool.Commands 12 | { 13 | public class TemplateFileData 14 | { 15 | public string RelativeFilePath { get; set; } 16 | public Stream Stream { get; set; } 17 | } 18 | 19 | public interface INewTemplateFetcher 20 | { 21 | IEnumerable GetTemplateFiles (string from); 22 | } 23 | 24 | public class NewTemplateDiskFetcher : INewTemplateFetcher 25 | { 26 | public IEnumerable GetTemplateFiles (string from) 27 | { 28 | // get a list of files and iterate over them 29 | var files = Directory.GetFiles (from); 30 | foreach (string file in Directory.EnumerateFiles (from, "*.*", SearchOption.AllDirectories)) 31 | { 32 | FileStream sourceStream = File.Open (file, FileMode.Open); 33 | { 34 | string relativePath = file.Replace (from, string.Empty); 35 | if (relativePath.StartsWith(Path.DirectorySeparatorChar)) 36 | { 37 | relativePath = relativePath.Substring (1, relativePath.Length - 1); 38 | } 39 | 40 | yield return new TemplateFileData 41 | { 42 | RelativeFilePath = relativePath, 43 | Stream = sourceStream 44 | }; 45 | } 46 | } 47 | } 48 | 49 | } 50 | 51 | public class NewCommand : ICliCommand 52 | { 53 | 54 | private IConfigurationRoot config; 55 | private string[] args; 56 | 57 | public NewCommand (string[] args) 58 | { 59 | if (args != null && args.Length < 3) 60 | { 61 | throw new ChloroplastException ($"Need two parameters for 'new' command:{Environment.NewLine}chloroplast new