feat: initial release

This commit is contained in:
Dylan R. E. Moonfire 2021-09-07 00:15:45 -05:00
commit 78054ee2a7
164 changed files with 22055 additions and 0 deletions

122
.editorconfig Normal file
View File

@ -0,0 +1,122 @@
# EditorConfig is awesome: https://EditorConfig.org
root = true
[*]
charset=utf-8
end_of_line = lf
insert_final_newline=true
indent_style=space
indent_size=4
# Microsoft .NET properties
csharp_new_line_before_members_in_object_initializers=false
csharp_preferred_modifier_order=public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion
csharp_space_after_cast=false
csharp_style_var_elsewhere=false:hint
csharp_style_var_for_built_in_types=false:hint
csharp_style_var_when_type_is_apparent=true:hint
csharp_preserve_single_line_statements=false
csharp_preserve_single_line_blocks=true
dotnet_style_predefined_type_for_locals_parameters_members=true:hint
dotnet_style_predefined_type_for_member_access=true:hint
dotnet_style_qualification_for_event=true:hint
dotnet_style_qualification_for_field=true:hint
dotnet_style_qualification_for_method=true:hint
dotnet_style_qualification_for_property=true:hint
dotnet_style_require_accessibility_modifiers=for_non_interface_members:hint
# ReSharper properties
resharper_alignment_tab_fill_style=optimal_fill
resharper_apply_on_completion=true
resharper_blank_lines_after_control_transfer_statements=1
resharper_blank_lines_around_single_line_auto_property=1
resharper_blank_lines_around_single_line_property=1
resharper_blank_lines_before_single_line_comment=1
resharper_blank_lines_between_using_groups=1
resharper_braces_for_for=required
resharper_braces_for_foreach=required
resharper_braces_for_ifelse=required
resharper_braces_for_while=required
resharper_can_use_global_alias=false
resharper_csharp_blank_lines_around_single_line_field=1
resharper_csharp_blank_lines_around_single_line_invocable=1
resharper_csharp_indent_style=tab
resharper_csharp_insert_final_newline=true
resharper_csharp_keep_blank_lines_in_code=1
resharper_csharp_keep_blank_lines_in_declarations=1
resharper_csharp_new_line_before_while=true
resharper_csharp_use_indent_from_vs=false
resharper_csharp_wrap_arguments_style=chop_if_long
resharper_csharp_wrap_extends_list_style=chop_if_long
resharper_csharp_wrap_parameters_style=chop_if_long
resharper_css_insert_final_newline=false
resharper_enforce_line_ending_style=true
resharper_html_insert_final_newline=false
resharper_indent_nested_fixed_stmt=true
resharper_js_indent_style=tab
resharper_js_insert_final_newline=true
resharper_js_keep_blank_lines_in_code=1
resharper_js_stick_comment=false
resharper_js_use_indent_from_vs=false
resharper_js_wrap_before_binary_opsign=true
resharper_js_wrap_chained_method_calls=chop_if_long
resharper_keep_blank_lines_between_declarations=1
resharper_min_blank_lines_after_imports=1
resharper_place_attribute_on_same_line=False
resharper_place_constructor_initializer_on_same_line=false
resharper_place_type_constraints_on_same_line=false
resharper_protobuf_insert_final_newline=false
resharper_qualified_using_at_nested_scope=true
resharper_resx_insert_final_newline=false
resharper_space_within_single_line_array_initializer_braces=true
resharper_use_indents_from_main_language_in_file=false
resharper_vb_insert_final_newline=false
resharper_wrap_after_declaration_lpar=true
resharper_wrap_after_invocation_lpar=true
resharper_wrap_before_extends_colon=true
resharper_wrap_before_first_type_parameter_constraint=true
resharper_wrap_before_type_parameter_langle=true
resharper_xmldoc_indent_child_elements=ZeroIndent
resharper_xmldoc_indent_text=ZeroIndent
resharper_xmldoc_insert_final_newline=false
resharper_xml_insert_final_newline=false
# ReSharper inspection severities
resharper_check_namespace_highlighting=none
resharper_convert_to_auto_property_highlighting=none
resharper_localizable_element_highlighting=none
resharper_redundant_comma_in_attribute_list_highlighting=none
resharper_redundant_comma_in_enum_declaration_highlighting=none
resharper_redundant_comma_in_initializer_highlighting=none
resharper_string_compare_to_is_culture_specific_highlighting=none
resharper_string_index_of_is_culture_specific_1_highlighting=none
resharper_use_null_propagation_highlighting=none
resharper_use_object_or_collection_initializer_highlighting=hint
resharper_use_string_interpolation_highlighting=hint
# Matches the exact files either package.json or .travis.yml
[{package.json,.travis.yml}]
indent_style=space
indent_size=2
tab_width=2
[*.{cs,js,json,jsx,proto,resjson,ts,tsx}]
indent_style=space
indent_size=space
tab_width=4
[*.{asax,ascx,aspx,cshtml,css,htm,html,master,razor,skin,vb,xaml,xamlx,xoml}]
indent_style=space
indent_size=4
tab_width=4
[*.{appxmanifest,build,config,csproj,dbml,discomap,dtd,jsproj,lsproj,njsproj,nuspec,proj,props,resw,resx,StyleCop,targets,tasks,vbproj,xml,xsd}]
indent_style=space
indent_size=2
tab_width=2
[*.proto]
indent_style=space
indent_size=2
tab_width=2

1
.envrc Normal file
View File

@ -0,0 +1 @@
use asdf

13
.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
launchSettings.json
*~
*.user
Directory.Build.props
obj/
[Bb]in/
.vs/
.vscode/
.idea/
_ReSharper.Caches/
node_modules/

51
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,51 @@
stages:
- build
default:
before_script:
- curl -sL https://deb.nodesource.com/setup_15.x | bash -
- apt-get install -y nodejs
build:
image: mcr.microsoft.com/dotnet/sdk:5.0
stage: build
script:
# Set up the environment.
- npx npm install --ci
- npx commitlint-gitlab-ci -x @commitlint/config-conventional
# Build and test everything.
- dotnet restore
- dotnet build
#- 'dotnet test --test-adapter-path:. --logger:"junit;LogFilePath=../artifacts/{assembly}-test-result.xml;MethodFormat=Default;FailureBodyFormat=Verbose" --collect:"XPlat Code Coverage"'
# Summarize the output for Gitlab CI reporting.
#- dotnet new tool-manifest
#- dotnet tool install dotnet-reportgenerator-globaltool
#- dotnet tool run reportgenerator -reports:src/*/TestResults/*/coverage.cobertura.xml -targetdir:./coverage "-reporttypes:Cobertura;TextSummary"
#- grep "Line coverage" coverage/Summary.txt
# Perform the release.
- npx semantic-release
rules:
- if: '$CI_COMMIT_TITLE =~ /^chore\(release\)/'
when: never
- if: '$CI_COMMIT_TAG'
when: never
- if: '$CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH'
when: never
- when: on_success
artifacts:
when: always
paths:
- ./**/*test-result.xml
- ./coverage/Cobertura.xml
- ./coverage/Summary.*
- ./**/*.nupkg
reports:
junit:
- ./**/*test-result.xml
cobertura:
- ./coverage/Cobertura.xml

4
.husky/commit-msg Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no-install commitlint --edit $1

3
.tool-versions Normal file
View File

@ -0,0 +1,3 @@
dotnet-core 5.0.100
yarn 1.22.10
nodejs 15.0.1

21
LICENSE.txt Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) Moonfire Games
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

264
Nitride.sln Normal file
View File

@ -0,0 +1,264 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26124.0
MinimumVisualStudioVersion = 15.0.26124.0
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{570184FD-ECE4-4EC8-86E1-C1265E17D647}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nitride.Calendar", "src\Nitride.Calendar\Nitride.Calendar.csproj", "{D480943C-764D-4A8A-B546-642ED10586BB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nitride.Feeds", "src\Nitride.Feeds\Nitride.Feeds.csproj", "{1204DECC-654A-433A-BF82-53F98AB24DCF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nitride.Gemtext", "src\Nitride.Gemtext\Nitride.Gemtext.csproj", "{23C7CBF7-9624-457A-8296-C03F75BC9BC6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nitride.Generators", "src\Nitride.Generators\Nitride.Generators.csproj", "{4ACB11B7-1EEB-48E7-845A-528770839125}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nitride.Handlebars", "src\Nitride.Handlebars\Nitride.Handlebars.csproj", "{56E595A6-7880-416E-B328-93B9617F92A6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nitride.Html", "src\Nitride.Html\Nitride.Html.csproj", "{3F9292D3-DA50-4DBA-AE90-E33E462470A1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nitride.IO", "src\Nitride.IO\Nitride.IO.csproj", "{534BF940-25B2-4948-A101-7890CC9C4EA5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nitride.IO.Tests", "src\Nitride.IO.Tests\Nitride.IO.Tests.csproj", "{57F4C4C3-A2C9-4D6C-AA51-9A7EC926918D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nitride.Javascript", "src\Nitride.Javascript\Nitride.Javascript.csproj", "{285A855E-B2FC-4770-AFD9-4CC67AD3C83C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nitride.Markdown", "src\Nitride.Markdown\Nitride.Markdown.csproj", "{41FF3823-7008-43B1-AD6A-92437E0600B7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nitride", "src\Nitride\Nitride.csproj", "{757BA115-3465-46C5-ADDB-7B96D6900F33}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nitride.Slugs", "src\Nitride.Slugs\Nitride.Slugs.csproj", "{17BF2A03-2C1D-4F75-9C18-B4341FAAF1A5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nitride.Temporal", "src\Nitride.Temporal\Nitride.Temporal.csproj", "{FEFFA469-245E-45A7-A094-3F0E89CEF3A9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nitride.Tests", "src\Nitride.Tests\Nitride.Tests.csproj", "{A0EED899-35C7-4C1C-8BBF-F7FD2F281839}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nitride.Yaml", "src\Nitride.Yaml\Nitride.Yaml.csproj", "{1A5B4B4D-CF32-4458-8F2C-83A2AE494837}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nitride.Yaml.Tests", "src\Nitride.Yaml.Tests\Nitride.Yaml.Tests.csproj", "{C8CD60C2-2A26-48AE-848D-A71AEE2267C9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{D480943C-764D-4A8A-B546-642ED10586BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D480943C-764D-4A8A-B546-642ED10586BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D480943C-764D-4A8A-B546-642ED10586BB}.Debug|x64.ActiveCfg = Debug|Any CPU
{D480943C-764D-4A8A-B546-642ED10586BB}.Debug|x64.Build.0 = Debug|Any CPU
{D480943C-764D-4A8A-B546-642ED10586BB}.Debug|x86.ActiveCfg = Debug|Any CPU
{D480943C-764D-4A8A-B546-642ED10586BB}.Debug|x86.Build.0 = Debug|Any CPU
{D480943C-764D-4A8A-B546-642ED10586BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D480943C-764D-4A8A-B546-642ED10586BB}.Release|Any CPU.Build.0 = Release|Any CPU
{D480943C-764D-4A8A-B546-642ED10586BB}.Release|x64.ActiveCfg = Release|Any CPU
{D480943C-764D-4A8A-B546-642ED10586BB}.Release|x64.Build.0 = Release|Any CPU
{D480943C-764D-4A8A-B546-642ED10586BB}.Release|x86.ActiveCfg = Release|Any CPU
{D480943C-764D-4A8A-B546-642ED10586BB}.Release|x86.Build.0 = Release|Any CPU
{1204DECC-654A-433A-BF82-53F98AB24DCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1204DECC-654A-433A-BF82-53F98AB24DCF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1204DECC-654A-433A-BF82-53F98AB24DCF}.Debug|x64.ActiveCfg = Debug|Any CPU
{1204DECC-654A-433A-BF82-53F98AB24DCF}.Debug|x64.Build.0 = Debug|Any CPU
{1204DECC-654A-433A-BF82-53F98AB24DCF}.Debug|x86.ActiveCfg = Debug|Any CPU
{1204DECC-654A-433A-BF82-53F98AB24DCF}.Debug|x86.Build.0 = Debug|Any CPU
{1204DECC-654A-433A-BF82-53F98AB24DCF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1204DECC-654A-433A-BF82-53F98AB24DCF}.Release|Any CPU.Build.0 = Release|Any CPU
{1204DECC-654A-433A-BF82-53F98AB24DCF}.Release|x64.ActiveCfg = Release|Any CPU
{1204DECC-654A-433A-BF82-53F98AB24DCF}.Release|x64.Build.0 = Release|Any CPU
{1204DECC-654A-433A-BF82-53F98AB24DCF}.Release|x86.ActiveCfg = Release|Any CPU
{1204DECC-654A-433A-BF82-53F98AB24DCF}.Release|x86.Build.0 = Release|Any CPU
{23C7CBF7-9624-457A-8296-C03F75BC9BC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{23C7CBF7-9624-457A-8296-C03F75BC9BC6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{23C7CBF7-9624-457A-8296-C03F75BC9BC6}.Debug|x64.ActiveCfg = Debug|Any CPU
{23C7CBF7-9624-457A-8296-C03F75BC9BC6}.Debug|x64.Build.0 = Debug|Any CPU
{23C7CBF7-9624-457A-8296-C03F75BC9BC6}.Debug|x86.ActiveCfg = Debug|Any CPU
{23C7CBF7-9624-457A-8296-C03F75BC9BC6}.Debug|x86.Build.0 = Debug|Any CPU
{23C7CBF7-9624-457A-8296-C03F75BC9BC6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{23C7CBF7-9624-457A-8296-C03F75BC9BC6}.Release|Any CPU.Build.0 = Release|Any CPU
{23C7CBF7-9624-457A-8296-C03F75BC9BC6}.Release|x64.ActiveCfg = Release|Any CPU
{23C7CBF7-9624-457A-8296-C03F75BC9BC6}.Release|x64.Build.0 = Release|Any CPU
{23C7CBF7-9624-457A-8296-C03F75BC9BC6}.Release|x86.ActiveCfg = Release|Any CPU
{23C7CBF7-9624-457A-8296-C03F75BC9BC6}.Release|x86.Build.0 = Release|Any CPU
{4ACB11B7-1EEB-48E7-845A-528770839125}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4ACB11B7-1EEB-48E7-845A-528770839125}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4ACB11B7-1EEB-48E7-845A-528770839125}.Debug|x64.ActiveCfg = Debug|Any CPU
{4ACB11B7-1EEB-48E7-845A-528770839125}.Debug|x64.Build.0 = Debug|Any CPU
{4ACB11B7-1EEB-48E7-845A-528770839125}.Debug|x86.ActiveCfg = Debug|Any CPU
{4ACB11B7-1EEB-48E7-845A-528770839125}.Debug|x86.Build.0 = Debug|Any CPU
{4ACB11B7-1EEB-48E7-845A-528770839125}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4ACB11B7-1EEB-48E7-845A-528770839125}.Release|Any CPU.Build.0 = Release|Any CPU
{4ACB11B7-1EEB-48E7-845A-528770839125}.Release|x64.ActiveCfg = Release|Any CPU
{4ACB11B7-1EEB-48E7-845A-528770839125}.Release|x64.Build.0 = Release|Any CPU
{4ACB11B7-1EEB-48E7-845A-528770839125}.Release|x86.ActiveCfg = Release|Any CPU
{4ACB11B7-1EEB-48E7-845A-528770839125}.Release|x86.Build.0 = Release|Any CPU
{56E595A6-7880-416E-B328-93B9617F92A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{56E595A6-7880-416E-B328-93B9617F92A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{56E595A6-7880-416E-B328-93B9617F92A6}.Debug|x64.ActiveCfg = Debug|Any CPU
{56E595A6-7880-416E-B328-93B9617F92A6}.Debug|x64.Build.0 = Debug|Any CPU
{56E595A6-7880-416E-B328-93B9617F92A6}.Debug|x86.ActiveCfg = Debug|Any CPU
{56E595A6-7880-416E-B328-93B9617F92A6}.Debug|x86.Build.0 = Debug|Any CPU
{56E595A6-7880-416E-B328-93B9617F92A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{56E595A6-7880-416E-B328-93B9617F92A6}.Release|Any CPU.Build.0 = Release|Any CPU
{56E595A6-7880-416E-B328-93B9617F92A6}.Release|x64.ActiveCfg = Release|Any CPU
{56E595A6-7880-416E-B328-93B9617F92A6}.Release|x64.Build.0 = Release|Any CPU
{56E595A6-7880-416E-B328-93B9617F92A6}.Release|x86.ActiveCfg = Release|Any CPU
{56E595A6-7880-416E-B328-93B9617F92A6}.Release|x86.Build.0 = Release|Any CPU
{3F9292D3-DA50-4DBA-AE90-E33E462470A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3F9292D3-DA50-4DBA-AE90-E33E462470A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3F9292D3-DA50-4DBA-AE90-E33E462470A1}.Debug|x64.ActiveCfg = Debug|Any CPU
{3F9292D3-DA50-4DBA-AE90-E33E462470A1}.Debug|x64.Build.0 = Debug|Any CPU
{3F9292D3-DA50-4DBA-AE90-E33E462470A1}.Debug|x86.ActiveCfg = Debug|Any CPU
{3F9292D3-DA50-4DBA-AE90-E33E462470A1}.Debug|x86.Build.0 = Debug|Any CPU
{3F9292D3-DA50-4DBA-AE90-E33E462470A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3F9292D3-DA50-4DBA-AE90-E33E462470A1}.Release|Any CPU.Build.0 = Release|Any CPU
{3F9292D3-DA50-4DBA-AE90-E33E462470A1}.Release|x64.ActiveCfg = Release|Any CPU
{3F9292D3-DA50-4DBA-AE90-E33E462470A1}.Release|x64.Build.0 = Release|Any CPU
{3F9292D3-DA50-4DBA-AE90-E33E462470A1}.Release|x86.ActiveCfg = Release|Any CPU
{3F9292D3-DA50-4DBA-AE90-E33E462470A1}.Release|x86.Build.0 = Release|Any CPU
{534BF940-25B2-4948-A101-7890CC9C4EA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{534BF940-25B2-4948-A101-7890CC9C4EA5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{534BF940-25B2-4948-A101-7890CC9C4EA5}.Debug|x64.ActiveCfg = Debug|Any CPU
{534BF940-25B2-4948-A101-7890CC9C4EA5}.Debug|x64.Build.0 = Debug|Any CPU
{534BF940-25B2-4948-A101-7890CC9C4EA5}.Debug|x86.ActiveCfg = Debug|Any CPU
{534BF940-25B2-4948-A101-7890CC9C4EA5}.Debug|x86.Build.0 = Debug|Any CPU
{534BF940-25B2-4948-A101-7890CC9C4EA5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{534BF940-25B2-4948-A101-7890CC9C4EA5}.Release|Any CPU.Build.0 = Release|Any CPU
{534BF940-25B2-4948-A101-7890CC9C4EA5}.Release|x64.ActiveCfg = Release|Any CPU
{534BF940-25B2-4948-A101-7890CC9C4EA5}.Release|x64.Build.0 = Release|Any CPU
{534BF940-25B2-4948-A101-7890CC9C4EA5}.Release|x86.ActiveCfg = Release|Any CPU
{534BF940-25B2-4948-A101-7890CC9C4EA5}.Release|x86.Build.0 = Release|Any CPU
{57F4C4C3-A2C9-4D6C-AA51-9A7EC926918D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{57F4C4C3-A2C9-4D6C-AA51-9A7EC926918D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{57F4C4C3-A2C9-4D6C-AA51-9A7EC926918D}.Debug|x64.ActiveCfg = Debug|Any CPU
{57F4C4C3-A2C9-4D6C-AA51-9A7EC926918D}.Debug|x64.Build.0 = Debug|Any CPU
{57F4C4C3-A2C9-4D6C-AA51-9A7EC926918D}.Debug|x86.ActiveCfg = Debug|Any CPU
{57F4C4C3-A2C9-4D6C-AA51-9A7EC926918D}.Debug|x86.Build.0 = Debug|Any CPU
{57F4C4C3-A2C9-4D6C-AA51-9A7EC926918D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{57F4C4C3-A2C9-4D6C-AA51-9A7EC926918D}.Release|Any CPU.Build.0 = Release|Any CPU
{57F4C4C3-A2C9-4D6C-AA51-9A7EC926918D}.Release|x64.ActiveCfg = Release|Any CPU
{57F4C4C3-A2C9-4D6C-AA51-9A7EC926918D}.Release|x64.Build.0 = Release|Any CPU
{57F4C4C3-A2C9-4D6C-AA51-9A7EC926918D}.Release|x86.ActiveCfg = Release|Any CPU
{57F4C4C3-A2C9-4D6C-AA51-9A7EC926918D}.Release|x86.Build.0 = Release|Any CPU
{285A855E-B2FC-4770-AFD9-4CC67AD3C83C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{285A855E-B2FC-4770-AFD9-4CC67AD3C83C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{285A855E-B2FC-4770-AFD9-4CC67AD3C83C}.Debug|x64.ActiveCfg = Debug|Any CPU
{285A855E-B2FC-4770-AFD9-4CC67AD3C83C}.Debug|x64.Build.0 = Debug|Any CPU
{285A855E-B2FC-4770-AFD9-4CC67AD3C83C}.Debug|x86.ActiveCfg = Debug|Any CPU
{285A855E-B2FC-4770-AFD9-4CC67AD3C83C}.Debug|x86.Build.0 = Debug|Any CPU
{285A855E-B2FC-4770-AFD9-4CC67AD3C83C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{285A855E-B2FC-4770-AFD9-4CC67AD3C83C}.Release|Any CPU.Build.0 = Release|Any CPU
{285A855E-B2FC-4770-AFD9-4CC67AD3C83C}.Release|x64.ActiveCfg = Release|Any CPU
{285A855E-B2FC-4770-AFD9-4CC67AD3C83C}.Release|x64.Build.0 = Release|Any CPU
{285A855E-B2FC-4770-AFD9-4CC67AD3C83C}.Release|x86.ActiveCfg = Release|Any CPU
{285A855E-B2FC-4770-AFD9-4CC67AD3C83C}.Release|x86.Build.0 = Release|Any CPU
{41FF3823-7008-43B1-AD6A-92437E0600B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{41FF3823-7008-43B1-AD6A-92437E0600B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{41FF3823-7008-43B1-AD6A-92437E0600B7}.Debug|x64.ActiveCfg = Debug|Any CPU
{41FF3823-7008-43B1-AD6A-92437E0600B7}.Debug|x64.Build.0 = Debug|Any CPU
{41FF3823-7008-43B1-AD6A-92437E0600B7}.Debug|x86.ActiveCfg = Debug|Any CPU
{41FF3823-7008-43B1-AD6A-92437E0600B7}.Debug|x86.Build.0 = Debug|Any CPU
{41FF3823-7008-43B1-AD6A-92437E0600B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{41FF3823-7008-43B1-AD6A-92437E0600B7}.Release|Any CPU.Build.0 = Release|Any CPU
{41FF3823-7008-43B1-AD6A-92437E0600B7}.Release|x64.ActiveCfg = Release|Any CPU
{41FF3823-7008-43B1-AD6A-92437E0600B7}.Release|x64.Build.0 = Release|Any CPU
{41FF3823-7008-43B1-AD6A-92437E0600B7}.Release|x86.ActiveCfg = Release|Any CPU
{41FF3823-7008-43B1-AD6A-92437E0600B7}.Release|x86.Build.0 = Release|Any CPU
{757BA115-3465-46C5-ADDB-7B96D6900F33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{757BA115-3465-46C5-ADDB-7B96D6900F33}.Debug|Any CPU.Build.0 = Debug|Any CPU
{757BA115-3465-46C5-ADDB-7B96D6900F33}.Debug|x64.ActiveCfg = Debug|Any CPU
{757BA115-3465-46C5-ADDB-7B96D6900F33}.Debug|x64.Build.0 = Debug|Any CPU
{757BA115-3465-46C5-ADDB-7B96D6900F33}.Debug|x86.ActiveCfg = Debug|Any CPU
{757BA115-3465-46C5-ADDB-7B96D6900F33}.Debug|x86.Build.0 = Debug|Any CPU
{757BA115-3465-46C5-ADDB-7B96D6900F33}.Release|Any CPU.ActiveCfg = Release|Any CPU
{757BA115-3465-46C5-ADDB-7B96D6900F33}.Release|Any CPU.Build.0 = Release|Any CPU
{757BA115-3465-46C5-ADDB-7B96D6900F33}.Release|x64.ActiveCfg = Release|Any CPU
{757BA115-3465-46C5-ADDB-7B96D6900F33}.Release|x64.Build.0 = Release|Any CPU
{757BA115-3465-46C5-ADDB-7B96D6900F33}.Release|x86.ActiveCfg = Release|Any CPU
{757BA115-3465-46C5-ADDB-7B96D6900F33}.Release|x86.Build.0 = Release|Any CPU
{17BF2A03-2C1D-4F75-9C18-B4341FAAF1A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{17BF2A03-2C1D-4F75-9C18-B4341FAAF1A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{17BF2A03-2C1D-4F75-9C18-B4341FAAF1A5}.Debug|x64.ActiveCfg = Debug|Any CPU
{17BF2A03-2C1D-4F75-9C18-B4341FAAF1A5}.Debug|x64.Build.0 = Debug|Any CPU
{17BF2A03-2C1D-4F75-9C18-B4341FAAF1A5}.Debug|x86.ActiveCfg = Debug|Any CPU
{17BF2A03-2C1D-4F75-9C18-B4341FAAF1A5}.Debug|x86.Build.0 = Debug|Any CPU
{17BF2A03-2C1D-4F75-9C18-B4341FAAF1A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{17BF2A03-2C1D-4F75-9C18-B4341FAAF1A5}.Release|Any CPU.Build.0 = Release|Any CPU
{17BF2A03-2C1D-4F75-9C18-B4341FAAF1A5}.Release|x64.ActiveCfg = Release|Any CPU
{17BF2A03-2C1D-4F75-9C18-B4341FAAF1A5}.Release|x64.Build.0 = Release|Any CPU
{17BF2A03-2C1D-4F75-9C18-B4341FAAF1A5}.Release|x86.ActiveCfg = Release|Any CPU
{17BF2A03-2C1D-4F75-9C18-B4341FAAF1A5}.Release|x86.Build.0 = Release|Any CPU
{FEFFA469-245E-45A7-A094-3F0E89CEF3A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FEFFA469-245E-45A7-A094-3F0E89CEF3A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FEFFA469-245E-45A7-A094-3F0E89CEF3A9}.Debug|x64.ActiveCfg = Debug|Any CPU
{FEFFA469-245E-45A7-A094-3F0E89CEF3A9}.Debug|x64.Build.0 = Debug|Any CPU
{FEFFA469-245E-45A7-A094-3F0E89CEF3A9}.Debug|x86.ActiveCfg = Debug|Any CPU
{FEFFA469-245E-45A7-A094-3F0E89CEF3A9}.Debug|x86.Build.0 = Debug|Any CPU
{FEFFA469-245E-45A7-A094-3F0E89CEF3A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FEFFA469-245E-45A7-A094-3F0E89CEF3A9}.Release|Any CPU.Build.0 = Release|Any CPU
{FEFFA469-245E-45A7-A094-3F0E89CEF3A9}.Release|x64.ActiveCfg = Release|Any CPU
{FEFFA469-245E-45A7-A094-3F0E89CEF3A9}.Release|x64.Build.0 = Release|Any CPU
{FEFFA469-245E-45A7-A094-3F0E89CEF3A9}.Release|x86.ActiveCfg = Release|Any CPU
{FEFFA469-245E-45A7-A094-3F0E89CEF3A9}.Release|x86.Build.0 = Release|Any CPU
{A0EED899-35C7-4C1C-8BBF-F7FD2F281839}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A0EED899-35C7-4C1C-8BBF-F7FD2F281839}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A0EED899-35C7-4C1C-8BBF-F7FD2F281839}.Debug|x64.ActiveCfg = Debug|Any CPU
{A0EED899-35C7-4C1C-8BBF-F7FD2F281839}.Debug|x64.Build.0 = Debug|Any CPU
{A0EED899-35C7-4C1C-8BBF-F7FD2F281839}.Debug|x86.ActiveCfg = Debug|Any CPU
{A0EED899-35C7-4C1C-8BBF-F7FD2F281839}.Debug|x86.Build.0 = Debug|Any CPU
{A0EED899-35C7-4C1C-8BBF-F7FD2F281839}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A0EED899-35C7-4C1C-8BBF-F7FD2F281839}.Release|Any CPU.Build.0 = Release|Any CPU
{A0EED899-35C7-4C1C-8BBF-F7FD2F281839}.Release|x64.ActiveCfg = Release|Any CPU
{A0EED899-35C7-4C1C-8BBF-F7FD2F281839}.Release|x64.Build.0 = Release|Any CPU
{A0EED899-35C7-4C1C-8BBF-F7FD2F281839}.Release|x86.ActiveCfg = Release|Any CPU
{A0EED899-35C7-4C1C-8BBF-F7FD2F281839}.Release|x86.Build.0 = Release|Any CPU
{1A5B4B4D-CF32-4458-8F2C-83A2AE494837}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1A5B4B4D-CF32-4458-8F2C-83A2AE494837}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1A5B4B4D-CF32-4458-8F2C-83A2AE494837}.Debug|x64.ActiveCfg = Debug|Any CPU
{1A5B4B4D-CF32-4458-8F2C-83A2AE494837}.Debug|x64.Build.0 = Debug|Any CPU
{1A5B4B4D-CF32-4458-8F2C-83A2AE494837}.Debug|x86.ActiveCfg = Debug|Any CPU
{1A5B4B4D-CF32-4458-8F2C-83A2AE494837}.Debug|x86.Build.0 = Debug|Any CPU
{1A5B4B4D-CF32-4458-8F2C-83A2AE494837}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1A5B4B4D-CF32-4458-8F2C-83A2AE494837}.Release|Any CPU.Build.0 = Release|Any CPU
{1A5B4B4D-CF32-4458-8F2C-83A2AE494837}.Release|x64.ActiveCfg = Release|Any CPU
{1A5B4B4D-CF32-4458-8F2C-83A2AE494837}.Release|x64.Build.0 = Release|Any CPU
{1A5B4B4D-CF32-4458-8F2C-83A2AE494837}.Release|x86.ActiveCfg = Release|Any CPU
{1A5B4B4D-CF32-4458-8F2C-83A2AE494837}.Release|x86.Build.0 = Release|Any CPU
{C8CD60C2-2A26-48AE-848D-A71AEE2267C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C8CD60C2-2A26-48AE-848D-A71AEE2267C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C8CD60C2-2A26-48AE-848D-A71AEE2267C9}.Debug|x64.ActiveCfg = Debug|Any CPU
{C8CD60C2-2A26-48AE-848D-A71AEE2267C9}.Debug|x64.Build.0 = Debug|Any CPU
{C8CD60C2-2A26-48AE-848D-A71AEE2267C9}.Debug|x86.ActiveCfg = Debug|Any CPU
{C8CD60C2-2A26-48AE-848D-A71AEE2267C9}.Debug|x86.Build.0 = Debug|Any CPU
{C8CD60C2-2A26-48AE-848D-A71AEE2267C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C8CD60C2-2A26-48AE-848D-A71AEE2267C9}.Release|Any CPU.Build.0 = Release|Any CPU
{C8CD60C2-2A26-48AE-848D-A71AEE2267C9}.Release|x64.ActiveCfg = Release|Any CPU
{C8CD60C2-2A26-48AE-848D-A71AEE2267C9}.Release|x64.Build.0 = Release|Any CPU
{C8CD60C2-2A26-48AE-848D-A71AEE2267C9}.Release|x86.ActiveCfg = Release|Any CPU
{C8CD60C2-2A26-48AE-848D-A71AEE2267C9}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{D480943C-764D-4A8A-B546-642ED10586BB} = {570184FD-ECE4-4EC8-86E1-C1265E17D647}
{1204DECC-654A-433A-BF82-53F98AB24DCF} = {570184FD-ECE4-4EC8-86E1-C1265E17D647}
{23C7CBF7-9624-457A-8296-C03F75BC9BC6} = {570184FD-ECE4-4EC8-86E1-C1265E17D647}
{4ACB11B7-1EEB-48E7-845A-528770839125} = {570184FD-ECE4-4EC8-86E1-C1265E17D647}
{56E595A6-7880-416E-B328-93B9617F92A6} = {570184FD-ECE4-4EC8-86E1-C1265E17D647}
{3F9292D3-DA50-4DBA-AE90-E33E462470A1} = {570184FD-ECE4-4EC8-86E1-C1265E17D647}
{534BF940-25B2-4948-A101-7890CC9C4EA5} = {570184FD-ECE4-4EC8-86E1-C1265E17D647}
{57F4C4C3-A2C9-4D6C-AA51-9A7EC926918D} = {570184FD-ECE4-4EC8-86E1-C1265E17D647}
{285A855E-B2FC-4770-AFD9-4CC67AD3C83C} = {570184FD-ECE4-4EC8-86E1-C1265E17D647}
{41FF3823-7008-43B1-AD6A-92437E0600B7} = {570184FD-ECE4-4EC8-86E1-C1265E17D647}
{757BA115-3465-46C5-ADDB-7B96D6900F33} = {570184FD-ECE4-4EC8-86E1-C1265E17D647}
{17BF2A03-2C1D-4F75-9C18-B4341FAAF1A5} = {570184FD-ECE4-4EC8-86E1-C1265E17D647}
{FEFFA469-245E-45A7-A094-3F0E89CEF3A9} = {570184FD-ECE4-4EC8-86E1-C1265E17D647}
{A0EED899-35C7-4C1C-8BBF-F7FD2F281839} = {570184FD-ECE4-4EC8-86E1-C1265E17D647}
{1A5B4B4D-CF32-4458-8F2C-83A2AE494837} = {570184FD-ECE4-4EC8-86E1-C1265E17D647}
{C8CD60C2-2A26-48AE-848D-A71AEE2267C9} = {570184FD-ECE4-4EC8-86E1-C1265E17D647}
EndGlobalSection
EndGlobal

3
Nitride.sln.DotSettings Normal file
View File

@ -0,0 +1,3 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Gemtext/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Markdig/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

1370
Nitride.sln.Dotsettings Normal file

File diff suppressed because it is too large Load Diff

7
NuGet.Config Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
<add key="mfgames" value="https://www.myget.org/F/mfgames/api/v3/index.json" protocolVersion="3" />
</packageSources>
</configuration>

4
README.md Normal file
View File

@ -0,0 +1,4 @@
Nitride CIL
===========
A static site generator based on the [Gallium ECS](https://gitlab.com/mfgames-cil/gallium-cil/).

3
commitlint.config.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
extends: ["@commitlint/config-conventional"],
};

12304
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "nitride-cil",
"version": "1.0.0",
"private": true,
"scripts": {
"prepare": "husky install"
},
"devDependencies": {
"@commitlint/cli": "^13.1.0",
"@commitlint/config-conventional": "^13.1.0",
"@semantic-release/changelog": "^5.0.1",
"@semantic-release/git": "^9.0.0",
"@semantic-release/gitlab": "^6.2.2",
"@semantic-release/npm": "^7.1.3",
"commitlint-gitlab-ci": "^0.0.4",
"husky": "^7.0.2",
"semantic-release": "^17.4.7",
"semantic-release-dotnet": "^1.0.0",
"semantic-release-nuget": "^1.1.0"
}
}

20
release.config.js Normal file
View File

@ -0,0 +1,20 @@
module.exports = {
branches: ["main"],
message: "chore(release): v${nextRelease.version}\n\n${nextRelease.notes}",
plugins: [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/npm",
"semantic-release-dotnet",
[
"semantic-release-nuget",
{
packArguments: ["--include-symbols", "--include-source"],
pushFiles: ["src/*/bin/Debug/*.nupkg"],
},
],
"@semantic-release/changelog",
"@semantic-release/git",
"@semantic-release/gitlab",
],
};

14
src/Directory.Build.props Normal file
View File

@ -0,0 +1,14 @@
<Project>
<PropertyGroup>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Authors>Dylan Moonfire</Authors>
<Company>Moonfire Games</Company>
<RepositoryUrl>https://gitlab.com/mfgames-cil/nitride-cil</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<PackageTags>nitride</PackageTags>
<PackageProjectUrl>https://gitlab.com/mfgames-cil/nitride-cil</PackageProjectUrl>
<PackageLicense>MIT</PackageLicense>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Gallium;
using Ical.Net.CalendarComponents;
using Ical.Net.DataTypes;
using Ical.Net.Serialization;
using Nitride.Contents;
using Nitride.Temporal;
using NodaTime;
using Zio;
namespace Nitride.Calendar
{
/// <summary>
/// Creates an iCalendar file from all the entities passed into the method
/// that have a NodaTime.Instant component. This will write both past and
/// future events.
/// </summary>
[WithProperties]
public partial class CreateCalender : NitrideOperationBase
{
private readonly NitrideClock clock;
public CreateCalender(NitrideClock clock)
{
this.clock = clock;
}
/// <summary>
/// Gets or sets a callback to get the summary of the event representing
/// the entity.
/// </summary>
public Func<Entity, string>? GetEventSummary { get; set; }
/// <summary>
/// Gets or sets a callback to get the optional URL of an event for
/// the entity.
/// </summary>
public Func<Entity, Uri?>? GetEventUrl { get; set; }
/// <summary>
/// Gets or sets the file system path for the resulting calendar.
/// </summary>
public UPath? Path { get; set; }
/// <inheritdoc />
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
{
this.CheckNotNull(x => x.Path);
this.CheckNotNull(x => x.GetEventSummary);
IEnumerable<Entity> output = input
.ForEntities<Instant>(this.CreateCalendarEntity);
return output;
}
private IEnumerable<Entity> CreateCalendarEntity(
IEnumerable<Entity> entities)
{
// Create the calendar in the same time zone as the rest of the system.
var calendar = new Ical.Net.Calendar();
calendar.TimeZones.Add(new VTimeZone(this.clock.DateTimeZone.Id));
// Go through the events and add all of them.
List<Entity> input = entities.ToList();
IEnumerable<CalendarEvent> events =
input.Select(this.CreateCalendarEvent);
calendar.Events.AddRange(events);
// Create the iCalendar file.
var serializer = new CalendarSerializer();
string serializedCalendar = serializer.SerializeToString(calendar);
// Create the calendar entity and populate everything.
Entity calendarEntity = new Entity()
.Set(IsCalendar.Instance)
.Set(this.Path!.Value)
.SetTextContent(serializedCalendar);
// Return the results along with the new calendar.
return input.Union(new[] { calendarEntity });
}
private CalendarEvent CreateCalendarEvent(Entity entity)
{
var instant = entity.Get<Instant>();
var when = this.clock.ToDateTime(instant);
string summary = this.GetEventSummary!(entity);
Uri? url = this.GetEventUrl?.Invoke(entity);
var calendarEvent = new CalendarEvent
{
Summary = summary,
Start = new CalDateTime(when),
Url = url,
};
return calendarEvent;
}
}
}

View File

@ -0,0 +1,10 @@
namespace Nitride.Calendar
{
/// <summary>
/// A marker component for identifying an entity that represents a calendar.
/// </summary>
public class IsCalendar
{
public static IsCalendar Instance { get; } = new();
}
}

View File

@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<Description>An extension to Nitride static site generator to generate iCalendar files.</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Nitride.IO\Nitride.IO.csproj" />
<ProjectReference Include="..\Nitride.Temporal\Nitride.Temporal.csproj" />
<ProjectReference Include="..\Nitride\Nitride.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Gallium" Version="1.0.2" />
<PackageReference Include="Ical.Net" Version="4.2.0" />
<PackageReference Include="NodaTime" Version="3.0.5" />
<PackageReference Include="Zio" Version="0.12.0" />
</ItemGroup>
<!-- Include the source generator -->
<PropertyGroup>
<EmitCompilerGeneratedFiles>True</EmitCompilerGeneratedFiles>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Nitride.Generators\Nitride.Generators.csproj">
<OutputItemType>Analyzer</OutputItemType>
<ReferenceOutputAssembly>False</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>
</Project>

View File

@ -0,0 +1,16 @@
using Autofac;
using Nitride.Temporal;
namespace Nitride.Calendar
{
public static class NitrideCalendarBuilderExtensions
{
public static NitrideBuilder UseCalendar(this NitrideBuilder builder)
{
return builder
.UseTemporal()
.ConfigureContainer(
x => x.RegisterModule<NitrideCalendarModule>());
}
}
}

View File

@ -0,0 +1,6 @@
namespace Nitride.Calendar
{
public class NitrideCalendarModule : NitrideModuleBase
{
}
}

View File

@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Gallium;
using Nitride.Contents;
using Nitride.Feeds.Structure;
using NodaTime;
using Serilog;
using Zio;
namespace Nitride.Feeds
{
/// <summary>
/// Creates various feeds from the given input.
/// </summary>
[WithProperties]
public partial class CreateAtomFeeds : NitrideOperationBase
{
private readonly ILogger logger;
public CreateAtomFeeds(ILogger logger)
{
this.logger = logger;
this.GetAlternateMimeType = _ => "text/html";
}
/// <summary>
/// Gets or sets the base URL for all the links.
/// </summary>
public string? BaseUrl { get; set; }
/// <summary>
/// Gets or sets the alternate MIME type.
/// </summary>
public Func<Entity, string> GetAlternateMimeType { get; set; }
/// <summary>
/// Gets or sets the alternate URL associated with the feed.
/// </summary>
public Func<Entity, Uri>? GetAlternateUrl { get; set; }
/// <summary>
/// Gets or sets the callback to get the author for the feed.
/// </summary>
public Func<Entity, AtomAuthor>? GetAuthor { get; set; }
/// <summary>
/// Gets or sets the callback to get the entries associated with the
/// feed.
/// </summary>
public Func<Entity, IEnumerable<AtomEntry>>? GetEntries { get; set; }
/// <summary>
/// Gets or sets the identifier (typically a URL) of the feed.
/// </summary>
public Func<Entity, string>? GetId { get; set; }
/// <summary>
/// Gets or sets the callback to get the path of the generated feed.
/// </summary>
public Func<Entity, UPath>? GetPath { get; set; }
/// <summary>
/// Gets or sets the rights (license) of the feed.
/// </summary>
public Func<Entity, string>? GetRights { get; set; }
/// <summary>
/// A callback that gets the title of the feed from the given entity.
/// </summary>
public Func<Entity, string>? GetTitle { get; set; }
/// <summary>
/// Gets or sets the updated timestamp for the feed.
/// </summary>
public Func<Entity, Instant>? GetUpdated { get; set; }
/// <summary>
/// Gets or sets the URL associated with the feed.
/// </summary>
public Func<Entity, Uri>? GetUrl { get; set; }
/// <inheritdoc />
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
{
this.CheckNotNull(x => this.GetTitle);
this.CheckNotNull(x => this.GetPath);
this.CheckNotNull(x => this.GetEntries);
return input.SelectMany(this.CreateEntityFeed);
}
private IEnumerable<Entity> CreateEntityFeed(Entity entity)
{
// Create the top-level feed. All the nullable callbacks were
// verified in the function that calls this.
var feed = new AtomFeed()
.WithTitle(this.GetTitle?.Invoke(entity))
.WithId(this.GetId?.Invoke(entity))
.WithRights(this.GetRights?.Invoke(entity))
.WithUpdated(this.GetUpdated?.Invoke(entity))
.WithUrl(this.GetUrl?.Invoke(entity))
.WithAlternateUrl(this.GetAlternateUrl?.Invoke(entity))
.WithAlternateMimeType(this.GetAlternateMimeType.Invoke(entity))
.WithAuthor(this.GetAuthor?.Invoke(entity))
.ToXElement();
// Go through all the items inside the feed and add them.
foreach (var entry in this.GetEntries!(entity))
{
feed.Add(entry.ToXElement());
}
// Create the feed entity and return both objects.
Entity feedEntity = new Entity()
.Set(IsFeed.Instance)
.Set(this.GetPath!(entity))
.SetTextContent(feed + "\n");
return new[] { entity, feedEntity };
}
}
}

View File

@ -0,0 +1,15 @@
namespace Nitride.Feeds
{
/// <summary>
/// A marker component that indicates this entity has a feed associated with
/// it.
/// </summary>
public class HasFeed
{
public HasFeed()
{
}
public static HasFeed Instance { get; } = new();
}
}

View File

@ -0,0 +1,14 @@
namespace Nitride.Feeds
{
/// <summary>
/// A marker component that indicates this page is a feed.
/// </summary>
public class IsFeed
{
public IsFeed()
{
}
public static IsFeed Instance { get; } = new();
}
}

View File

@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<Description>An extension to Nitride static site generator to generate Atom feeds.</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Nitride.IO\Nitride.IO.csproj" />
<ProjectReference Include="..\Nitride.Temporal\Nitride.Temporal.csproj" />
<ProjectReference Include="..\Nitride\Nitride.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Gallium" Version="1.0.2" />
<PackageReference Include="NodaTime" Version="3.0.5" />
<PackageReference Include="Zio" Version="0.12.0" />
</ItemGroup>
<!-- Include the source generator -->
<PropertyGroup>
<EmitCompilerGeneratedFiles>True</EmitCompilerGeneratedFiles>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Nitride.Generators\Nitride.Generators.csproj">
<OutputItemType>Analyzer</OutputItemType>
<ReferenceOutputAssembly>False</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>
</Project>

View File

@ -0,0 +1,16 @@
using Autofac;
using Nitride.Temporal;
namespace Nitride.Feeds
{
public static class NitrideFeedsBuilderExtensions
{
public static NitrideBuilder UseFeeds(this NitrideBuilder builder)
{
return builder
.UseTemporal()
.ConfigureContainer(
x => x.RegisterModule<NitrideFeedsModule>());
}
}
}

View File

@ -0,0 +1,6 @@
namespace Nitride.Feeds
{
public class NitrideFeedsModule : NitrideModuleBase
{
}
}

View File

@ -0,0 +1,41 @@
using System.Xml.Linq;
namespace Nitride.Feeds.Structure
{
/// <summary>
/// The type-safe structure for an author element.
/// </summary>
[WithProperties]
public partial class AtomAuthor
{
/// <summary>
/// Gets or sets the name of the author.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Creates an XML element out of the feed along with all items inside
/// the feed.
/// </summary>
/// <returns></returns>
public XElement? ToXElement()
{
if (this.Name == null)
{
return null;
}
var author = new XElement(XmlConstants.AtomNamespace + "author");
if (!string.IsNullOrEmpty(this.Name))
{
author.Add(
new XElement(
XmlConstants.AtomNamespace + "name",
new XText(this.Name)));
}
return author;
}
}
}

View File

@ -0,0 +1,57 @@
using System;
using System.Xml.Linq;
namespace Nitride.Feeds.Structure
{
/// <summary>
/// The type-safe structure for a entry's category element.
/// </summary>
[WithProperties]
public partial class AtomCategory
{
/// <summary>
/// Gets or sets the label associated with the category.
/// </summary>
public string? Label { get; set; }
/// <summary>
/// Gets or sets the scheme associated with the category.
/// </summary>
public Uri? Scheme { get; set; }
/// <summary>
/// Gets or sets the term of the category.
/// </summary>
public string? Term { get; set; }
/// <summary>
/// Creates an XML element out of the feed along with all items inside
/// the feed.
/// </summary>
/// <returns></returns>
public XElement ToXElement()
{
if (this.Term == null)
{
throw new NullReferenceException(
"Category term cannot be null.");
}
var elem = new XElement(
XmlConstants.AtomNamespace + "category",
new XAttribute("term", this.Term));
if (this.Scheme != null)
{
elem.Add(new XAttribute("scheme", this.Scheme.ToString()));
}
if (!string.IsNullOrEmpty(this.Label))
{
elem.Add(new XAttribute("label", this.Label));
}
return elem;
}
}
}

View File

@ -0,0 +1,121 @@
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using NodaTime;
using static Nitride.Feeds.Structure.XmlConstants;
namespace Nitride.Feeds.Structure
{
/// <summary>
/// The type-safe structure for an entry in the Atom feed.
/// </summary>
[WithProperties]
public partial class AtomEntry
{
/// <summary>
/// Gets or sets the author for the feed.
/// </summary>
public AtomAuthor? Author { get; set; }
/// <summary>
/// Gets or sets the categories associated with this entry.
/// </summary>
public IEnumerable<AtomCategory>? Categories { get; set; }
/// <summary>
/// Gets or sets the content of the entry.
/// </summary>
public string? Content { get; set; }
/// <summary>
/// Gets or sets the type of content (text, html) of the content.
/// </summary>
public string ContentType { get; set; } = "html";
/// <summary>
/// Gets or sets the ID of the feed.
/// </summary>
public string? Id { get; set; }
/// <summary>
/// Gets or sets the summary of the entry.
/// </summary>
public string? Summary { get; set; }
/// <summary>
/// Gets or sets the type of content (text, html) of the summary.
/// </summary>
public string SummaryType { get; set; } = "html";
/// <summary>
/// Gets or sets the title of the Feed.
/// </summary>
public string? Title { get; set; }
/// <summary>
/// Gets or sets the timestamp that the feed was updated.
/// </summary>
public Instant? Updated { get; set; }
/// <summary>
/// Gets or sets the URL associated with this feed.
/// </summary>
public Uri? Url { get; set; }
/// <summary>
/// Creates an XML element out of the feed along with all items inside
/// the feed.
/// </summary>
/// <returns></returns>
public XElement? ToXElement()
{
var elem = new XElement(AtomNamespace + "entry");
AtomHelper.AddIfSet(elem, "title", this.Title);
if (this.Url != null)
{
elem.Add(
new XElement(
AtomNamespace + "link",
new XAttribute("rel", "alternate"),
new XAttribute("href", this.Url.ToString())));
}
AtomHelper.AddIfSet(
elem,
"updated",
this.Updated?.ToString("g", null));
AtomHelper.AddIfSet(elem, "id", this.Id);
AtomHelper.AddIfSet(elem, this.Author?.ToXElement());
if (this.Categories != null)
{
foreach (var category in this.Categories)
{
elem.Add(category.ToXElement());
}
}
if (!string.IsNullOrWhiteSpace(this.Summary))
{
elem.Add(
new XElement(
AtomNamespace + "summary",
new XAttribute("type", this.SummaryType),
new XText(this.Summary)));
}
if (!string.IsNullOrWhiteSpace(this.Content))
{
elem.Add(
new XElement(
AtomNamespace + "content",
new XAttribute("type", this.ContentType),
new XText(this.Content)));
}
return elem;
}
}
}

View File

@ -0,0 +1,104 @@
using System;
using System.Xml.Linq;
using NodaTime;
using static Nitride.Feeds.Structure.XmlConstants;
namespace Nitride.Feeds.Structure
{
/// <summary>
/// The type-safe structure of the top-level feed.
/// </summary>
[WithProperties]
public partial class AtomFeed
{
/// <summary>
/// Gets or sets the MIME type for the alternate URL.
/// </summary>
public string AlternateMimeType { get; set; } = "text/html";
/// <summary>
/// Gets or sets the alternate URL for this feed.
/// </summary>
public Uri? AlternateUrl { get; set; }
/// <summary>
/// Gets or sets the author for the feed.
/// </summary>
public AtomAuthor? Author { get; set; }
/// <summary>
/// Gets or sets the ID of the feed.
/// </summary>
public string? Id { get; set; }
/// <summary>
/// Gets or sets the rights (license) of the feed.
/// </summary>
public string? Rights { get; set; }
/// <summary>
/// Gets or sets the title of the Feed.
/// </summary>
public string? Title { get; set; }
/// <summary>
/// Gets or sets the timestamp that the feed was updated.
/// </summary>
public Instant? Updated { get; set; }
/// <summary>
/// Gets or sets the URL associated with this feed.
/// </summary>
public Uri? Url { get; set; }
/// <summary>
/// Creates an XML element out of the feed along with all items inside
/// the feed.
/// </summary>
/// <returns></returns>
public XElement ToXElement()
{
var elem = new XElement(AtomNamespace + "feed");
if (!string.IsNullOrWhiteSpace(this.Title))
{
elem.Add(
new XElement(
AtomNamespace + "title",
new XAttribute("type", "text"),
new XAttribute(XNamespace.Xml + "lang", "en"),
new XText(this.Title)));
}
if (this.Url != null)
{
elem.Add(
new XElement(
AtomNamespace + "link",
new XAttribute("type", "application/atom+xml"),
new XAttribute("href", this.Url.ToString()),
new XAttribute("rel", "self")));
}
if (this.AlternateUrl != null)
{
elem.Add(
new XElement(
AtomNamespace + "link",
new XAttribute("type", this.AlternateMimeType),
new XAttribute("href", this.AlternateUrl.ToString()),
new XAttribute("rel", "alternate")));
}
AtomHelper.AddIfSet(
elem,
"updated",
this.Updated?.ToString("g", null));
AtomHelper.AddIfSet(elem, "id", this.Id);
AtomHelper.AddIfSet(elem, this.Author?.ToXElement());
AtomHelper.AddIfSet(elem, "rights", this.Rights);
return elem;
}
}
}

View File

@ -0,0 +1,29 @@
using System.Xml.Linq;
namespace Nitride.Feeds.Structure
{
/// <summary>
/// Helper methods for working with XML elements.
/// </summary>
public static class AtomHelper
{
public static void AddIfSet(XElement root, XElement? elem)
{
if (elem != null)
{
root.Add(elem);
}
}
public static void AddIfSet(XElement elem, string name, string? text)
{
if (!string.IsNullOrWhiteSpace(text))
{
elem.Add(
new XElement(
XmlConstants.AtomNamespace + name,
new XText(text)));
}
}
}
}

View File

@ -0,0 +1,22 @@
using System.Xml.Linq;
namespace Nitride.Feeds.Structure
{
/// <summary>
/// Common constants used while generating feeds.
/// </summary>
public static class XmlConstants
{
/// <summary>
/// The XML namespace for Atom feeds.
/// </summary>
public static readonly XNamespace AtomNamespace =
"http://www.w3.org/2005/Atom";
/// <summary>
/// The XML namespace for media.
/// </summary>
public static readonly XNamespace MediaNamespace =
"http://search.yahoo.com/mrss/";
}
}

View File

@ -0,0 +1,11 @@
namespace Nitride.Gemtext
{
/// <summary>
/// A marker component for indicating that an entity is Gemtext, the format
/// for text files using the Gemini protocol.
/// </summary>
public class IsGemtext
{
public static IsGemtext Instance { get; } = new IsGemtext();
}
}

View File

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<Description>An extension to Nitride static site generator to generate Gemtext output.</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Nitride\Nitride.csproj"/>
</ItemGroup>
<!-- Include the source generator -->
<PropertyGroup>
<EmitCompilerGeneratedFiles>True</EmitCompilerGeneratedFiles>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Nitride.Generators\Nitride.Generators.csproj">
<OutputItemType>Analyzer</OutputItemType>
<ReferenceOutputAssembly>False</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>
</Project>

View File

@ -0,0 +1,14 @@
using Autofac;
namespace Nitride.Gemtext
{
public static class NitrideGemtextBuilderExtensions
{
public static NitrideBuilder UseGemtext(this NitrideBuilder builder)
{
return builder
.ConfigureContainer(
x => x.RegisterModule<NitrideGemtextModule>());
}
}
}

View File

@ -0,0 +1,6 @@
namespace Nitride.Gemtext
{
public class NitrideGemtextModule : NitrideModuleBase
{
}
}

View File

@ -0,0 +1,159 @@
using Microsoft.CodeAnalysis;
namespace Nitride.Generators
{
/// <summary>
/// Various wrappers around the diagnostics to simplify generation.
/// </summary>
public static class CodeAnalysisExtensions
{
/// <summary>
/// Creates an error message to break the build while generating code.
/// </summary>
/// <param name="context">The context that contains the diagnostic.</param>
/// <param name="messageCode">The normalized message code.</param>
/// <param name="format">The string format for the message.</param>
/// <param name="parameters">The optional parameters.</param>
public static void Error(
this GeneratorExecutionContext context,
MessageCode messageCode,
string format,
params object[] parameters)
{
Error(context, messageCode, null, format, parameters);
}
/// <summary>
/// Creates an error message to break the build while generating code.
/// </summary>
/// <param name="context">The context that contains the diagnostic.</param>
/// <param name="messageCode">The normalized message code.</param>
/// <param name="location">The optional location for the message.</param>
/// <param name="format">The string format for the message.</param>
/// <param name="parameters">The optional parameters.</param>
public static void Error(
this GeneratorExecutionContext context,
MessageCode messageCode,
Location? location,
string format,
params object[] parameters)
{
context.Message(
messageCode,
location,
DiagnosticSeverity.Error,
format,
parameters);
}
/// <summary>
/// Creates an informational message to break the build while generating code.
/// </summary>
/// <param name="context">The context that contains the diagnostic.</param>
/// <param name="messageCode">The normalized message code.</param>
/// <param name="format">The string format for the message.</param>
/// <param name="parameters">The optional parameters.</param>
public static void Information(
this GeneratorExecutionContext context,
MessageCode messageCode,
string format,
params object[] parameters)
{
Information(context, messageCode, null, format, parameters);
}
/// <summary>
/// Creates an informational message to break the build while generating code.
/// </summary>
/// <param name="context">The context that contains the diagnostic.</param>
/// <param name="messageCode">The normalized message code.</param>
/// <param name="location">The optional location for the message.</param>
/// <param name="format">The string format for the message.</param>
/// <param name="parameters">The optional parameters.</param>
public static void Information(
this GeneratorExecutionContext context,
MessageCode messageCode,
Location? location,
string format,
params object[] parameters)
{
context.Message(
messageCode,
location,
DiagnosticSeverity.Info,
format,
parameters);
}
/// <summary>
/// Creates a warning message to break the build while generating code.
/// </summary>
/// <param name="context">The context that contains the diagnostic.</param>
/// <param name="messageCode">The normalized message code.</param>
/// <param name="format">The string format for the message.</param>
/// <param name="parameters">The optional parameters.</param>
public static void Warning(
this GeneratorExecutionContext context,
MessageCode messageCode,
string format,
params object[] parameters)
{
Warning(context, messageCode, null, format, parameters);
}
/// <summary>
/// Creates a warning message to break the build while generating code.
/// </summary>
/// <param name="context">The context that contains the diagnostic.</param>
/// <param name="messageCode">The normalized message code.</param>
/// <param name="location">The optional location for the message.</param>
/// <param name="format">The string format for the message.</param>
/// <param name="parameters">The optional parameters.</param>
public static void Warning(
this GeneratorExecutionContext context,
MessageCode messageCode,
Location? location,
string format,
params object[] parameters)
{
context.Message(
messageCode,
location,
DiagnosticSeverity.Warning,
format,
parameters);
}
/// <summary>
/// Creates a message to break the build while generating code.
/// </summary>
/// <param name="context">The context that contains the diagnostic.</param>
/// <param name="messageCode">The normalized message code.</param>
/// <param name="location">The optional location for the message.</param>
/// <param name="format">The string format for the message.</param>
/// <param name="parameters">The optional parameters.</param>
/// <param name="severity">The severity of the message.</param>
private static void Message(
this GeneratorExecutionContext context,
MessageCode messageCode,
Location? location,
DiagnosticSeverity severity,
string format,
params object[] parameters)
{
context.ReportDiagnostic(
Diagnostic.Create(
"GN" + ((int)messageCode).ToString("D4"),
"Nitride",
string.Format(format, parameters),
severity,
severity,
true,
severity is DiagnosticSeverity.Warning
or DiagnosticSeverity.Info
? 4
: 0,
location: location));
}
}
}

View File

@ -0,0 +1,10 @@
namespace Nitride.Generators
{
/// <summary>
/// All the error messages produced by the generators.
/// </summary>
public enum MessageCode
{
Debug = 1,
}
}

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<Description>Common source generators for Nitride.</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Gallium" Version="1.0.2" />
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="3.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.8.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,209 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace Nitride.Generators
{
/// <summary>
/// Implements a source generator that creates Set* methods for the various
/// properties that also returns the same object for purposes of chaining
/// together calls.
/// </summary>
[Generator]
public class WithPropertySourceGenerator : ISourceGenerator
{
public void Execute(GeneratorExecutionContext context)
{
// Get the generator infrastructure will create a receiver and
// populate it we can retrieve the populated instance via the
// context.
var syntaxReceiver = (SyntaxReceiver?)context.SyntaxReceiver;
if (syntaxReceiver == null)
{
return;
}
// Report any messages.
foreach (var message in syntaxReceiver.Messages)
{
context.Information(
MessageCode.Debug,
Location.Create(
"Temporary.g.cs",
TextSpan.FromBounds(0, 0),
new LinePositionSpan(
new LinePosition(0, 0),
new LinePosition(0, 0))),
"Generating additional identifier code: {0}",
message);
}
// If we didn't find anything, then there is nothing to do.
if (syntaxReceiver.ClassesToAugment.Count == 0)
{
return;
}
// Go through each one.
foreach (var cds in syntaxReceiver.ClassesToAugment)
{
this.GenerateClassFile(context, cds);
}
}
public void Initialize(GeneratorInitializationContext context)
{
// Register a factory that can create our custom syntax receiver
context.RegisterForSyntaxNotifications(
() => new SyntaxReceiver(context));
}
private void GenerateClassFile(
GeneratorExecutionContext context,
ClassDeclarationSyntax cds)
{
// Get the namespace.
var nds = (NamespaceDeclarationSyntax?)cds.Parent;
if (nds == null)
{
return;
}
string? ns = nds.Name.ToString();
// Create the partial class.
StringBuilder buffer = new();
buffer.AppendLine("#nullable enable");
// Copy the using statements from the file.
if (nds.Parent is CompilationUnitSyntax cus)
{
foreach (var uds in cus.Usings)
{
buffer.AppendLine(uds.ToString());
}
buffer.AppendLine();
}
// Create the namespace.
buffer.AppendLine($"namespace {ns}");
buffer.AppendLine("{");
buffer.AppendLine($" public partial class {cds.Identifier}");
buffer.AppendLine(" {");
// Go through the properties of the namespace.
IEnumerable<PropertyDeclarationSyntax> properties = cds.Members
.Where(m => m.Kind() == SyntaxKind.PropertyDeclaration)
.Cast<PropertyDeclarationSyntax>();
bool first = true;
foreach (PropertyDeclarationSyntax pds in properties)
{
// See if we have a setter.
bool found =
pds.AccessorList?.Accessors
.Any(x => x.Keyword.ToString() == "set")
?? false;
if (!found)
{
continue;
}
// If we aren't first, then add a newline before it.
if (first)
{
first = false;
}
else
{
buffer.AppendLine();
}
// Write some documentation.
buffer.AppendLine(" /// <summary>");
buffer.AppendLine(
string.Format(
" /// Sets the {0} value and returns the operation for chaining.",
pds.Identifier));
buffer.AppendLine(" /// </summary>");
// We have the components for writing out a setter.
buffer.AppendLine(
string.Format(
" public {0} With{1}({2} value)",
cds.Identifier,
pds.Identifier,
pds.Type));
buffer.AppendLine(" {");
buffer.AppendLine(
string.Format(
" this.{0} = value;",
pds.Identifier));
buffer.AppendLine(" return this;");
buffer.AppendLine(" }");
}
// Finish up the class.
buffer.AppendLine(" }");
buffer.AppendLine("}");
// Create the source text and write out the file.
SourceText sourceText = SourceText.From(
buffer.ToString(),
Encoding.UTF8);
context.AddSource(cds.Identifier + ".Generated.cs", sourceText);
}
private class SyntaxReceiver : ISyntaxReceiver
{
private readonly GeneratorInitializationContext context;
public SyntaxReceiver(GeneratorInitializationContext context)
{
this.context = context;
this.ClassesToAugment = new List<ClassDeclarationSyntax>();
this.Messages = new List<string>();
}
public List<ClassDeclarationSyntax> ClassesToAugment { get; }
public List<string> Messages { get; }
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
// We only care about class declarations.
if (syntaxNode is not ClassDeclarationSyntax cds)
{
return;
}
// See if the class has our set properties attribute.
bool found = cds
.AttributeLists
.AsEnumerable()
.SelectMany(x => x.Attributes)
.Select(x => x.Name.ToString())
.Any(
x => x switch
{
"WithProperties" => true,
"WithPropertiesAttribute" => true,
_ => false,
});
if (found)
{
this.ClassesToAugment.Add(cds);
}
}
}
}
}

View File

@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using Gallium;
using HandlebarsDotNet;
using Nitride.Contents;
namespace Nitride.Handlebars
{
/// <summary>
/// An operation that uses the content to create a template that is then
/// applied against the content and metadata and then replaces the content
/// of that entity.
/// </summary>
public class ApplyContentHandlebarsTemplate<TModel> : NitrideOperationBase
{
private readonly HandlebarsTemplateCache cache;
public ApplyContentHandlebarsTemplate(HandlebarsTemplateCache cache)
{
this.cache = cache;
}
/// <summary>
/// Gets or sets the callback used to create a model from a given
/// entity. This allows for the website to customize what information is
/// being passed to the template.
/// </summary>
public Func<Entity, TModel>? CreateModelCallback { get; set; }
/// <inheritdoc />
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
{
this.CheckNotNull(x => x.CreateModelCallback);
return input
.ForEachEntity<HasHandlebarsTemplate, ITextContent>(this.Apply);
}
/// <summary>
/// Sets the callback for the template and returns the operation to
/// chain operations.
/// </summary>
/// <param name="callback">The callback to set.</param>
/// <returns>The ApplyContentHandlebarsTemplate to chain requests.</returns>
public ApplyContentHandlebarsTemplate<TModel> WithCreateModelCallback(
Func<Entity, TModel>? callback)
{
this.CreateModelCallback = callback;
return this;
}
private Entity Apply(
Entity entity,
HasHandlebarsTemplate _,
ITextContent content)
{
// Create the model using the callback.
TModel model = this.CreateModelCallback!(entity);
// Create a template from the contents.
string text = content.GetText();
HandlebarsTemplate<object, object> template =
this.cache.GetLiteralTemplate(text);
// Render the template and create a new entity with the updated
// text.
string result = template(model!);
return entity
.Remove<HasHandlebarsTemplate>()
.SetTextContent(new StringTextContent(result));
}
}
}

View File

@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using Gallium;
using HandlebarsDotNet;
using Nitride.Contents;
namespace Nitride.Handlebars
{
/// <summary>
/// An operation that applies a common or shared template on the content of
/// a document that includes theme or styling information.
/// </summary>
public class ApplyStyleHandlebarsTemplate<TModel> : NitrideOperationBase
{
private readonly HandlebarsTemplateCache cache;
public ApplyStyleHandlebarsTemplate(HandlebarsTemplateCache cache)
{
this.cache = cache;
}
/// <summary>
/// Gets or sets the callback used to create a model from a given
/// entity. This allows for the website to customize what information is
/// being passed to the template.
/// </summary>
public Func<Entity, TModel>? CreateModelCallback { get; set; }
/// <summary>
/// Gets or sets the callback used to determine which template to use
/// for a given entity. This lets one have a per-page template change.
/// </summary>
public Func<Entity, string>? GetTemplateName { get; set; }
public IHandlebars? Handlebars { get; set; }
/// <inheritdoc />
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
{
// Make sure we have sane data.
this.CheckNotNull(x => x.CreateModelCallback);
this.CheckNotNull(x => x.Handlebars);
this.CheckNotNull(x => x.GetTemplateName);
// Create and set up the Handlebars.
return input
.ForEachEntity<ITextContent>(this.Apply);
}
/// <summary>
/// Sets the callback for the template and returns the operation to
/// chain operations.
/// </summary>
/// <param name="callback">The callback to set.</param>
/// <returns>The ApplyContentHandlebarsTemplate to chain requests.</returns>
public ApplyStyleHandlebarsTemplate<TModel> WithCreateModelCallback(
Func<Entity, TModel>? callback)
{
this.CreateModelCallback = callback;
return this;
}
public ApplyStyleHandlebarsTemplate<TModel> WithGetTemplateName(
Func<Entity, string>? callback)
{
this.GetTemplateName = callback;
return this;
}
public ApplyStyleHandlebarsTemplate<TModel> WithHandlebars(
IHandlebars? handlebars)
{
this.Handlebars = handlebars;
return this;
}
private Entity Apply(Entity entity, ITextContent content)
{
TModel model = this.CreateModelCallback!(entity);
string name = this.GetTemplateName!(entity);
HandlebarsTemplate<object, object> template =
this.cache.GetNamedTemplate(name);
string result = template(model!);
return entity.SetTextContent(result);
}
}
}

View File

@ -0,0 +1,70 @@
using System.Collections.Generic;
using HandlebarsDotNet;
using Open.Threading;
namespace Nitride.Handlebars
{
/// <summary>
/// Implements a cache for templates to prevent compiling the same template
/// more than once.
/// </summary>
public class HandlebarsTemplateCache
{
private readonly IHandlebars handlebars;
private readonly ModificationSynchronizer locker;
private readonly Dictionary<string, HandlebarsTemplate<object, object>>
templates;
public HandlebarsTemplateCache(IHandlebars handlebars)
{
this.handlebars = handlebars;
this.locker = new ModificationSynchronizer();
this.templates =
new Dictionary<string, HandlebarsTemplate<object, object>>();
}
/// <summary>
/// Caches the template by name based on the contents of the disk. It
/// does it once in a multi-threaded manner to ensure it is only cached
/// once in memory.
/// </summary>
/// <param name="literal">The string that contains the template.</param>
/// <returns></returns>
public HandlebarsTemplate<object, object> GetLiteralTemplate(
string literal)
{
// Start with a read lock to see if we've already compiled it.
this.locker.Modifying(
() => !this.templates.ContainsKey(literal),
() =>
{
HandlebarsTemplate<object, object> template =
this.handlebars
!.Compile(literal);
this.templates[literal] = template;
return true;
});
return this.locker.Reading(() => this.templates[literal]);
}
/// <summary>
/// Caches the template by name based on the contents of the disk. It
/// does it once in a multi-threaded manner to ensure it is only cached
/// once in memory.
/// </summary>
/// <param name="templateName"></param>
/// <returns></returns>
public HandlebarsTemplate<object, object> GetNamedTemplate(
string templateName)
{
string template = $"{{{{> {templateName}}}}}";
return this.GetLiteralTemplate(template);
}
}
}

View File

@ -0,0 +1,11 @@
namespace Nitride.Handlebars
{
/// <summary>
/// A marker component that indicates that a given file with text component
/// has a Handlebars template in it.
/// </summary>
public class HasHandlebarsTemplate
{
public static HasHandlebarsTemplate Instance { get; } = new();
}
}

View File

@ -0,0 +1,32 @@
using System.Collections.Generic;
using Gallium;
using Nitride.Contents;
namespace Nitride.Handlebars
{
/// <summary>
/// An operation that discovers which text files have a Handlebars template
/// inside them.
/// </summary>
public class IdentifyHandlebars : INitrideOperation
{
/// <inheritdoc />
public IEnumerable<Entity> Run(IEnumerable<Entity> input)
{
return input
.ForEachEntity<ITextContent>(this.ScanContent);
}
private Entity ScanContent(Entity entity, ITextContent content)
{
string text = content.GetText();
if (text.Contains("{{") && text.Contains("}}"))
{
return entity.Set(HasHandlebarsTemplate.Instance);
}
return entity;
}
}
}

View File

@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<Description>An extension to Nitride static site generator to style output with Handlebars.</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Autofac" Version="6.2.0" />
<PackageReference Include="Gallium" Version="1.0.2" />
<PackageReference Include="Handlebars.Net" Version="2.0.8" />
<PackageReference Include="NodaTime.Testing" Version="3.0.5" />
<PackageReference Include="Open.Threading" Version="1.6.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Nitride\Nitride.csproj" />
</ItemGroup>
<!-- Include the source generator -->
<PropertyGroup>
<EmitCompilerGeneratedFiles>True</EmitCompilerGeneratedFiles>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Nitride.Generators\Nitride.Generators.csproj">
<OutputItemType>Analyzer</OutputItemType>
<ReferenceOutputAssembly>False</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>
</Project>

View File

@ -0,0 +1,14 @@
using Autofac;
namespace Nitride.Handlebars
{
public static class NitrideHandlebarsBuilderExtensions
{
public static NitrideBuilder UseHandlebars(this NitrideBuilder builder)
{
return builder
.ConfigureContainer(
x => x.RegisterModule<NitrideHandlebarsModule>());
}
}
}

View File

@ -0,0 +1,22 @@
using Autofac;
namespace Nitride.Handlebars
{
public class NitrideHandlebarsModule : NitrideModuleBase
{
/// <inheritdoc />
protected override void Register(ContainerBuilder builder)
{
builder
.RegisterType<HandlebarsTemplateCache>()
.AsSelf()
.SingleInstance();
builder
.RegisterGeneric(typeof(ApplyContentHandlebarsTemplate<>))
.As(typeof(ApplyContentHandlebarsTemplate<>));
builder
.RegisterGeneric(typeof(ApplyStyleHandlebarsTemplate<>))
.As(typeof(ApplyStyleHandlebarsTemplate<>));
}
}
}

View File

@ -0,0 +1,26 @@
Nitride.Handlebars
==================
This is a collection
of [Handlebars](https://github.com/Handlebars-Net/Handlebars.Net)
operations that work with Nitride. There are two ways of using Handelbars in
this package.
## IHandlebars
This library does *not* configure or register `IHandlebars` but requires it to
be injected into the DI container (Autofac) along with any templates or inlines
loaded.
## Styling Templates
The first is to use it to apply a Handlebars theme to a page or text content.
This would include adding links to the CSS, any javascript, generating menus,
and common navigation elements.
## Content Templates
The second is used to apply it to the Handlebars inside the text content. This
is the page-specific content that needs to be resolved. A good example of this
might be creating a "last five posts" page or embedding some metadata from the
YAML header into the page.

View File

@ -0,0 +1,31 @@
using System.Collections.Generic;
using System.Net;
using Gallium;
using Nitride.Contents;
namespace Nitride.Html
{
/// <summary>
/// Converts the text input that uses HTML entities and turns them into
/// Unicode variations.
/// </summary>
public class HtmlEntitiesToUnicode : NitrideOperationBase
{
/// <inheritdoc />
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
{
return input
.ForEachEntity<ITextContent>(this.ResolveHtmlEntities);
}
private Entity ResolveHtmlEntities(
Entity entity,
ITextContent content)
{
string text = content.GetText();
string resolved = WebUtility.HtmlDecode(text);
return entity.SetTextContent(resolved);
}
}
}

View File

@ -0,0 +1,10 @@
namespace Nitride.Html
{
/// <summary>
/// A marker component that indicates that the entity is an HTML file.
/// </summary>
public class IsHtml
{
public static IsHtml Instance { get; } = new();
}
}

View File

@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<Description>An extension to Nitride static site generator to generate HTML output.</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Nitride\Nitride.csproj" />
</ItemGroup>
<!-- Include the source generator -->
<PropertyGroup>
<EmitCompilerGeneratedFiles>True</EmitCompilerGeneratedFiles>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Nitride.Generators\Nitride.Generators.csproj">
<OutputItemType>Analyzer</OutputItemType>
<ReferenceOutputAssembly>False</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Gallium" Version="1.0.2" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,14 @@
using Autofac;
namespace Nitride.Html
{
public static class NitrideHtmlBuilderExtensions
{
public static NitrideBuilder UseHtml(this NitrideBuilder builder)
{
return builder
.ConfigureContainer(
x => x.RegisterModule<NitrideHtmlModule>());
}
}
}

View File

@ -0,0 +1,6 @@
namespace Nitride.Html
{
public class NitrideHtmlModule : NitrideModuleBase
{
}
}

View File

@ -0,0 +1,48 @@
using System.Linq;
using Autofac;
using Nitride.IO.Contents;
using Nitride.IO.Paths;
using Xunit;
using Xunit.Abstractions;
using Zio;
using Zio.FileSystems;
namespace Nitride.IO.Tests
{
public class AddPathPrefixTests : NitrideIOTestsBase
{
private readonly MemoryFileSystem fileSystem;
public AddPathPrefixTests(ITestOutputHelper output)
: base(output)
{
this.fileSystem = new MemoryFileSystem();
this.fileSystem.CreateFile("/b1.txt");
this.fileSystem.CreateFile("/c1.md");
}
[Fact]
public void PrefixAllFiles()
{
// Set up the operation.
var readFiles = this.Container.Resolve<ReadFiles.Factory>();
var op = new AddPathPrefix("/prefix");
// Read and replace the paths.
IOrderedEnumerable<string> output = readFiles(this.fileSystem)
.Read()
.Run(op)
.Select(x => x.Get<UPath>().ToString())
.OrderBy(x => x);
// Verify the results.
Assert.Equal(
new[]
{
"/prefix/b1.txt",
"/prefix/c1.md",
},
output);
}
}
}

View File

@ -0,0 +1,80 @@
using System.Linq;
using Autofac;
using Nitride.IO.Contents;
using Nitride.IO.Paths;
using Xunit;
using Xunit.Abstractions;
using Zio;
using Zio.FileSystems;
namespace Nitride.IO.Tests
{
public class MoveToIndexPathsTests : NitrideIOTestsBase
{
private readonly MemoryFileSystem fileSystem;
public MoveToIndexPathsTests(ITestOutputHelper output)
: base(output)
{
this.fileSystem = new MemoryFileSystem();
this.fileSystem.CreateDirectory("/c1");
this.fileSystem.CreateFile("/a1");
this.fileSystem.CreateFile("/b1.txt");
this.fileSystem.CreateFile("/c1/index.md");
this.fileSystem.CreateFile("/d1.html");
}
[Fact]
public void MoveAllFiles()
{
// Set up the operation.
var readFiles = this.Container.Resolve<ReadFiles.Factory>();
var op = new MoveToIndexPaths();
// Read and replace the paths.
IOrderedEnumerable<string> output = readFiles(this.fileSystem)
.Read()
.Run(op)
.Select(x => x.Get<UPath>().ToString())
.OrderBy(x => x);
// Verify the results.
Assert.Equal(
new[]
{
"/a1",
"/b1.txt",
"/c1/index.md",
"/d1/index.html",
},
output);
}
[Fact]
public void OverrideCanMoveCallback()
{
// Set up the operation.
var readFiles = this.Container.Resolve<ReadFiles.Factory>();
MoveToIndexPaths? op = new MoveToIndexPaths()
.WithCanMoveCallback((path) => path.ToString().Contains("a1"));
// Read and replace the paths.
IOrderedEnumerable<string> output = readFiles(this.fileSystem)
.Read()
.Run(op)
.Select(x => x.Get<UPath>().ToString())
.OrderBy(x => x);
// Verify the results.
Assert.Equal(
new[]
{
"/a1/index",
"/b1.txt",
"/c1/index.md",
"/d1.html",
},
output);
}
}
}

View File

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Nitride.IO\Nitride.IO.csproj" />
<ProjectReference Include="..\Nitride.Tests\Nitride.Tests.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Gallium" Version="1.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="JunitXml.TestLogger" Version="2.1.81" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Zio" Version="0.12.0" />
<PackageReference Include="coverlet.collector" Version="3.0.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@ -0,0 +1,21 @@
using Autofac;
using Nitride.Tests;
using Xunit.Abstractions;
namespace Nitride.IO.Tests
{
public abstract class NitrideIOTestsBase : NitrideTestsBase
{
protected NitrideIOTestsBase(ITestOutputHelper output)
: base(output)
{
}
/// <inheritdoc />
protected override void ConfigureContainer(ContainerBuilder builder)
{
base.ConfigureContainer(builder);
builder.RegisterModule<NitrideIOModule>();
}
}
}

View File

@ -0,0 +1,97 @@
using System.Linq;
using Autofac;
using Nitride.IO.Contents;
using Xunit;
using Xunit.Abstractions;
using Zio;
using Zio.FileSystems;
namespace Nitride.IO.Tests
{
/// <summary>
/// Tests the functionality of the ReadFiles().
/// </summary>
public class ReadFilesTests : NitrideIOTestsBase
{
private readonly MemoryFileSystem fileSystem;
public ReadFilesTests(ITestOutputHelper output)
: base(output)
{
this.fileSystem = new MemoryFileSystem();
this.fileSystem.CreateDirectory("/b1");
this.fileSystem.CreateDirectory("/c1");
this.fileSystem.CreateDirectory("/c1/c2");
this.fileSystem.CreateDirectory("/d1");
this.fileSystem.WriteAllText("/a.txt", "File A");
this.fileSystem.WriteAllText("/b1/b.md", "File B");
this.fileSystem.WriteAllText("/c1/c.txt", "File C");
this.fileSystem.WriteAllText("/c1/c2/e.md", "File C");
}
[Fact]
public void ReadAllFiles()
{
// Set up the operation.
var factory = this.Container.Resolve<ReadFiles.Factory>();
ReadFiles op = factory(this.fileSystem);
// Verify the paths.
IOrderedEnumerable<string> paths = op.Read()
.Select(x => x.Get<UPath>())
.Select(x => (string)x)
.OrderBy(x => x);
Assert.Equal(
new[]
{
"/a.txt",
"/b1/b.md",
"/c1/c.txt",
"/c1/c2/e.md",
},
paths);
}
[Fact]
public void ReadGlob()
{
// Set up the operation.
var factory = this.Container.Resolve<ReadFiles.Factory>();
ReadFiles op = factory(this.fileSystem);
// Verify the paths.
IOrderedEnumerable<string> paths = op.Read("/*.txt")
.Select(x => (string)x.Get<UPath>())
.OrderBy(x => x);
Assert.Equal(
new[]
{
"/a.txt",
},
paths);
}
[Fact]
public void ReadGlobWithSubdirectories()
{
// Set up the operation.
var factory = this.Container.Resolve<ReadFiles.Factory>();
ReadFiles op = factory(this.fileSystem);
// Verify the paths.
IOrderedEnumerable<string> paths = op.Read("**/*.txt")
.Select(x => (string)x.Get<UPath>())
.OrderBy(x => x);
Assert.Equal(
new[]
{
"/a.txt",
"/c1/c.txt",
},
paths);
}
}
}

View File

@ -0,0 +1,50 @@
using System.Linq;
using Autofac;
using Nitride.IO.Contents;
using Nitride.IO.Paths;
using Xunit;
using Xunit.Abstractions;
using Zio;
using Zio.FileSystems;
namespace Nitride.IO.Tests
{
public class RemovePathPrefixTests : NitrideIOTestsBase
{
private readonly MemoryFileSystem fileSystem;
public RemovePathPrefixTests(ITestOutputHelper output)
: base(output)
{
this.fileSystem = new MemoryFileSystem();
this.fileSystem.CreateDirectory("/a");
this.fileSystem.CreateDirectory("/a/a");
this.fileSystem.CreateFile("/a/b1.txt");
this.fileSystem.CreateFile("/a/a/c1.md");
}
[Fact]
public void PrefixAllFiles()
{
// Set up the operation.
var readFiles = this.Container.Resolve<ReadFiles.Factory>();
var op = new RemovePathPrefix("/a");
// Read and replace the paths.
IOrderedEnumerable<string> output = readFiles(this.fileSystem)
.Read()
.Run(op)
.Select(x => x.Get<UPath>().ToString())
.OrderBy(x => x);
// Verify the results.
Assert.Equal(
new[]
{
"/a/c1.md",
"/b1.txt",
},
output);
}
}
}

View File

@ -0,0 +1,63 @@
using System.Linq;
using Autofac;
using Nitride.Contents;
using Nitride.IO.Contents;
using Xunit;
using Xunit.Abstractions;
using Zio;
using Zio.FileSystems;
namespace Nitride.IO.Tests
{
/// <summary>
/// Tests the functionality of the WriteFiles().
/// </summary>
public class WriteFilesTest : NitrideIOTestsBase
{
private readonly MemoryFileSystem fileSystem;
public WriteFilesTest(ITestOutputHelper output)
: base(output)
{
this.fileSystem = new MemoryFileSystem();
this.fileSystem.CreateDirectory("/b1");
this.fileSystem.CreateDirectory("/c1");
this.fileSystem.CreateDirectory("/c1/c2");
this.fileSystem.CreateDirectory("/d1");
this.fileSystem.WriteAllText("/a.txt", "File A");
this.fileSystem.WriteAllText("/b1/b.md", "File B");
this.fileSystem.WriteAllText("/c1/c2/e.md", "File E");
}
[Fact]
public void WriteAllFiles()
{
// Set up the operation.
var output = new MemoryFileSystem();
var readFiles = this.Container.Resolve<ReadFiles.Factory>();
var factory = this.Container.Resolve<WriteFiles.Factory>();
WriteFiles op = factory(output);
// Read and write out the files. We switch one of the files to be
// text content to make sure that works too.
readFiles(this.fileSystem)
.Read()
.Select(
x => x.Get<UPath>() == "/b1/b.md"
? x.SetTextContent(
((ITextContentConvertable)x.GetBinaryContent())
.ToTextContent())
: x)
.Run(op);
// Verify the results.
Assert.True(output.FileExists("/a.txt"));
Assert.True(output.FileExists("/b1/b.md"));
Assert.True(output.FileExists("/c1/c2/e.md"));
Assert.Equal("File A", output.ReadAllText("/a.txt"));
Assert.Equal("File B", output.ReadAllText("/b1/b.md"));
Assert.Equal("File E", output.ReadAllText("/c1/c2/e.md"));
}
}
}

View File

@ -0,0 +1,35 @@
using System.IO;
using Nitride.Contents;
using Zio;
namespace Nitride.IO.Contents
{
/// <summary>
/// Contains a wrapper around a file entry to retrieve the binary data.
/// </summary>
public class FileEntryBinaryContent
: IBinaryContent, ITextContentConvertable
{
private readonly FileEntry entry;
public FileEntryBinaryContent(FileEntry entry)
{
this.entry = entry;
}
/// <inheritdoc />
public Stream GetStream()
{
return this.entry.Open(
FileMode.Open,
FileAccess.Read,
FileShare.Read);
}
/// <inheritdoc />
public ITextContent ToTextContent()
{
return new FileEntryTextContent(this.entry);
}
}
}

View File

@ -0,0 +1,38 @@
using System.IO;
using System.Text;
using Nitride.Contents;
using Zio;
namespace Nitride.IO.Contents
{
/// <summary>
/// Contains a wrapper around a file entry to retrieve text data.
/// </summary>
public class FileEntryTextContent
: ITextContent, IBinaryContentConvertable
{
private readonly FileEntry entry;
public FileEntryTextContent(FileEntry entry)
{
this.entry = entry;
}
/// <inheritdoc />
public TextReader GetReader()
{
return new StreamReader(
this.entry.Open(
FileMode.Open,
FileAccess.Read,
FileShare.Read),
Encoding.UTF8);
}
/// <inheritdoc />
public IBinaryContent ToBinaryContent()
{
return new FileEntryBinaryContent(this.entry);
}
}
}

View File

@ -0,0 +1,89 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using DotNet.Globbing;
using Gallium;
using Nitride.Contents;
using Zio;
namespace Nitride.IO.Contents
{
/// <summary>
/// A module that reads files from the file system and wraps them in an
/// entity with the following components: UPath, IContent.
/// </summary>
public class ReadFiles : FileSystemOperation
{
public ReadFiles(IFileSystem fileSystem)
: base(fileSystem)
{
}
/// <summary>
/// Primary method for creating a read file.
/// </summary>
public delegate ReadFiles Factory(IFileSystem fileSystem);
/// <summary>
/// Reads all files from the file system and returns them.
/// </summary>
/// <returns>A populated collection of entities.</returns>
public IEnumerable<Entity> Read(
UPath path = new(),
string searchPattern = "*",
SearchOption search = SearchOption.AllDirectories)
{
// Normalize the path.
path = path == new UPath() ? "/" : path;
// Search for the file and wrap the results.
IEnumerable<FileEntry> files = this.FileSystem
.EnumerateFileEntries(path, searchPattern, search);
IEnumerable<Entity> entities = files.Select(this.ToEntity);
return entities;
}
/// <summary>
/// Reads all files from the file system and returns them.
/// </summary>
/// <returns>A populated collection of entities.</returns>
public IEnumerable<Entity> Read(string glob)
{
Glob parsed = Glob.Parse(glob);
return this.Read(parsed);
}
/// <summary>
/// Reads all files from the file system, filtering them out by the
/// minimatch pattern (as defined by DotNet.Blob).
/// </summary>
/// <returns>A populated collection of entities.</returns>
public IEnumerable<Entity> Read(Glob glob)
{
IEnumerable<FileEntry> files = this.FileSystem
.EnumerateFileEntries("/", "*.*", SearchOption.AllDirectories)
.Where(x => glob.IsMatch(x.Path.ToString()));
IEnumerable<Entity> entities = files.Select(this.ToEntity);
return entities;
}
/// <summary>
/// Creates an entity with the standard components for all Zio-based
/// files. This attaches the file's path relative to the file system
/// and a way of accessing the content from the file system.
/// </summary>
/// <param name="file">The Zio file entry.</param>
/// <returns>An Entity with appropriate content.</returns>
private Entity ToEntity(FileEntry file)
{
Entity entity = new Entity()
.Set(file.Path)
.SetBinaryContent(new FileEntryBinaryContent(file));
return entity;
}
}
}

View File

@ -0,0 +1,220 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Gallium;
using Nitride.Contents;
using Serilog;
using Zio;
namespace Nitride.IO.Contents
{
/// <summary>
/// An operation that writes out entities to a file system.
/// </summary>
[WithProperties]
public partial class WriteFiles : FileSystemOperation, INitrideOperation
{
private readonly ILogger logger;
private Dictionary<Type, Func<IContent, Stream>> factories;
public WriteFiles(
IFileSystem fileSystem,
ILogger logger)
: base(fileSystem)
{
this.logger = logger.ForContext<WriteFiles>();
this.factories = new Dictionary<Type, Func<IContent, Stream>>
{
[typeof(IBinaryContent)] = GetBinaryStream,
[typeof(ITextContent)] = this.GetTextStream,
};
}
/// <summary>
/// Primary method for creating a write files operation.
/// </summary>
public delegate WriteFiles Factory(IFileSystem fileSystem);
public Dictionary<Type, Func<IContent, Stream>> StreamFactories
{
get => this.factories;
set => this.factories =
value ?? throw new ArgumentNullException(nameof(value));
}
/// <summary>
/// Gets or sets the encoding to force any text output.
/// </summary>
public Encoding? TextEncoding { get; set; }
/// <summary>
/// Writes out all the files to the given file system using the paths
/// currently stored in the `UPath` component. Only files that have
/// a path and a registered writer will be written.
/// </summary>
/// <param name="entities">The entities to parse.</param>
/// <returns>The same list of entities without changes.</returns>
public IEnumerable<Entity> Run(IEnumerable<Entity> entities)
{
// We need the `ToList()` here, otherwise it doesn't work.
IEnumerable<Entity> results = entities
.ForEachEntity<UPath>(this.Process)
.ToList();
return results;
}
private static Stream GetBinaryStream(IContent content)
{
return ((IBinaryContent)content).GetStream();
}
private Stream GetTextStream(IContent content)
{
// See if we can convert the stream first. If that is the case, then
// we don't have to load it entirely in memory.
if (content is IBinaryContentConvertable convertable)
{
return convertable.ToBinaryContent().GetStream();
}
// We have the load the text into memory and convert it.
var textContent = (ITextContent)content;
string text = textContent.GetReader().ReadToEnd();
var stream = new MemoryStream();
var writer = new StreamWriter(
stream,
this.TextEncoding ?? Encoding.UTF8);
writer.Write(text);
writer.Flush();
stream.Position = 0;
return stream;
}
/// <summary>
/// Internal method for writing out the entity. This handles the
/// registered writers to allow for multiple `IContent` types being
/// written out automatically.
/// </summary>
/// <param name="entity">The entity to write out.</param>
/// <param name="path">The path of the entity.</param>
/// <returns>The entity passed in.</returns>
private Entity Process(Entity entity, UPath path)
{
// See if we have any content. If we don't, then there is nothing
// to do.
if (!entity.HasContent())
{
return entity;
}
// First see if we have a factory for the exact type of content.
IContent content = entity.GetContent();
if (this.factories.TryGetValue(
content.GetType(),
out var getStream))
{
Stream stream = getStream(content);
return this.Process(entity, path, stream);
}
// If we have an easy conversion, then use that so we don't have to
// walk up the tree looking for one we do have.
if (content is IBinaryContentConvertable binaryConvertable
&& this.factories.TryGetValue(
typeof(IBinaryContent),
out var binaryContent))
{
Stream stream =
binaryContent(binaryConvertable.ToBinaryContent());
return this.Process(entity, path, stream);
}
if (content is ITextContentConvertable textConvertable
&& this.factories.TryGetValue(
typeof(ITextContent),
out var textContent))
{
Stream stream = textContent(textConvertable.ToTextContent());
return this.Process(entity, path, stream);
}
// For everything else, we have to find a content that we have a
// registered type for by walking up the inheritance tree and
// finding the right type.
List<Type> types = new() { content.GetType() };
while (types.Count > 0)
{
// Check to see if we have any of these types.
Func<IContent, Stream>? found = types
.Select(
x => this.factories.TryGetValue(x, out var factory)
? factory
: null)
.FirstOrDefault(x => x != null);
if (found != null)
{
Stream stream = found(content);
return this.Process(entity, path, stream);
}
// We didn't find one, so add all the parent types and try
// again with the new list.
types = types
.SelectMany(
x => new[] { x.BaseType }
.Union(x.GetInterfaces()))
.Where(x => x != null)
.Select(x => x!)
.ToList();
}
// If we got this far, we never found a content to handle.
throw new InvalidOperationException(
"Cannot write out entity "
+ path
+ " because cannot determine how to get a stream out content type "
+ content.GetType().FullName
+ ". To resolve, register a function to this.StreamFactories.");
}
/// <summary>
/// Writes out a stream to the given path in the file system.
/// </summary>
/// <param name="entity">The entity being written out.</param>
/// <param name="path">The path to write out, directories will be created.</param>
/// <param name="stream">The stream to write out.</param>
/// <returns>The entity passed in.</returns>
private Entity Process(Entity entity, UPath path, Stream stream)
{
// Make sure we have the directory structure.
UPath directory = path.GetDirectory();
if (directory != "/")
{
this.FileSystem.CreateDirectory(directory);
}
// Write out the file.
using Stream fileStream = this.FileSystem.CreateFile(path);
stream.CopyTo(fileStream);
stream.Close();
// Return the entity because we've written out the files.
return entity;
}
}
}

View File

@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using Gallium;
using Serilog;
using Zio;
namespace Nitride.IO.Directories
{
/// <summary>
/// A Nitride operation that removes the contents of a directory but not
/// the directory itself. This is used because some tools don't handle
/// when the root directory is removed.
/// This will create the top-level directory if it doesn't exist.
/// </summary>
[WithProperties]
public partial class ClearDirectory : FileSystemOperation, INitrideOperation
{
private readonly ILogger logger;
public ClearDirectory(
ILogger logger,
IFileSystem fileSystem)
: base(fileSystem)
{
this.logger = logger.ForContext<ClearDirectory>();
}
/// <summary>
/// Gets or sets the path of the directory to clear.
/// </summary>
public UPath? Path { get; set; }
public IEnumerable<Entity> Run()
{
return this.Run(new List<Entity>());
}
/// <inheritdoc />
public IEnumerable<Entity> Run(IEnumerable<Entity> input)
{
// This really isn't an input-type of operation, but it can fit
// inside one to keep a pattern.
if (!this.Path.HasValue)
{
throw new InvalidOperationException(
nameof(ClearDirectory)
+ "cannot be used without setting the path either by the"
+ "factory method, the constructor, the property, or "
+ "SetPath method.");
}
// See if the directory exists. If it doesn't, then we make it.
UPath path = this.Path.Value;
if (!this.FileSystem.DirectoryExists(path))
{
this.logger.Information(
"Creating the directory {Path}",
path);
this.FileSystem.CreateDirectory(path);
}
// Clear out the contents.
IEnumerable<UPath> files = this.FileSystem.EnumerateFiles(path);
IEnumerable<UPath> directories =
this.FileSystem.EnumerateDirectories(path);
foreach (UPath file in files)
{
this.FileSystem.DeleteFile(file);
}
foreach (UPath directory in directories)
{
this.FileSystem.DeleteDirectory(directory, true);
}
// Just pass the input on.
return input;
}
}
}

View File

@ -0,0 +1,14 @@
using Zio;
namespace Nitride.IO
{
public abstract class FileSystemOperation
{
protected FileSystemOperation(IFileSystem fileSystem)
{
this.FileSystem = fileSystem;
}
protected IFileSystem FileSystem { get; }
}
}

View File

@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<Nullable>enable</Nullable>
<RootNamespace>Nitride.IO</RootNamespace>
</PropertyGroup>
<PropertyGroup>
<Description>An extension to Nitride static site generator to read and write files.</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Autofac" Version="6.2.0" />
<PackageReference Include="DotNet.Glob" Version="3.1.2" />
<PackageReference Include="Gallium" Version="1.0.2" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Zio" Version="0.12.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Nitride\Nitride.csproj" />
</ItemGroup>
<!-- Include the source generator -->
<PropertyGroup>
<EmitCompilerGeneratedFiles>True</EmitCompilerGeneratedFiles>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Nitride.Generators\Nitride.Generators.csproj">
<OutputItemType>Analyzer</OutputItemType>
<ReferenceOutputAssembly>False</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>
</Project>

View File

@ -0,0 +1,14 @@
using Autofac;
namespace Nitride.IO
{
public static class NitrideIOBuilderExtensions
{
public static NitrideBuilder UseIO(this NitrideBuilder builder)
{
return builder
.ConfigureContainer(
x => x.RegisterModule<NitrideIOModule>());
}
}
}

View File

@ -0,0 +1,6 @@
namespace Nitride.IO
{
public class NitrideIOModule : NitrideModuleBase
{
}
}

View File

@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using Gallium;
using Zio;
namespace Nitride.IO.Paths
{
[WithProperties]
public partial class AddPathPrefix : INitrideOperation
{
public AddPathPrefix()
{
}
public AddPathPrefix(UPath pathPrefix)
{
this.PathPrefix = pathPrefix;
}
/// <summary>
/// Gets or sets the prefix for the path operations.
/// </summary>
public UPath? PathPrefix { get; set; }
public IEnumerable<Entity> Run(IEnumerable<Entity> input)
{
if (this.PathPrefix == null)
{
throw new InvalidOperationException(
nameof(AddPathPrefix)
+ ".Prefix was not set "
+ "(via constructor, property, or SetPrefix) before "
+ "Run() was called.");
}
ReplacePaths replacePaths = new ReplacePaths()
.WithReplacement(this.RunReplacement);
return replacePaths.Run(input);
}
private UPath RunReplacement(Entity _, UPath path)
{
string innerRelativePath = path.ToString().TrimStart('/');
return this.PathPrefix!.Value / innerRelativePath;
}
}
}

View File

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using Gallium;
using Zio;
namespace Nitride.IO.Paths
{
/// <summary>
/// Changes the extension of the paths given.
/// </summary>
[WithProperties]
public partial class ChangePathExtension : INitrideOperation
{
public ChangePathExtension()
{
}
public ChangePathExtension(string extension)
{
this.Extension = extension;
}
/// <summary>
/// Gets or sets the prefix for the path operations.
/// </summary>
public string? Extension { get; set; }
public IEnumerable<Entity> Run(IEnumerable<Entity> input)
{
if (this.Extension == null)
{
throw new InvalidOperationException(
nameof(AddPathPrefix)
+ ".Extension was not set "
+ "(via constructor, property, or SetExtension) before "
+ "Run() was called.");
}
ReplacePaths replacePaths = new ReplacePaths()
.WithReplacement(this.RunReplacement);
return replacePaths.Run(input);
}
private UPath RunReplacement(Entity _, UPath path)
{
return path.ChangeExtension(this.Extension!);
}
}
}

View File

@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using Gallium;
using Zio;
namespace Nitride.IO.Paths
{
/// <summary>
/// Moves various files to indexes of a direction with the base filename.
/// </summary>
[WithProperties]
public partial class MoveToIndexPaths : INitrideOperation
{
public MoveToIndexPaths()
{
this.CanMoveCallback = DefaultCanMoveCallback;
}
/// <summary>
/// Gets or sets the callback to determine if the file should be moved.
/// This will not be called if the file is already an index.
/// </summary>
public Func<UPath, bool>? CanMoveCallback { get; set; }
/// <summary>
/// Default implement of the operation moves .html, .htm, .md, and
/// .markdown files into their indexes.
/// </summary>
/// <param name="path"></param>
/// <returns>True if the file should move, otherwise false.</returns>
public static bool DefaultCanMoveCallback(UPath path)
{
return path.GetExtensionWithDot() switch
{
".htm" => true,
".html" => true,
".md" => true,
".markdown" => true,
_ => false,
};
}
public IEnumerable<Entity> Run(IEnumerable<Entity> input)
{
if (this.CanMoveCallback == null)
{
throw new InvalidOperationException(
nameof(MoveToIndexPaths)
+ ".CanMoveCallback was not set "
+ "(via constructor, property, or SetCanMoveCallback) before "
+ "Run() was called.");
}
ReplacePaths replacePaths = new ReplacePaths()
.WithReplacement(this.RunReplacement);
return replacePaths.Run(input);
}
private UPath RunReplacement(Entity _, UPath path)
{
// See if we are already an index. If that is true, then we don't
// have to move any further.
string? nameWithoutExtension = path.GetNameWithoutExtension();
if (nameWithoutExtension is null or "index")
{
return path;
}
// See if the path should be moved. If it can't, then just stop
// processing.
if (!this.CanMoveCallback!.Invoke(path))
{
return path;
}
// Move the file to an index.
UPath parent = path.GetDirectory();
string? extension = path.GetExtensionWithDot();
string index = "index" + extension;
return parent / nameWithoutExtension / index;
}
}
}

View File

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using Gallium;
using Zio;
namespace Nitride.IO.Paths
{
/// <summary>
/// An operation that removes a path prefix from the input.
/// </summary>
[WithProperties]
public partial class RemovePathPrefix : INitrideOperation
{
public RemovePathPrefix()
{
}
public RemovePathPrefix(UPath pathPrefix)
{
this.PathPrefix = pathPrefix;
}
/// <summary>
/// Gets or sets the prefix for the path operations.
/// </summary>
public UPath? PathPrefix { get; set; }
public IEnumerable<Entity> Run(IEnumerable<Entity> input)
{
if (this.PathPrefix == null)
{
throw new InvalidOperationException(
nameof(RemovePathPrefix)
+ ".Prefix was not set "
+ "(via constructor, property, or SetPrefix) before "
+ "Replace() was called.");
}
ReplacePaths replacePaths = new ReplacePaths()
.WithReplacement(this.RunReplacement);
return replacePaths.Run(input);
}
private UPath RunReplacement(Entity _, UPath path)
{
string normalized = path.ToString();
string prefix = this.PathPrefix.ToString()!;
if (normalized.StartsWith(prefix))
{
return (UPath)path.ToString().Substring(prefix.Length);
}
return path;
}
}
}

View File

@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using Gallium;
using Zio;
namespace Nitride.IO.Paths
{
/// <summary>
/// A pipeline operation that replaces the UPath of the given entity
/// with the results of a lambda output. Entities without a path component
/// are passed on without touching.
/// </summary>
[WithProperties]
public partial class ReplacePaths : INitrideOperation
{
public ReplacePaths()
{
}
public ReplacePaths(Func<Entity, UPath, UPath> replacement)
{
this.Replacement = replacement;
}
/// <summary>
/// Gets or sets the replacement callback to alter the paths.
/// </summary>
public Func<Entity, UPath, UPath>? Replacement { get; set; }
/// <summary>
/// Performs the replacement on the input streams and outputs the
/// resulting entities. Only entities that have had their paths changed
/// will be updated, the others will be passed on as-is.
/// </summary>
/// <param name="input">The list of input entities.</param>
/// <returns>The output entities.</returns>
public IEnumerable<Entity> Run(IEnumerable<Entity> input)
{
if (this.Replacement == null)
{
throw new InvalidOperationException(
"ReplacePaths.Replacement was not set "
+ "(via constructor, property, or SetReplacement) before "
+ "Replace() was called.");
}
return input
.ForEachEntity<UPath>(
(entity, oldPath) =>
{
UPath newPath = this.Replacement(entity, oldPath);
return newPath != oldPath
? entity.Set(newPath)
: entity;
});
}
}
}

View File

@ -0,0 +1,41 @@
using Zio;
namespace Nitride.IO.Paths
{
/// <summary>
/// Extension methods for the UPath class.
/// </summary>
public static class UPathExtensions
{
/// <summary>
/// Gets the directory path, which excludes the directory at the end
/// of the path.
/// </summary>
/// <param name="path">The path to manipulate.</param>
/// <returns>A normalized path.</returns>
public static string GetDirectoryIndexPath(this UPath path)
{
if (path.GetNameWithoutExtension() == "index")
{
return path.GetDirectory().ToString().TrimEnd('/') + "/";
}
return path.ToString();
}
public static string GetParentDirectoryIndexPath(this UPath path)
{
UPath indexPath = path.GetDirectoryIndexPath();
UPath parent = indexPath.GetDirectory();
if (parent == null!)
{
parent = "/";
}
string parentPath = parent.ToString().TrimEnd('/') + "/";
return parentPath;
}
}
}

31
src/Nitride.IO/README.md Normal file
View File

@ -0,0 +1,31 @@
# Nitride.IO
This assembly contains the primary system for reading and writing from the disk,
along with various processes to manipulate paths. It contains three primary
components:
- File System I/O
- Path Normalization
- Disk-Based Content
## File System I/O
Internally, this assembly uses [Zio](https://github.com/xoofx/zio), an file
system abstraction that also has an in-memory implementation which is ideal for
running unit tests. Zio also provides a way of treating an arbitrary directory
as a root directory so all paths are relative to the "root".
## Path Normalization
Zio also provides `UPath`, a normalization class that handles relative and
absolute paths. Entities that are read from the disk, such as with `ReadFiles`,
will have a `UPath` component with the path from the file. This component is
also used to determine the path when writing out the results.
## Disk-Based Content
This assembly also extends the `Nitride.Contents.IBinaryContent` and
`Nitride.Contents.ITextContent` to have file system based implementations. These
keep track of the original path and file system regardless of changes made to
the `UPath` component.

View File

@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Gallium;
using Serilog;
using Zio;
namespace Nitride.Javascript
{
/// <summary>
/// Installs Yarn in a given directory if it is missing. If the
/// `node_modules` is already there, this step is skipped unless
/// ForceInstall is set to true.
/// </summary>
[WithProperties]
public partial class InstallYarn : INitrideOperation
{
private readonly IFileSystem fileSystem;
private readonly ILogger logger;
public InstallYarn(ILogger logger, IFileSystem fileSystem)
{
this.fileSystem = fileSystem;
this.logger = logger.ForContext<InstallYarn>();
this.Directory = "/";
}
/// <summary>
/// Gets or sets the directory that the install should be run from. This
/// is typically the same file as the package.json and the yarn.lock.
/// </summary>
public UPath? Directory { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the install should be
/// forced.
/// </summary>
public bool ForceInstall { get; set; }
/// <inheritdoc />
public IEnumerable<Entity> Run(IEnumerable<Entity> input)
{
// Make sure we have a sane value.
if (!this.Directory.HasValue)
{
throw new InvalidOperationException(
nameof(InstallYarn)
+ ".Directory was not set before Run() was called.");
}
// Make sure the directory exists in case we are the first.
UPath directory = this.Directory.Value;
if (!this.fileSystem.DirectoryExists(directory))
{
this.logger.Information("Creating directory {Path}", directory);
this.fileSystem.CreateDirectory(directory);
}
// Check to see if the directory exists.
UPath modulesPath = directory / "node_modules";
bool exists = this.fileSystem.DirectoryExists(modulesPath);
if (exists)
{
if (!this.ForceInstall)
{
this.logger.Debug(
"{Path} already exists, skipping",
modulesPath);
return input;
}
this.logger.Information(
"{Path} already exists, forcing installation",
modulesPath);
this.fileSystem.DeleteDirectory(modulesPath, true);
}
// Run Yarn install on the directory. Sadly, Yarn doesn't understand
// a C# abstraction layer, so we need the "real" path to work with
// these operations.
string realPath = this.fileSystem
.ConvertPathToInternal(directory);
// Run the install.
string command = YarnHelper.GetYarnCommand();
var start = new ProcessStartInfo
{
FileName = command,
Arguments = "install",
WorkingDirectory = realPath,
RedirectStandardOutput = true,
};
Process process = Process.Start(start);
if (process == null)
{
throw new InvalidOperationException(
"Cannot start yarn install");
}
process.WaitForExit();
// We are done, so just pass our input on.
return input;
}
}
}

View File

@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<Description>An extension to Nitride static site generator to generate Webpack output.</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Nitride.IO\Nitride.IO.csproj" />
<ProjectReference Include="..\Nitride\Nitride.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Gallium" Version="1.0.2" />
<PackageReference Include="Zio" Version="0.12.0" />
</ItemGroup>
<!-- Include the source generator -->
<PropertyGroup>
<EmitCompilerGeneratedFiles>True</EmitCompilerGeneratedFiles>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Nitride.Generators\Nitride.Generators.csproj">
<OutputItemType>Analyzer</OutputItemType>
<ReferenceOutputAssembly>False</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>
</Project>

View File

@ -0,0 +1,14 @@
using Autofac;
namespace Nitride.Javascript
{
public static class NitrideJavascriptBuilderExtensions
{
public static NitrideBuilder UseJavascript(this NitrideBuilder builder)
{
return builder
.ConfigureContainer(
x => x.RegisterModule<NitrideJavascriptModule>());
}
}
}

View File

@ -0,0 +1,6 @@
namespace Nitride.Javascript
{
public class NitrideJavascriptModule : NitrideModuleBase
{
}
}

View File

@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Gallium;
using Nitride.IO.Contents;
using Nitride.IO.Paths;
using Serilog;
using Zio;
namespace Nitride.Javascript
{
/// <summary>
/// Runs `webpack` in the directory, writing out whatever files need to be
/// written.
/// </summary>
[WithProperties]
public partial class RunWebpack : INitrideOperation
{
private readonly IFileSystem fileSystem;
private readonly ILogger logger;
private readonly ReadFiles readFiles;
public RunWebpack(
ILogger logger,
IFileSystem fileSystem,
ReadFiles readFiles)
{
this.fileSystem = fileSystem;
this.readFiles = readFiles;
this.logger = logger.ForContext<RunWebpack>();
this.WebpackDirectory = "/";
}
/// <summary>
/// Gets or sets the directory that contains the output from the
/// webpack execution.
/// </summary>
public UPath? OutputDirectory { get; set; }
/// <summary>
/// Gets or sets the directory that the install should be run from. This
/// is typically the same file as the webpack.config.js.
/// </summary>
public UPath? WebpackDirectory { get; set; }
/// <inheritdoc />
public IEnumerable<Entity> Run(IEnumerable<Entity> input)
{
// Make sure we have a sane values.
if (!this.WebpackDirectory.HasValue)
{
throw new InvalidOperationException(
nameof(InstallYarn)
+ ".WebpackDirectory was not set before Run() was called.");
}
if (!this.OutputDirectory.HasValue)
{
throw new InvalidOperationException(
nameof(InstallYarn)
+ ".OutputDirectory was not set before Run() was called.");
}
// Make sure the directory exists in case we are the first.
UPath directory = this.WebpackDirectory.Value;
if (!this.fileSystem.DirectoryExists(directory))
{
this.logger.Information("Creating directory {Path}", directory);
this.fileSystem.CreateDirectory(directory);
}
// Sadly, Yarn doesn't understand virtual file systems, so we need
// the "real" path to the directory for this.
string realPath = this.fileSystem
.ConvertPathToInternal(directory);
// Run the install.
this.logger.Debug("Running Webpack via Yarn");
string command = YarnHelper.GetYarnCommand();
var start = new ProcessStartInfo
{
FileName = command,
Arguments = "run webpack",
WorkingDirectory = realPath,
RedirectStandardOutput = true,
};
Process process = Process.Start(start);
if (process == null)
{
throw new InvalidOperationException(
"Cannot start yarn run webpack");
}
process.WaitForExit();
// We are done and all the output will be written into the
// filesystem, so merge it with our input.
this.logger.Debug("Adding Webpack output to the pipeline");
UPath outputDirectory = this.OutputDirectory.Value;
return input
.Union(
this.readFiles
.Read(outputDirectory)
.Run(new RemovePathPrefix(outputDirectory)));
}
}
}

View File

@ -0,0 +1,34 @@
using System;
using System.IO;
namespace Nitride.Javascript
{
/// <summary>
/// A helper class for finding how to run or install Yarn.
/// </summary>
public static class YarnHelper
{
/// <summary>
/// Gets the name of the command to run Yarn.
/// </summary>
/// <returns>The command to run.</returns>
public static string GetYarnCommand()
{
// Figure out how to run Yarn. This is needed because Windows can't
// run the script like the shell can.
string command = "yarn";
string appData = Path.Combine(
Environment.GetFolderPath(
Environment.SpecialFolder.ApplicationData),
"npm",
"yarn.cmd");
if (File.Exists(appData))
{
command = appData;
}
return command;
}
}
}

View File

@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using Gallium;
using Nitride.Contents;
using Zio;
namespace Nitride.Markdown
{
/// <summary>
/// An operation that identifies Markdown files by their common extensions
/// and converts them to text input while also adding the IsMarkdown
/// component to identify them.
/// </summary>
[WithProperties]
public partial class IdentifyMarkdown : INitrideOperation
{
public IdentifyMarkdown()
{
this.IsMarkdownTest = DefaultIsMarkdown;
}
public Func<Entity, UPath, bool> IsMarkdownTest { get; set; }
/// <inheritdoc />
public IEnumerable<Entity> Run(IEnumerable<Entity> input)
{
return input
.ForEachEntity<UPath, ITextContent>(
(entity, path, _) =>
{
// If we aren't a Markdown file, then there is nothing
// we can do about that.
if (!this.IsMarkdownTest(entity, path))
{
return entity;
}
// We are already text, so just mark it as Markdown.
entity = entity
.Set(IsMarkdown.Instance);
return entity;
})
.ForEachEntity<UPath, IBinaryContent>(
(entity, path, binary) =>
{
// If we aren't a Markdown file, then there is nothing
// we can do about that.
if (!this.IsMarkdownTest(entity, path))
{
return entity;
}
// Convert the file as a binary.
if (binary is ITextContentConvertable textConvertable)
{
entity = entity
.SetTextContent(textConvertable.ToTextContent())
.Set(IsMarkdown.Instance);
}
else
{
throw new InvalidOperationException(
"Cannot convert a binary content to a "
+ "text without ITextContentConvertable.");
}
return entity;
});
}
private static bool DefaultIsMarkdown(Entity entity, UPath path)
{
return (path.GetExtensionWithDot() ?? string.Empty)
.ToLowerInvariant() switch
{
".md" => true,
".markdown" => true,
_ => false,
};
}
}
}

View File

@ -0,0 +1,10 @@
namespace Nitride.Markdown
{
/// <summary>
/// A marker class that indicates that the file is a Markdown file.
/// </summary>
public class IsMarkdown
{
public static IsMarkdown Instance { get; } = new();
}
}

View File

@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using Gallium;
using Markdig;
using Nitride.Contents;
namespace Nitride.Markdown
{
public abstract class MarkdownOperationBase : INitrideOperation
{
/// <summary>
/// Gets or sets an additional callback to configure additional features
/// from the baseline Markdown.
/// </summary>
public Action<MarkdownPipelineBuilder>? ConfigureMarkdown { get; set; }
/// <inheritdoc />
public IEnumerable<Entity> Run(IEnumerable<Entity> input)
{
// Create the Markdown pipeline used for formatting.
var builder = new MarkdownPipelineBuilder();
this.ConfigureMarkdown?.Invoke(builder);
MarkdownPipeline options = builder.Build();
// Process the Markdown files (while passing everything on).
return input
.ForEachEntity<IsMarkdown, ITextContent>(
(entity, _, content) => this.Convert(
entity,
content,
options));
}
/// <summary>
/// Converts the Markdown file into HTML.
/// </summary>
/// <param name="entity">The entity to convert.</param>
/// <param name="markdownContent">The content for this entity.</param>
/// <param name="options">The markdown pipeline.</param>
/// <returns>A converted entity.</returns>
protected abstract Entity Convert(
Entity entity,
ITextContent markdownContent,
MarkdownPipeline options);
}
}

View File

@ -0,0 +1,40 @@
using Gallium;
using Markdig;
using MfGames.Markdown.Gemtext;
using Nitride.Contents;
using Nitride.Gemtext;
namespace Nitride.Markdown
{
/// <summary>
/// Converts the input Markdown files into Gemtext using Markdig and
/// MfGames.Markdown.Gemtext. This only processes files with a text input
/// and the IsMarkdown component.
/// </summary>
public class MarkdownToGemtext : MarkdownOperationBase
{
/// <summary>
/// Converts the Markdown file into HTML.
/// </summary>
/// <param name="entity">The entity to convert.</param>
/// <param name="markdownContent">The content for this entity.</param>
/// <param name="options">The markdown pipeline.</param>
/// <returns>A converted entity.</returns>
protected override Entity Convert(
Entity entity,
ITextContent markdownContent,
MarkdownPipeline options)
{
string markdown = markdownContent.GetText();
string gemtext = MarkdownGemtext.ToGemtext(markdown, options);
var content = new StringTextContent(gemtext);
entity = entity
.SetTextContent(content)
.Remove<IsMarkdown>()
.Set(IsGemtext.Instance);
return entity;
}
}
}

View File

@ -0,0 +1,40 @@
using Gallium;
using Markdig;
using Nitride.Contents;
using Nitride.Html;
namespace Nitride.Markdown
{
/// <summary>
/// Converts the input Markdown files into HTML using Markdig. This only
/// processes files with a text input and the IsMarkdown component.
/// </summary>
public class MarkdownToHtml : MarkdownOperationBase
{
/// <summary>
/// Converts the Markdown file into HTML.
/// </summary>
/// <param name="entity">The entity to convert.</param>
/// <param name="markdownContent">The content for this entity.</param>
/// <param name="options">The markdown pipeline.</param>
/// <returns>A converted entity.</returns>
protected override Entity Convert(
Entity entity,
ITextContent markdownContent,
MarkdownPipeline options)
{
// Convert the entity to Html.
string markdown = markdownContent.GetText();
string html = Markdig.Markdown.ToHtml(markdown, options);
var htmlContent = new StringTextContent(html);
entity = entity
.SetTextContent(htmlContent)
.Remove<IsMarkdown>()
.Set(IsHtml.Instance);
// Return the resulting entity.
return entity;
}
}
}

View File

@ -0,0 +1,22 @@
using System;
using Markdig;
namespace Nitride.Markdown
{
/// <summary>
/// Extension methods to handle generics while configuring a Markdown
/// operation.
/// </summary>
public static class MarkdownToOperationBaseExtension
{
public static T WithConfigureMarkdown<T>(
this T operation,
Action<MarkdownPipelineBuilder>? callback)
where T : MarkdownOperationBase
{
operation.ConfigureMarkdown = callback;
return operation;
}
}
}

View File

@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<Description>An extension to Nitride static site generator to render Markdown content.</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Nitride.Gemtext\Nitride.Gemtext.csproj" />
<ProjectReference Include="..\Nitride.Html\Nitride.Html.csproj" />
<ProjectReference Include="..\Nitride\Nitride.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Gallium" Version="1.0.2" />
<PackageReference Include="Markdig" Version="0.25.0" />
<PackageReference Include="MfGames.Markdown.Gemtext" Version="1.0.0" />
<PackageReference Include="Zio" Version="0.12.0" />
</ItemGroup>
<!-- Include the source generator -->
<PropertyGroup>
<EmitCompilerGeneratedFiles>True</EmitCompilerGeneratedFiles>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Nitride.Generators\Nitride.Generators.csproj">
<OutputItemType>Analyzer</OutputItemType>
<ReferenceOutputAssembly>False</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>
</Project>

View File

@ -0,0 +1,14 @@
using Autofac;
namespace Nitride.Markdown
{
public static class NitrideMarkdownBuilderExtensions
{
public static NitrideBuilder UseMarkdown(this NitrideBuilder builder)
{
return builder
.ConfigureContainer(
x => x.RegisterModule<NitrideMarkdownModule>());
}
}
}

View File

@ -0,0 +1,6 @@
namespace Nitride.Markdown
{
public class NitrideMarkdownModule : NitrideModuleBase
{
}
}

View File

@ -0,0 +1,15 @@
namespace Nitride.Slugs
{
/// <summary>
/// An interface that provides slugs for various paths.
/// </summary>
public interface ISlugProvider
{
/// <summary>
/// Converts the given input into a slug.
/// </summary>
/// <param name="input">The input string to normalize.</param>
/// <returns>The resulting slug.</returns>
string ToSlug(string input);
}
}

View File

@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<Description>An extension to Nitride static site generator to generate slugs.</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Slugify.Core" Version="3.0.0"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Nitride\Nitride.csproj"/>
</ItemGroup>
<!-- Include the source generator -->
<PropertyGroup>
<EmitCompilerGeneratedFiles>True</EmitCompilerGeneratedFiles>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Nitride.Generators\Nitride.Generators.csproj">
<OutputItemType>Analyzer</OutputItemType>
<ReferenceOutputAssembly>False</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>
</Project>

View File

@ -0,0 +1,28 @@
using System;
using Autofac;
namespace Nitride.Slugs
{
public static class NitrideSlugsBuilderExtensions
{
public static NitrideBuilder UseSlugs(
this NitrideBuilder builder,
ISlugProvider slugs)
{
if (slugs == null)
{
throw new ArgumentNullException(nameof(slugs));
}
return builder
.ConfigureContainer(
x =>
{
x.RegisterInstance(slugs)
.As<ISlugProvider>()
.SingleInstance();
x.RegisterModule<NitrideSlugsModule>();
});
}
}
}

View File

@ -0,0 +1,8 @@
using Autofac;
namespace Nitride.Slugs
{
public class NitrideSlugsModule : Module
{
}
}

View File

@ -0,0 +1,78 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Slugify;
namespace Nitride.Slugs
{
/// <summary>
/// A default implementation of ISlugProvider.
/// </summary>
public class SimpleSlugProvider : ISlugProvider, IEnumerable<string>
{
private readonly List<(string, string)> replacements;
public SimpleSlugProvider()
{
this.replacements = new List<(string, string)>();
}
public SimpleSlugProvider(IDictionary<string, string> replacements)
: this()
{
foreach (KeyValuePair<string, string> pair in replacements)
{
this.Add(pair.Key, pair.Value);
}
}
/// <summary>
/// Adds a replacement for the resulting slug.
/// </summary>
/// <param name="search">The text to search for.</param>
/// <param name="replace">The replacement string.</param>
public void Add(string search, string replace)
{
this.replacements.Add((search, replace));
}
/// <inheritdoc />
public IEnumerator<string> GetEnumerator()
{
return this.replacements
.Select(x => x.Item1)
.GetEnumerator();
}
/// <inheritdoc />
public virtual string ToSlug(string input)
{
// If we have null or whitespace, we have a problem.
if (string.IsNullOrWhiteSpace(input))
{
throw new ArgumentException(
"Cannot have a blank or null input",
nameof(input));
}
// Create a slug..
var helper = new SlugHelper();
string output = helper.GenerateSlug(input);
// Perform any additional replacements.
foreach ((string search, string replace) in this.replacements)
{
output = output.Replace(search, replace);
}
return output;
}
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}
}

View File

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
namespace Nitride.Slugs
{
/// <summary>
/// Extends the slug provider to strip out accented characters for
/// a normalized form.
/// </summary>
public class UnicodeNormalizingSlugProvider : SimpleSlugProvider
{
/// <inheritdoc />
public UnicodeNormalizingSlugProvider()
{
}
/// <inheritdoc />
public UnicodeNormalizingSlugProvider(
IDictionary<string, string> replacements)
: base(replacements)
{
}
/// <inheritdoc />
public override string ToSlug(string input)
{
// If we have null or whitespace, we have a problem.
if (string.IsNullOrWhiteSpace(input))
{
throw new ArgumentException(
"Cannot have a blank or null input",
nameof(input));
}
// Normalize the Unicode objects.
// Strip out the accents. This is a cheesy way of doing so.
char[] chars = input
.Normalize(NormalizationForm.FormD)
.Where(this.IsNonSpacingMark)
.ToArray();
string normalized = new string(chars)
.Normalize(NormalizationForm.FormC);
// Return the base implementation.
return base.ToSlug(normalized);
}
private bool IsNonSpacingMark(char c)
{
UnicodeCategory category = CharUnicodeInfo.GetUnicodeCategory(c);
return category != UnicodeCategory.NonSpacingMark;
}
}
}

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