From d31902723eaf0d9826e4cad3321a22a7718f54a1 Mon Sep 17 00:00:00 2001 From: Coen van den Munckhof Date: Sat, 14 Oct 2023 13:39:32 +0200 Subject: [PATCH 001/430] WIP --- RepoM.sln | 94 +++ RepoM.sln.DotSettings | 1 + docs_new/file.generated.md | 92 +++ docs_new/repository.generated.md | 62 ++ docs_new/strings.generated.md | 494 +++++++++++++++ .../KalkDescriptor.cs | 30 + .../KalkParamDescriptor.cs | 21 + .../KalkToGenerate.cs | 64 ++ src/RepoM.ActionMenu.CodeGen/Program.cs | 560 ++++++++++++++++++ .../RepoM.ActionMenu.CodeGen.csproj | 28 + .../Templates/Docs.scriban-cs | 58 ++ .../Templates/Module.scriban-cs | 58 ++ src/RepoM.ActionMenu.Core/Misc/FastStack.cs | 84 +++ .../Misc/FixedTemplateParser.cs | 29 + .../Misc/ITemplateParser.cs | 10 + .../Misc/TemplateExtensions.cs | 18 + .../Model/ActionMenuGenerationContext.cs | 244 ++++++++ .../Model/DisposableContextScriptObject.cs | 78 +++ .../Model/Env/EnvScriptObject.cs | 97 +++ .../Model/Env/EnvSetScriptObject.cs | 117 ++++ .../Model/Functions/FileFunctions.cs | 79 +++ .../Model/Functions/FileFunctionsKalk.cs | 76 +++ .../Model/Functions/RepositoryFunctions.cs | 53 ++ .../Model/Functions/StringModule.cs | 387 ++++++++++++ .../Model/IActionMenuDeserializer.cs | 10 + src/RepoM.ActionMenu.Core/Model/Lexers.cs | 19 + .../Model/RepoMScriptObject.cs | 131 ++++ .../Model/ScribanModuleWithFunctions.cs | 180 ++++++ .../Model/TemplateEvaluatorExtensions.cs | 73 +++ .../PublicApi/Factory.cs | 28 + .../IUserInterfaceActionMenuFactory.cs | 13 + .../RepoM.ActionMenu.Core.csproj | 18 + .../RepoMCodeGen.generated.cs | 72 +++ .../UserInterfaceActionMenuFactory.cs | 70 +++ ...SubActionsUserInterfaceRepositoryAction.cs | 36 ++ .../UserInterfaceRepositoryAction.cs | 15 + .../Yaml/Model/ActionMenus/ActionMenu.cs | 8 + .../ActionAssociateFileV1Mapper.cs | 22 + .../RepositoryActionAssociateFileV1.cs | 25 + .../BrowseRepositoryV1Mapper.cs | 59 ++ .../RepositoryActionBrowseRepositoryV1.cs | 25 + .../Command/RepositoryActionCommandV1.cs | 35 ++ .../RepositoryActionCommandV1Mapper.cs | 24 + .../Folder/RepositoryActionFolderV1.cs | 33 ++ .../Folder/RepositoryActionFolderV1Mapper.cs | 42 ++ .../ForEach/RepositoryActionForEachV1.cs | 47 ++ .../RepositoryActionForEachV1Mapper.cs | 71 +++ .../Checkout/RepositoryActionGitCheckoutV1.cs | 23 + .../RepositoryActionJustTextV1Mapper.cs | 26 + .../Yaml/Model/ActionMenus/IContext.cs | 8 + .../Yaml/Model/ActionMenus/IDeferred.cs | 6 + .../Yaml/Model/ActionMenus/IMenuActions.cs | 6 + .../Yaml/Model/ActionMenus/IName.cs | 11 + .../JustText/RepositoryActionJustTextV1.cs | 31 + .../RepositoryActionJustTextV1Mapper.cs | 16 + .../Yaml/Model/ContextRoot.cs | 8 + .../Yaml/Model/Ctx/Context.cs | 8 + .../Yaml/Model/Ctx/ContextActionMapperBase.cs | 20 + ...aluateVariableActionContextActionMapper.cs | 19 + .../EvaluateVariableContextAction.cs | 29 + .../Model/Ctx/ExecuteScript/ExecuteScript.cs | 29 + .../ExecuteScriptContextActionMapper.cs | 17 + .../Yaml/Model/Ctx/IContextActionMapper.cs | 12 + .../Yaml/Model/Ctx/IEnabled.cs | 6 + .../Yaml/Model/Ctx/INamedContextAction.cs | 8 + .../Ctx/LoadFile/LoadFileContextAction.cs | 29 + ...oadFileContextActionContextActionMapper.cs | 74 +++ .../Yaml/Model/Ctx/NamedContextAction.cs | 13 + ...RenderVariableActionContextActionMapper.cs | 20 + .../RenderVariableContextAction.cs | 31 + .../SetVariableActionContextActionMapper.cs | 13 + .../SetVariable/SetVariableContextAction.cs | 38 ++ src/RepoM.ActionMenu.Core/Yaml/Model/Root.cs | 10 + .../Yaml/Model/Tags/ITag.cs | 10 + .../Yaml/Model/Tags/TagObject.cs | 13 + .../Yaml/Model/Tags/Tags.cs | 7 + .../Yaml/Model/Templating/ICreateTemplate.cs | 8 + .../Templating/ScribanEvaluateBoolean.cs | 28 + .../Model/Templating/ScribanEvaluateInt.cs | 43 ++ .../Model/Templating/ScribanRenderString.cs | 27 + .../Serialization/ActionMenuDeserializer.cs | 118 ++++ .../Serialization/DefaultContextActionType.cs | 7 + .../DefaultContextActionTypeConverter.cs | 46 ++ .../Serialization/EvaluateObjectConverter.cs | 51 ++ ...eyValueTypeDiscriminatorWithDefaultType.cs | 33 ++ .../TemplateUpdatingNodeDeserializer.cs | 140 +++++ .../Serialization/YamlDotNetExtensions.cs | 25 + .../IActionMenuGenerationContext.cs | 24 + ...IContextMenuActionMenuGenerationContext.cs | 6 + .../ActionMenuFactory/IScope.cs | 15 + .../ActionMenuFactory/ITemplateEvaluator.cs | 10 + .../Attributes/ActionMenuMemberAttribute.cs | 23 + .../Attributes/ActionMenuModuleAttribute.cs | 14 + .../Commands/BrowseRepositoryCommand.cs | 11 + .../Commands/IRepositoryCommand.cs | 8 + .../Commands/NullRepositoryCommand.cs | 10 + .../Commands/StartProcessRepositoryCommand.cs | 15 + .../RepoM.ActionMenu.Interface.csproj | 11 + .../Scriban/IContextRegistration.cs | 40 ++ .../Scriban/ITemplateContextRegistration.cs | 13 + .../UserInterfaceRepositoryActionBase.cs | 24 + .../ActionToRepositoryActionMapperBase.cs | 21 + .../IActionToRepositoryActionMapper.cs | 13 + .../YamlModel/IContextAction.cs | 6 + .../YamlModel/IMenuAction.cs | 8 + .../YamlModel/Templating/EvaluateBoolean.cs | 59 ++ .../YamlModel/Templating/EvaluateInt.cs | 36 ++ .../YamlModel/Templating/EvaluateObject.cs | 17 + .../Templating/EvaluateToAttribute.cs | 7 + .../Templating/EvaluateToBooleanAttribute.cs | 14 + .../Templating/EvaluateToObjectAttribute.cs | 11 + .../YamlModel/Templating/RenderString.cs | 24 + .../RenderToNullableStringAttribute.cs | 14 + .../Templating/RenderToStringAttribute.cs | 14 + src/RepoM.Api/RepoM.Api.csproj | 2 +- .../BooleanWithoutXTests.GetTags.verified.txt | 5 + ...ooleanWithoutXTests.Serialize.verified.txt | 149 +++++ ...oleanWithoutXTests.UseFactory.verified.txt | 186 ++++++ .../DummyRepository.cs | 19 + .../RepoM.ActionMenu.Core.Tests.csproj | 43 ++ .../RepoM.ActionMenu.Core.Tests/UnitTest1.cs | 239 ++++++++ 121 files changed, 6067 insertions(+), 1 deletion(-) create mode 100644 docs_new/file.generated.md create mode 100644 docs_new/repository.generated.md create mode 100644 docs_new/strings.generated.md create mode 100644 src/RepoM.ActionMenu.CodeGen/KalkDescriptor.cs create mode 100644 src/RepoM.ActionMenu.CodeGen/KalkParamDescriptor.cs create mode 100644 src/RepoM.ActionMenu.CodeGen/KalkToGenerate.cs create mode 100644 src/RepoM.ActionMenu.CodeGen/Program.cs create mode 100644 src/RepoM.ActionMenu.CodeGen/RepoM.ActionMenu.CodeGen.csproj create mode 100644 src/RepoM.ActionMenu.CodeGen/Templates/Docs.scriban-cs create mode 100644 src/RepoM.ActionMenu.CodeGen/Templates/Module.scriban-cs create mode 100644 src/RepoM.ActionMenu.Core/Misc/FastStack.cs create mode 100644 src/RepoM.ActionMenu.Core/Misc/FixedTemplateParser.cs create mode 100644 src/RepoM.ActionMenu.Core/Misc/ITemplateParser.cs create mode 100644 src/RepoM.ActionMenu.Core/Misc/TemplateExtensions.cs create mode 100644 src/RepoM.ActionMenu.Core/Model/ActionMenuGenerationContext.cs create mode 100644 src/RepoM.ActionMenu.Core/Model/DisposableContextScriptObject.cs create mode 100644 src/RepoM.ActionMenu.Core/Model/Env/EnvScriptObject.cs create mode 100644 src/RepoM.ActionMenu.Core/Model/Env/EnvSetScriptObject.cs create mode 100644 src/RepoM.ActionMenu.Core/Model/Functions/FileFunctions.cs create mode 100644 src/RepoM.ActionMenu.Core/Model/Functions/FileFunctionsKalk.cs create mode 100644 src/RepoM.ActionMenu.Core/Model/Functions/RepositoryFunctions.cs create mode 100644 src/RepoM.ActionMenu.Core/Model/Functions/StringModule.cs create mode 100644 src/RepoM.ActionMenu.Core/Model/IActionMenuDeserializer.cs create mode 100644 src/RepoM.ActionMenu.Core/Model/Lexers.cs create mode 100644 src/RepoM.ActionMenu.Core/Model/RepoMScriptObject.cs create mode 100644 src/RepoM.ActionMenu.Core/Model/ScribanModuleWithFunctions.cs create mode 100644 src/RepoM.ActionMenu.Core/Model/TemplateEvaluatorExtensions.cs create mode 100644 src/RepoM.ActionMenu.Core/PublicApi/Factory.cs create mode 100644 src/RepoM.ActionMenu.Core/PublicApi/IUserInterfaceActionMenuFactory.cs create mode 100644 src/RepoM.ActionMenu.Core/RepoM.ActionMenu.Core.csproj create mode 100644 src/RepoM.ActionMenu.Core/RepoMCodeGen.generated.cs create mode 100644 src/RepoM.ActionMenu.Core/Services/UserInterfaceActionMenuFactory.cs create mode 100644 src/RepoM.ActionMenu.Core/UserInterface/DeferredSubActionsUserInterfaceRepositoryAction.cs create mode 100644 src/RepoM.ActionMenu.Core/UserInterface/UserInterfaceRepositoryAction.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/ActionMenu.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/AssociateFile/ActionAssociateFileV1Mapper.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/AssociateFile/RepositoryActionAssociateFileV1.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/BrowseRepository/BrowseRepositoryV1Mapper.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/BrowseRepository/RepositoryActionBrowseRepositoryV1.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/Command/RepositoryActionCommandV1.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/Command/RepositoryActionCommandV1Mapper.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/Folder/RepositoryActionFolderV1.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/Folder/RepositoryActionFolderV1Mapper.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/ForEach/RepositoryActionForEachV1.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/ForEach/RepositoryActionForEachV1Mapper.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/Git/Checkout/RepositoryActionGitCheckoutV1.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/Git/Checkout/RepositoryActionJustTextV1Mapper.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/IContext.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/IDeferred.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/IMenuActions.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/IName.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/JustText/RepositoryActionJustTextV1.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/JustText/RepositoryActionJustTextV1Mapper.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/ContextRoot.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/Context.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/ContextActionMapperBase.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/EvaluateVariable/EvaluateVariableActionContextActionMapper.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/EvaluateVariable/EvaluateVariableContextAction.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/ExecuteScript/ExecuteScript.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/ExecuteScript/ExecuteScriptContextActionMapper.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/IContextActionMapper.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/IEnabled.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/INamedContextAction.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/LoadFile/LoadFileContextAction.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/LoadFile/LoadFileContextActionContextActionMapper.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/NamedContextAction.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/RendererVariable/RenderVariableActionContextActionMapper.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/RendererVariable/RenderVariableContextAction.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/SetVariable/SetVariableActionContextActionMapper.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/SetVariable/SetVariableContextAction.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/Root.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/Tags/ITag.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/Tags/TagObject.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/Tags/Tags.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/Templating/ICreateTemplate.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/Templating/ScribanEvaluateBoolean.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/Templating/ScribanEvaluateInt.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Model/Templating/ScribanRenderString.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Serialization/ActionMenuDeserializer.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Serialization/DefaultContextActionType.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Serialization/DefaultContextActionTypeConverter.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Serialization/EvaluateObjectConverter.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Serialization/KeyValueTypeDiscriminatorWithDefaultType.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Serialization/TemplateUpdatingNodeDeserializer.cs create mode 100644 src/RepoM.ActionMenu.Core/Yaml/Serialization/YamlDotNetExtensions.cs create mode 100644 src/RepoM.ActionMenu.Interface/ActionMenuFactory/IActionMenuGenerationContext.cs create mode 100644 src/RepoM.ActionMenu.Interface/ActionMenuFactory/IContextMenuActionMenuGenerationContext.cs create mode 100644 src/RepoM.ActionMenu.Interface/ActionMenuFactory/IScope.cs create mode 100644 src/RepoM.ActionMenu.Interface/ActionMenuFactory/ITemplateEvaluator.cs create mode 100644 src/RepoM.ActionMenu.Interface/Attributes/ActionMenuMemberAttribute.cs create mode 100644 src/RepoM.ActionMenu.Interface/Attributes/ActionMenuModuleAttribute.cs create mode 100644 src/RepoM.ActionMenu.Interface/Commands/BrowseRepositoryCommand.cs create mode 100644 src/RepoM.ActionMenu.Interface/Commands/IRepositoryCommand.cs create mode 100644 src/RepoM.ActionMenu.Interface/Commands/NullRepositoryCommand.cs create mode 100644 src/RepoM.ActionMenu.Interface/Commands/StartProcessRepositoryCommand.cs create mode 100644 src/RepoM.ActionMenu.Interface/RepoM.ActionMenu.Interface.csproj create mode 100644 src/RepoM.ActionMenu.Interface/Scriban/IContextRegistration.cs create mode 100644 src/RepoM.ActionMenu.Interface/Scriban/ITemplateContextRegistration.cs create mode 100644 src/RepoM.ActionMenu.Interface/UserInterface/UserInterfaceRepositoryActionBase.cs create mode 100644 src/RepoM.ActionMenu.Interface/YamlModel/ActionToRepositoryActionMapperBase.cs create mode 100644 src/RepoM.ActionMenu.Interface/YamlModel/IActionToRepositoryActionMapper.cs create mode 100644 src/RepoM.ActionMenu.Interface/YamlModel/IContextAction.cs create mode 100644 src/RepoM.ActionMenu.Interface/YamlModel/IMenuAction.cs create mode 100644 src/RepoM.ActionMenu.Interface/YamlModel/Templating/EvaluateBoolean.cs create mode 100644 src/RepoM.ActionMenu.Interface/YamlModel/Templating/EvaluateInt.cs create mode 100644 src/RepoM.ActionMenu.Interface/YamlModel/Templating/EvaluateObject.cs create mode 100644 src/RepoM.ActionMenu.Interface/YamlModel/Templating/EvaluateToAttribute.cs create mode 100644 src/RepoM.ActionMenu.Interface/YamlModel/Templating/EvaluateToBooleanAttribute.cs create mode 100644 src/RepoM.ActionMenu.Interface/YamlModel/Templating/EvaluateToObjectAttribute.cs create mode 100644 src/RepoM.ActionMenu.Interface/YamlModel/Templating/RenderString.cs create mode 100644 src/RepoM.ActionMenu.Interface/YamlModel/Templating/RenderToNullableStringAttribute.cs create mode 100644 src/RepoM.ActionMenu.Interface/YamlModel/Templating/RenderToStringAttribute.cs create mode 100644 tests/RepoM.ActionMenu.Core.Tests/BooleanWithoutXTests.GetTags.verified.txt create mode 100644 tests/RepoM.ActionMenu.Core.Tests/BooleanWithoutXTests.Serialize.verified.txt create mode 100644 tests/RepoM.ActionMenu.Core.Tests/BooleanWithoutXTests.UseFactory.verified.txt create mode 100644 tests/RepoM.ActionMenu.Core.Tests/DummyRepository.cs create mode 100644 tests/RepoM.ActionMenu.Core.Tests/RepoM.ActionMenu.Core.Tests.csproj create mode 100644 tests/RepoM.ActionMenu.Core.Tests/UnitTest1.cs diff --git a/RepoM.sln b/RepoM.sln index 87aa8f73..3455cbbd 100644 --- a/RepoM.sln +++ b/RepoM.sln @@ -55,6 +55,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RepoM.Plugin.WebBrowser", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RepoM.Plugin.WebBrowser.Tests", "tests\RepoM.Plugin.WebBrowser.Tests\RepoM.Plugin.WebBrowser.Tests.csproj", "{E976587E-F48E-4647-A307-91EFEC8F571C}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RepoM.ActionMenu.Core", "src\RepoM.ActionMenu.Core\RepoM.ActionMenu.Core.csproj", "{2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RepoM.ActionMenu.Interface", "src\RepoM.ActionMenu.Interface\RepoM.ActionMenu.Interface.csproj", "{2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RepoM.ActionMenu.Core.Tests", "tests\RepoM.ActionMenu.Core.Tests\RepoM.ActionMenu.Core.Tests.csproj", "{4E22232B-1C6E-473A-AC82-F151C09D90CB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ActionMenu", "ActionMenu", "{D44E8C7C-76D4-4677-AB2C-4E4F32E93413}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RepoM.ActionMenu.CodeGen", "src\RepoM.ActionMenu.CodeGen\RepoM.ActionMenu.CodeGen.csproj", "{F493CFD2-1352-4D17-9A93-2B18D31F6F2C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -569,6 +579,86 @@ Global {E976587E-F48E-4647-A307-91EFEC8F571C}.Release|x64.Build.0 = Release|Any CPU {E976587E-F48E-4647-A307-91EFEC8F571C}.Release|x86.ActiveCfg = Release|Any CPU {E976587E-F48E-4647-A307-91EFEC8F571C}.Release|x86.Build.0 = Release|Any CPU + {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Debug|ARM.ActiveCfg = Debug|Any CPU + {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Debug|ARM.Build.0 = Debug|Any CPU + {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Debug|ARM64.Build.0 = Debug|Any CPU + {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Debug|x64.ActiveCfg = Debug|Any CPU + {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Debug|x64.Build.0 = Debug|Any CPU + {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Debug|x86.ActiveCfg = Debug|Any CPU + {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Debug|x86.Build.0 = Debug|Any CPU + {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Release|Any CPU.Build.0 = Release|Any CPU + {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Release|ARM.ActiveCfg = Release|Any CPU + {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Release|ARM.Build.0 = Release|Any CPU + {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Release|ARM64.ActiveCfg = Release|Any CPU + {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Release|ARM64.Build.0 = Release|Any CPU + {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Release|x64.ActiveCfg = Release|Any CPU + {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Release|x64.Build.0 = Release|Any CPU + {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Release|x86.ActiveCfg = Release|Any CPU + {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C}.Release|x86.Build.0 = Release|Any CPU + {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Debug|ARM.ActiveCfg = Debug|Any CPU + {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Debug|ARM.Build.0 = Debug|Any CPU + {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Debug|ARM64.Build.0 = Debug|Any CPU + {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Debug|x64.ActiveCfg = Debug|Any CPU + {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Debug|x64.Build.0 = Debug|Any CPU + {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Debug|x86.ActiveCfg = Debug|Any CPU + {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Debug|x86.Build.0 = Debug|Any CPU + {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Release|Any CPU.Build.0 = Release|Any CPU + {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Release|ARM.ActiveCfg = Release|Any CPU + {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Release|ARM.Build.0 = Release|Any CPU + {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Release|ARM64.ActiveCfg = Release|Any CPU + {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Release|ARM64.Build.0 = Release|Any CPU + {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Release|x64.ActiveCfg = Release|Any CPU + {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Release|x64.Build.0 = Release|Any CPU + {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Release|x86.ActiveCfg = Release|Any CPU + {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96}.Release|x86.Build.0 = Release|Any CPU + {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Debug|ARM.ActiveCfg = Debug|Any CPU + {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Debug|ARM.Build.0 = Debug|Any CPU + {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Debug|ARM64.Build.0 = Debug|Any CPU + {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Debug|x64.ActiveCfg = Debug|Any CPU + {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Debug|x64.Build.0 = Debug|Any CPU + {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Debug|x86.ActiveCfg = Debug|Any CPU + {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Debug|x86.Build.0 = Debug|Any CPU + {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Release|Any CPU.Build.0 = Release|Any CPU + {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Release|ARM.ActiveCfg = Release|Any CPU + {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Release|ARM.Build.0 = Release|Any CPU + {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Release|ARM64.ActiveCfg = Release|Any CPU + {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Release|ARM64.Build.0 = Release|Any CPU + {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Release|x64.ActiveCfg = Release|Any CPU + {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Release|x64.Build.0 = Release|Any CPU + {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Release|x86.ActiveCfg = Release|Any CPU + {4E22232B-1C6E-473A-AC82-F151C09D90CB}.Release|x86.Build.0 = Release|Any CPU + {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Debug|ARM.ActiveCfg = Debug|Any CPU + {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Debug|ARM.Build.0 = Debug|Any CPU + {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Debug|ARM64.Build.0 = Debug|Any CPU + {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Debug|x64.ActiveCfg = Debug|Any CPU + {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Debug|x64.Build.0 = Debug|Any CPU + {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Debug|x86.ActiveCfg = Debug|Any CPU + {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Debug|x86.Build.0 = Debug|Any CPU + {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Release|Any CPU.Build.0 = Release|Any CPU + {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Release|ARM.ActiveCfg = Release|Any CPU + {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Release|ARM.Build.0 = Release|Any CPU + {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Release|ARM64.ActiveCfg = Release|Any CPU + {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Release|ARM64.Build.0 = Release|Any CPU + {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Release|x64.ActiveCfg = Release|Any CPU + {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Release|x64.Build.0 = Release|Any CPU + {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Release|x86.ActiveCfg = Release|Any CPU + {F493CFD2-1352-4D17-9A93-2B18D31F6F2C}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -593,6 +683,10 @@ Global {A2264CB6-39DE-4EA2-8C1D-BA28115351E7} = {D6E372DC-10D3-4997-9DFC-568B4666635A} {23D796D1-1902-4357-9C95-4FB120A24A6E} = {D6E372DC-10D3-4997-9DFC-568B4666635A} {E976587E-F48E-4647-A307-91EFEC8F571C} = {D6E372DC-10D3-4997-9DFC-568B4666635A} + {2C3EDAB0-915C-4BB4-91AD-0EE5F4B8741C} = {D44E8C7C-76D4-4677-AB2C-4E4F32E93413} + {2947A3C0-A3D5-457B-BEAA-F5C3A242EC96} = {D44E8C7C-76D4-4677-AB2C-4E4F32E93413} + {4E22232B-1C6E-473A-AC82-F151C09D90CB} = {D44E8C7C-76D4-4677-AB2C-4E4F32E93413} + {F493CFD2-1352-4D17-9A93-2B18D31F6F2C} = {D44E8C7C-76D4-4677-AB2C-4E4F32E93413} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1765ABAA-0652-4DA5-ABBF-05396F2957D7} diff --git a/RepoM.sln.DotSettings b/RepoM.sln.DotSettings index cbce9ef1..24b37438 100644 --- a/RepoM.sln.DotSettings +++ b/RepoM.sln.DotSettings @@ -9,5 +9,6 @@ True True True + True True True \ No newline at end of file diff --git a/docs_new/file.generated.md b/docs_new/file.generated.md new file mode 100644 index 00000000..a3d6cf93 --- /dev/null +++ b/docs_new/file.generated.md @@ -0,0 +1,92 @@ +--- +title: File Module +url: /doc/api/file/ +--- + + + +In order to use the functions provided by this module, you need to import this module: + +```kalk +>>> import File +``` + +## dir_exists + +`dir_exists(path)` + +Checks if the specified directory path exists on the disk. + +- `path`: Absolute path to a directory. + +### Returns + +`true` if the specified directory path exists on the disk, `false` otherwise. + +### Example + +```kalk +dir_exists "testdir" +# dir_exists("testdir") +out = true + rmdir "testdir" + dir_exists "testdir" +# dir_exists("testdir") +out = false +``` + +## file_exists + +`file_exists(path)` + +Checks if the specified file path exists on the disk. + +- `path`: Absolute path to a file. + +### Returns + +`true` if the specified file path exists on the disk, `false` otherwise. + +### Example + +```kalk + rm "test.txt" + file_exists "test.txt" +# file_exists("test.txt") +out = false + save_text("content", "test.txt") + file_exists "test.txt" +# file_exists("test.txt") +out = true +``` + +## find_files + +`find_files(rootPath,searchPattern)` + +Find files in a given directory based on the search pattern. Resulting filenames are absolute path based. + +- `rootPath`: The root folder. +- `searchPattern`: The search string to match against the names of directories. This parameter can contain a combination of valid literal path and wildcard (`*` and `?`) characters, but it doesn't support regular expressions. + +### Returns + +Returns an enumerable collection of full paths of the files or directories that matches the specified search pattern. + +### Example + +```kalk +firstsecond + +find_files 'C:\Users\coenm\RepoM\src' '*.sln' + +xxx + sdf +find_files 'C:\Users\coenm\RepoM\src' '*.csproj' +[1, 2, 3, 4] +``` + +## find_files_interface + +`find_files_interface` + diff --git a/docs_new/repository.generated.md b/docs_new/repository.generated.md new file mode 100644 index 00000000..f32ab7e1 --- /dev/null +++ b/docs_new/repository.generated.md @@ -0,0 +1,62 @@ +--- +title: Repository Module +url: /doc/api/repository/ +--- + + + +In order to use the functions provided by this module, you need to import this module: + +```kalk +>>> import Repository +``` + +## branch + +`branch` + +Gets the current branch of the repository + +### Returns + +The name of the current branch. + +## branches + +`branches` + +Gets the current branch of the repository + +### Returns + +The name of the current branch. + +## local-branches + +`local-branches` + +Gets the local branches + +### Returns + +All local branches. + +## name + +`name` + +Gets the name of the repository. + +### Returns + +The name of the repository. + +## path + +`path` + +Gets the path of the repository. + +### Returns + +The path of the repository. diff --git a/docs_new/strings.generated.md b/docs_new/strings.generated.md new file mode 100644 index 00000000..e4f9611a --- /dev/null +++ b/docs_new/strings.generated.md @@ -0,0 +1,494 @@ +--- +title: Strings Module +url: /doc/api/strings/ +--- + +Modules that provides string functions (e.g `upcase`, `downcase`, `regex_escape`...). + +In order to use the functions provided by this module, you need to import this module: + +```kalk +>>> import Strings +``` + +## capitalize + +`capitalize(text)` + +Converts the first character of the passed string to a upper case character. + +- `text`: The input string + +### Returns + +The capitalized input string + +### Example + +```kalk +>>> "test" |> capitalize +# "test" |> capitalize +out = "Test" +``` + +## capitalize_words + +`capitalize_words(text)` + +Converts the first character of each word in the passed string to a upper case character. + +- `text`: The input string + +### Returns + +The capitalized input string + +### Example + +```kalk +>>> "This is easy" |> capitalize_words +# "This is easy" |> capitalize_words +out = "This Is Easy" +``` + +## downcase + +`downcase(text)` + +Converts the string to lower case. + +- `text`: The input string + +### Returns + +The input string lower case + +### Example + +```kalk +>>> "TeSt" |> downcase +# "TeSt" |> downcase +out = "test" +``` + +## endswith + +`endswith(text,end)` + +Returns a boolean indicating whether the input string ends with the specified string `value`. + +- `text`: The input string +- `end`: The string to look for + +### Returns + +true if `text` ends with the specified string `value` + +### Example + +```kalk +>>> "This is easy" |> endswith "easy" +# "This is easy" |> endswith("easy") +out = true +>>> "This is easy" |> endswith "none" +# "This is easy" |> endswith("none") +out = false +``` + +## escape + +`escape(text)` + +Escapes a string with escape characters. + +- `text`: The input string + +### Returns + +The two strings concatenated + +### Example + +```kalk +>>> "Hel\tlo\n\"W\\orld" |> escape +# "Hel\tlo\n\"W\\orld" |> escape +out = "Hel\\tlo\\n\\\"W\\\\orld" +``` + +## handleize + +`handleize(text)` + +Returns a url handle from the input string. + +- `text`: The input string + +### Returns + +A url handle + +### Example + +```kalk +>>> '100% M @ Ms!!!' |> handleize +# '100% M @ Ms!!!' |> handleize +out = "100-m-ms" +``` + +## lstrip + +`lstrip(text)` + +Removes any whitespace characters on the **left** side of the input string. + +- `text`: The input string + +### Returns + +The input string without any left whitespace characters + +### Example + +```kalk +>>> ' too many spaces' |> lstrip +# ' too many spaces' |> lstrip +out = "too many spaces" +``` + +## pad_left + +`pad_left(text,width)` + +Pads a string with leading spaces to a specified total length. + +- `text`: The input string +- `width`: The number of characters in the resulting string + +### Returns + +The input string padded + +### Example + +```kalk +>>> "world" |> pad_left 10 +# "world" |> pad_left(10) +out = " world" +``` + +## pad_right + +`pad_right(text,width)` + +Pads a string with trailing spaces to a specified total length. + +- `text`: The input string +- `width`: The number of characters in the resulting string + +### Returns + +The input string padded + +### Example + +```kalk +>>> "hello" |> pad_right 10 +# "hello" |> pad_right(10) +out = "hello " +``` + +## pluralize + +`pluralize(number,singular,plural)` + +Outputs the singular or plural version of a string based on the value of a number. + +- `number`: The number to check +- `singular`: The singular string to return if number is == 1 +- `plural`: The plural string to return if number is != 1 + +### Returns + +The singular or plural string based on number + +### Example + +```kalk +>>> 3 |> pluralize('product', 'products') +# 3 |> pluralize('product', 'products') +out = "products" +``` + +## regex_escape + +`regex_escape(text)` + +Escapes a minimal set of characters (`\`, `*`, `+`, `?`, `|`, `{`, `[`, `(`,`)`, `^`, `$`,`.`, `#`, and white space) +by replacing them with their escape codes. +This instructs the regular expression engine to interpret these characters literally rather than as metacharacters. + +- `text`: The input string that contains the text to convert. + +### Returns + +A string of characters with metacharacters converted to their escaped form. + +### Example + +```kalk +>>> "(abc.*)" |> regex_escape +# "(abc.*)" |> regex_escape +out = "\\(abc\\.\\*\\)" +``` + +## regex_match + +`regex_match(text,pattern,options?)` + +Searches an input string for a substring that matches a regular expression pattern and returns an array with the match occurences. + +- `text`: The string to search for a match. +- `pattern`: The regular expression pattern to match. +- `options`: A string with regex options, that can contain the following option characters (default is `null`): + - `i`: Specifies case-insensitive matching. + - `m`: Multiline mode. Changes the meaning of `^` and `$` so they match at the beginning and end, respectively, of any line, and not just the beginning and end of the entire string. + - `s`: Specifies single-line mode. Changes the meaning of the dot `.` so it matches every character (instead of every character except `\n`). + - `x`: Eliminates unescaped white space from the pattern and enables comments marked with `#`. + +### Returns + +An array that contains all the match groups. The first group contains the entire match. The other elements contain regex matched groups `(..)`. An empty array returned means no match. + +### Example + +```kalk +>>> "this is a text123" |> regex_match `(\w+) a ([a-z]+\d+)` +# "this is a text123" |> regex_match(`(\w+) a ([a-z]+\d+)`) +out = ["is a text123", "is", "text123"] +``` + +## regex_matches + +`regex_matches(context,text,pattern,options?)` + +Searches an input string for multiple substrings that matches a regular expression pattern and returns an array with the match occurences. + +- `context`: The template context (to fetch the timeout configuration) +- `text`: The string to search for a match. +- `pattern`: The regular expression pattern to match. +- `options`: A string with regex options, that can contain the following option characters (default is `null`): + - `i`: Specifies case-insensitive matching. + - `m`: Multiline mode. Changes the meaning of `^` and `$` so they match at the beginning and end, respectively, of any line, and not just the beginning and end of the entire string. + - `s`: Specifies single-line mode. Changes the meaning of the dot `.` so it matches every character (instead of every character except `\n`). + - `x`: Eliminates unescaped white space from the pattern and enables comments marked with `#`. + +### Returns + +An array of matches that contains all the match groups. The first group contains the entire match. The other elements contain regex matched groups `(..)`. An empty array returned means no match. + +### Example + +```kalk +>>> "this is a text123" |> regex_matches `(\w+)` +# "this is a text123" |> regex_matches(`(\w+)`) +out = [["this", "this"], ["is", "is"], ["a", "a"], ["text123", "text123"]] +``` + +## regex_replace + +`regex_replace(text,pattern,replace,options?)` + +In a specified input string, replaces strings that match a regular expression pattern with a specified replacement string. + +- `text`: The string to search for a match. +- `pattern`: The regular expression pattern to match. +- `replace`: The replacement string. +- `options`: A string with regex options, that can contain the following option characters (default is `null`): + - `i`: Specifies case-insensitive matching. + - `m`: Multiline mode. Changes the meaning of `^` and `$` so they match at the beginning and end, respectively, of any line, and not just the beginning and end of the entire string. + - `s`: Specifies single-line mode. Changes the meaning of the dot `.` so it matches every character (instead of every character except `\n`). + - `x`: Eliminates unescaped white space from the pattern and enables comments marked with `#`. + +### Returns + +A new string that is identical to the input string, except that the replacement string takes the place of each matched string. If pattern is not matched in the current instance, the method returns the current instance unchanged. + +### Example + +```kalk +>>> "abbbbcccd" |> regex_replace("b+c+","-Yo-") +# "abbbbcccd" |> regex_replace("b+c+", "-Yo-") +out = "a-Yo-d" +``` + +## regex_split + +`regex_split(text,pattern,options?)` + +Splits an input string into an array of substrings at the positions defined by a regular expression match. + +- `text`: The string to split. +- `pattern`: The regular expression pattern to match. +- `options`: A string with regex options, that can contain the following option characters (default is `null`): + - `i`: Specifies case-insensitive matching. + - `m`: Multiline mode. Changes the meaning of `^` and `$` so they match at the beginning and end, respectively, of any line, and not just the beginning and end of the entire string. + - `s`: Specifies single-line mode. Changes the meaning of the dot `.` so it matches every character (instead of every character except `\n`). + - `x`: Eliminates unescaped white space from the pattern and enables comments marked with `#`. + +### Returns + +A string array. + +### Example + +```kalk +>>> "a, b , c, d" |> regex_split `\s*,\s*` +# "a, b , c, d" |> regex_split(`\s*,\s*`) +out = ["a", "b", "c", "d"] +``` + +## regex_unescape + +`regex_unescape(text)` + +Converts any escaped characters in the input string. + +- `text`: The input string containing the text to convert. + +### Returns + +A string of characters with any escaped characters converted to their unescaped form. + +### Example + +```kalk +>>> "\\(abc\\.\\*\\)" |> regex_unescape +# "\\(abc\\.\\*\\)" |> regex_unescape +out = "(abc.*)" +``` + +## rstrip + +`rstrip(text)` + +Removes any whitespace characters on the **right** side of the input string. + +- `text`: The input string + +### Returns + +The input string without any left whitespace characters + +### Example + +```kalk +>>> ' too many spaces ' |> rstrip +# ' too many spaces ' |> rstrip +out = " too many spaces" +``` + +## split + +`split(text,match)` + +The `split` function takes on a substring as a parameter. +The substring is used as a delimiter to divide a string into an array. You can output different parts of an array using `array` functions. + +- `text`: The input string +- `match`: The string used to split the input `text` string + +### Returns + +An enumeration of the substrings + +### Example + +```kalk +>>> "Hi, how are you today?" |> split ' ' +# "Hi, how are you today?" |> split(' ') +out = ["Hi,", "how", "are", "you", "today?"] +``` + +## startswith + +`startswith(text,start)` + +Returns a boolean indicating whether the input string starts with the specified string `value`. + +- `text`: The input string +- `start`: The string to look for + +### Returns + +true if `text` starts with the specified string `value` + +### Example + +```kalk +>>> "This is easy" |> startswith "This" +# "This is easy" |> startswith("This") +out = true +>>> "This is easy" |> startswith "easy" +# "This is easy" |> startswith("easy") +out = false +``` + +## strip + +`strip(text)` + +Removes any whitespace characters on the **left** and **right** side of the input string. + +- `text`: The input string + +### Returns + +The input string without any left and right whitespace characters + +### Example + +```kalk +>>> ' too many spaces ' |> strip +# ' too many spaces ' |> strip +out = "too many spaces" +``` + +## strip_newlines + +`strip_newlines(text)` + +Removes any line breaks/newlines from a string. + +- `text`: The input string + +### Returns + +The input string without any breaks/newlines characters + +### Example + +```kalk +>>> "This is a string.\r\n With \nanother \rstring" |> strip_newlines +# "This is a string.\r\n With \nanother \rstring" |> strip_newlines +out = "This is a string. With another string" +``` + +## upcase + +`upcase(text)` + +Converts the string to uppercase + +- `text`: The input string + +### Returns + +The input string upper case + +### Example + +```kalk +>>> "test" |> upcase +# "test" |> upcase +out = "TEST" +``` diff --git a/src/RepoM.ActionMenu.CodeGen/KalkDescriptor.cs b/src/RepoM.ActionMenu.CodeGen/KalkDescriptor.cs new file mode 100644 index 00000000..4743454b --- /dev/null +++ b/src/RepoM.ActionMenu.CodeGen/KalkDescriptor.cs @@ -0,0 +1,30 @@ +namespace Kalk.Core; + +using System.Collections.Generic; + +public class KalkDescriptor +{ + public KalkDescriptor() + { + Names = new List(); + Params = new List(); + } + + public List Names { get; } + + public bool IsCommand { get; set; } + + public string Category { get; set; } + + public string Description { get; set; } + + public List Params { get; } + + public string Syntax { get; set; } + + public string Returns { get; set; } + + public string Remarks { get; set; } + + public string Example { get; set; } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.CodeGen/KalkParamDescriptor.cs b/src/RepoM.ActionMenu.CodeGen/KalkParamDescriptor.cs new file mode 100644 index 00000000..254afe6a --- /dev/null +++ b/src/RepoM.ActionMenu.CodeGen/KalkParamDescriptor.cs @@ -0,0 +1,21 @@ +namespace Kalk.Core +{ + public class KalkParamDescriptor + { + public KalkParamDescriptor() + { + } + + public KalkParamDescriptor(string name, string description) + { + Name = name; + Description = description; + } + + public string Name { get; set; } + + public string Description { get; set; } + + public bool IsOptional { get; set; } + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.CodeGen/KalkToGenerate.cs b/src/RepoM.ActionMenu.CodeGen/KalkToGenerate.cs new file mode 100644 index 00000000..8f15960d --- /dev/null +++ b/src/RepoM.ActionMenu.CodeGen/KalkToGenerate.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using Kalk.Core; + +namespace Kalk.CodeGen +{ + public abstract class KalkDescriptorToGenerate : KalkDescriptor + { + protected KalkDescriptorToGenerate() + { + Tests = new List<(string, string)>(); + } + + public bool IsModule { get; set; } + + public bool IsBuiltin { get; set; } + + public List<(string, string)> Tests { get; } + } + + + public class KalkModuleToGenerate : KalkDescriptorToGenerate + { + public KalkModuleToGenerate() + { + Members = new List(); + IsModule = true; + } + + public string Name { get; set; } + + public string Title { get; set; } + + public string Url { get; set; } + + public string Namespace { get; set; } + + public string ClassName { get; set; } + + public List Members { get; } + } + + public class KalkMemberToGenerate : KalkDescriptorToGenerate + { + public KalkMemberToGenerate() + { + } + + public string Name { get; set; } + + public string XmlId { get; set; } + + public string CSharpName { get; set; } + + public bool IsFunc { get; set; } + + public bool IsAction { get; set; } + + public bool IsConst { get; set; } + + public string Cast { get; set; } + + public KalkModuleToGenerate Module { get; set; } + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.CodeGen/Program.cs b/src/RepoM.ActionMenu.CodeGen/Program.cs new file mode 100644 index 00000000..c6198839 --- /dev/null +++ b/src/RepoM.ActionMenu.CodeGen/Program.cs @@ -0,0 +1,560 @@ +namespace RepoM.ActionMenu.CodeGen; + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Web; +using System.Xml; +using System.Xml.Linq; +using Broslyn; +using Kalk.CodeGen; +using Kalk.Core; +using Microsoft.CodeAnalysis; +using RepoM.ActionMenu.Interface.Attributes; +using Scriban; +using Scriban.Runtime; + +public partial class Program +{ + private static readonly Regex _removeCode = RemoveCodeRegex(); + private static readonly Regex _promptRegex = PromptRegex(); + + static async Task Main(string[] args) + { + // not sure why Kalk has this. + _ = typeof(System.Composition.CompositionContext).Name; + + var rootFolder = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, "../../../../..")); + var srcFolder = Path.Combine(rootFolder, "src"); + var docsFolder = Path.Combine(rootFolder, "docs_new"); + var projectName = "RepoM.ActionMenu.Core"; + var pathToSolution = Path.Combine(srcFolder, projectName, $"{projectName}.csproj"); + var pathToGeneratedCode = Path.Combine(srcFolder, projectName, "RepoMCodeGen.generated.cs"); + + if (!Directory.Exists(Path.Combine(rootFolder, ".git"))) + { + throw new Exception("Wrong root folder"); + } + + if (!Directory.Exists(srcFolder)) + { + throw new Exception($"src folder `{srcFolder}` doesn't exist"); + } + + if (!Directory.Exists(docsFolder)) + { + throw new Exception($"docsFolder folder `{docsFolder}` doesn't exist"); + } + + if (!File.Exists(pathToSolution)) + { + throw new Exception($"File `{pathToSolution}` does not exist"); + } + + CSharpCompilationCaptureResult compilationCaptureResult = CSharpCompilationCapture.Build(pathToSolution); + Solution solution = compilationCaptureResult.Workspace.CurrentSolution; + Project[] solutionProjects = solution.Projects.ToArray(); + Project project = Array.Find(solutionProjects, x => x.Name == projectName) ?? throw new Exception($"Project `{projectName}` not found in solution"); + + // Make sure that doc will be parsed + project = project.WithParseOptions(project.ParseOptions!.WithDocumentationMode(DocumentationMode.Parse)); + + // Compile the project + Compilation compilation = await project.GetCompilationAsync() ?? throw new Exception("Compilation failed"); + + ImmutableArray diagnostics = compilation.GetDiagnostics(); + Diagnostic[] errors = diagnostics.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error).ToArray(); + if (errors.Length > 0) + { + Console.WriteLine("Compilation errors:"); + foreach (var error in errors) + { + Console.WriteLine(error); + } + + Console.WriteLine("Error, Exiting."); + Environment.Exit(1); + return; + } + + //var kalkEngine = compilation.GetTypeByMetadataName("Kalk.Core.KalkEngine"); + var mapNameToModule = new Dictionary(); + + void GetOrCreateModule(ITypeSymbol typeSymbol, string className, AttributeData moduleAttribute, out KalkModuleToGenerate moduleToGenerate) + { + var ns = typeSymbol.ContainingNamespace.ToDisplayString(); + + var fullClassName = $"{ns}.{className}"; + if (!mapNameToModule.TryGetValue(fullClassName, out moduleToGenerate)) + { + moduleToGenerate = new KalkModuleToGenerate() + { + Namespace = typeSymbol.ContainingNamespace.ToDisplayString(), + ClassName = className, + }; + mapNameToModule.Add(fullClassName, moduleToGenerate); + + if (moduleAttribute != null) + { + moduleToGenerate.Name = moduleAttribute.ConstructorArguments[0].Value.ToString(); + moduleToGenerate.Names.Add(moduleToGenerate.Name!); + moduleToGenerate.Category = "Modules (e.g `import Files`)"; + } + else + { + moduleToGenerate.Name = className.Replace("Module", ""); + moduleToGenerate.IsBuiltin = true; + } + + ExtractDocumentation(typeSymbol, moduleToGenerate); + } + } + + foreach (ISymbol type in compilation.GetSymbolsWithName(x => true, SymbolFilter.Type)) + { + if (type is not ITypeSymbol typeSymbol) + { + continue; + } + + var moduleAttribute = typeSymbol.GetAttributes().FirstOrDefault(x => x.AttributeClass.Name == nameof(ActionMenuModuleAttribute)); + KalkModuleToGenerate? moduleToGenerate = null; + if (moduleAttribute != null) + { + GetOrCreateModule(typeSymbol, typeSymbol.Name, moduleAttribute, out moduleToGenerate); + } + + foreach (var member in typeSymbol.GetMembers()) + { + var attr = member.GetAttributes().FirstOrDefault(x => x.AttributeClass.Name == nameof(ActionMenuMemberAttribute)); + if (attr == null) continue; + + var name = attr.ConstructorArguments[0].Value?.ToString(); + if (string.IsNullOrWhiteSpace(name)) + { + throw new Exception("Name cannot be null or empty."); + } + + var containingType = member.ContainingSymbol; + var className = containingType.Name; + + // In case the module is built-in, we still generate a module for it + if (moduleToGenerate == null) + { + GetOrCreateModule(typeSymbol, className, moduleAttribute!, out moduleToGenerate); + } + + var method = member as IMethodSymbol; + var desc = new KalkMemberToGenerate() + { + Name = name, + XmlId = member.GetDocumentationCommentId(), + Category = string.Empty, + IsCommand = method != null && method.ReturnsVoid, + Module = moduleToGenerate, + }; + desc.Names.Add(name); + + if (method != null) + { + desc.CSharpName = method.Name; + + var builder = new StringBuilder(); + desc.IsAction = method.ReturnsVoid; + desc.IsFunc = !desc.IsAction; + builder.Append(desc.IsAction ? "Action" : "Func"); + + if (method.Parameters.Length > 0 || desc.IsFunc) + { + builder.Append('<'); + } + + for (var i = 0; i < method.Parameters.Length; i++) + { + var parameter = method.Parameters[i]; + if (i > 0) builder.Append(", "); + builder.Append(GetTypeName(parameter.Type)); + } + + if (desc.IsFunc) + { + if (method.Parameters.Length > 0) + { + builder.Append(", "); + } + builder.Append(GetTypeName(method.ReturnType)); + } + + if (method.Parameters.Length > 0 || desc.IsFunc) + { + builder.Append('>'); + } + + desc.Cast = $"({builder})"; + } + else if (member is IPropertySymbol or IFieldSymbol) + { + desc.CSharpName = member.Name; + desc.IsConst = true; + } + + moduleToGenerate.Members.Add(desc); + ExtractDocumentation(member, desc); + } + } + + var modules = mapNameToModule.Values.OrderBy(x => x.ClassName).ToList(); + var templateStr = await File.ReadAllTextAsync("Templates/Module.scriban-cs"); + var template = Template.Parse(templateStr); + + var context = new TemplateContext + { + LoopLimit = 0, + MemberRenamer = x => x.Name + }; + var scriptObject = new ScriptObject() + { + { "modules", modules }, + }; + context.PushGlobal(scriptObject); + + var result = await template.RenderAsync(context); + await File.WriteAllTextAsync(pathToGeneratedCode, result); + // await File.WriteAllTextAsync(Path.Combine(srcFolder, "ScribanRepoM.Tests", "RepoM.ActionMenu", "Generated", "Coen.generated.cs"), result); + + + // Generate module site documentation + foreach(KalkModuleToGenerate module in modules) + { + await GenerateModuleSiteDocumentation(module, docsFolder); + } + + return; + + // Log any errors if a member doesn't have any doc or tests + var functionWithMissingDoc = 0; + var functionWithMissingTests = 0; + foreach (var module in modules) + { + foreach (var member in module.Members) + { + var hasNoDesc = string.IsNullOrEmpty(member.Description); + var hasNoTests = member.Tests.Count == 0; + if ((!hasNoDesc && !hasNoTests) || module.ClassName.Contains("Intrinsics")) + { + continue; + } + + // We don't log for all the matrix constructors, as they are tested separately. + if (module.ClassName == "TypesModule" && member.CSharpName.StartsWith("Create")) + { + continue; + } + + if (hasNoDesc) + { + ++functionWithMissingDoc; + } + + if (hasNoTests) + { + ++functionWithMissingTests; + } + + Console.WriteLine($"The member {member.Name} => {module.ClassName}.{member.CSharpName} doesn't have {(hasNoTests ? "any tests" + (hasNoDesc ? " and" : "") : "")} {(hasNoDesc ? "any docs" : "")}"); + } + } + + Console.WriteLine($"{modules.Count} modules generated."); + Console.WriteLine($"{modules.SelectMany(x => x.Members).Count()} functions generated."); + Console.WriteLine($"{modules.SelectMany(x => x.Members).SelectMany(y => y.Tests).Count()} tests generated."); + Console.WriteLine($"{functionWithMissingDoc} functions with missing doc."); + Console.WriteLine($"{functionWithMissingTests} functions with missing tests."); + } + + + private static async Task GenerateModuleSiteDocumentation(KalkModuleToGenerate module, string siteFolder) + { + if (module.Name == "KalkEngine") + { + module.Name = "General"; + } + + module.Members.Sort((left, right) => string.Compare(left.Name, right.Name, StringComparison.Ordinal)); + + module.Title = $"{module.Name} {(module.IsBuiltin ? "Functions":"Module")}"; + + var name = module.Name.ToLowerInvariant(); + module.Url = $"/doc/api/{name}/"; + + const string templateText = @"--- +title: {{module.Title}} +url: {{module.Url}} +--- +{{~ if !module.IsBuiltin ~}} + +{{ module.Description }} + +In order to use the functions provided by this module, you need to import this module: + +```kalk +>>> import {{module.Name}} +``` +{{~ end ~}} +{{~ if (module.Title | string.contains 'Intrinsics') ~}} + +In order to use the functions provided by this module, you need to import this module: + +```kalk +>>> import HardwareIntrinsics +``` +{%{~ +{{NOTE do}} +~}%} +These intrinsic functions are only available if your CPU supports `{{module.Name}}` features. +{%{~ +{{end}} +~}%} + +{{~ end ~}} +{{~ for member in module.Members ~}} + +## {{member.Name}} + +`{{member.Name}}{{~ if member.Params.size > 0 ~}}({{~ for param in member.Params ~}}{{ param.Name }}{{ param.IsOptional?'?':''}}{{ for.last?'':',' }}{{~ end ~}}){{~ end ~}}` + +{{~ if member.Description ~}} +{{ member.Description | regex.replace `^\s{4}` '' 'm' | string.rstrip }} +{{~ end ~}} +{{~ if member.Params.size > 0 ~}} + + {{~ for param in member.Params ~}} +- `{{ param.Name }}`: {{ param.Description}} + {{~end ~}} +{{~ end ~}} +{{~ if member.Returns ~}} + +### Returns + +{{ member.Returns | regex.replace `^\s{4}` '' 'm' | string.rstrip }} +{{~ end ~}} +{{~ if member.Remarks ~}} + +### Remarks + +{{ member.Remarks | regex.replace `^\s{4}` '' 'm' | string.rstrip }} +{{~ end ~}} +{{~ if member.Example ~}} + +### Example + +```kalk +{{ member.Example | regex.replace `^\s{4}` '' 'm' | string.rstrip }} +``` +{{~ end ~}} +{{~ end ~}} +"; + var template = Template.Parse(templateText); + + var apiFolder = siteFolder; + + // + // // Don't generate hardware.generated.md + // if (name == "hardware") + // { + // return; + // } + + var context = new TemplateContext + { + LoopLimit = 0, + }; + var scriptObject = new ScriptObject() + { + { "module", module }, + }; + context.PushGlobal(scriptObject); + context.MemberRenamer = x => x.Name; + var result = await template.RenderAsync(context); + + await File.WriteAllTextAsync(Path.Combine(apiFolder, $"{name}.generated.md"), result); + } + + private static (string, string)? TryParseTest(string text) + { + var testLines = new StringReader(text); + string? line; + string input = null; + var output = string.Empty; + var startColumn = -1; + while ((line = testLines.ReadLine()) != null) + { + line = line.TrimEnd(); + var matchPrompt = _promptRegex.Match(line); + if (matchPrompt.Success) + { + startColumn = matchPrompt.Groups[1].Length; + input += line.Substring(matchPrompt.Length) + Environment.NewLine; + } + else + { + if (startColumn < 0) + { + throw new InvalidOperationException($"Expecting a previous prompt line >>> before `{line}`"); + } + + line = line.Length >= startColumn ? line[startColumn..] : line; + // If we have a result with ellipsis `...` we can't test this text. + if (line.StartsWith("...")) + { + return null; + } + output += line + Environment.NewLine; + } + } + + return input != null ? (input.TrimEnd(), output.TrimEnd()) : null; + } + + private static void ExtractDocumentation(ISymbol symbol, KalkDescriptorToGenerate desc) + { + var xmlStr = symbol.GetDocumentationCommentXml(); + if (xmlStr.Contains("Find files in a given directory based on the search pattern. Resulting filenames are absolute path based.")) + { + xmlStr = xmlStr; + } + try + { + if (!string.IsNullOrEmpty(xmlStr)) + { + var xmlDoc = XElement.Parse(xmlStr); + var elements = xmlDoc.Elements().ToList(); + + foreach (var element in elements) + { + var text = GetCleanedString(element).Trim(); + if (element.Name == "summary") + { + desc.Description = text; + } + else if (element.Name == "param") + { + var argName = element.Attribute("name")?.Value; + if (argName != null && symbol is IMethodSymbol method) + { + var parameterSymbol = method.Parameters.FirstOrDefault(x => x.Name == argName); + var isOptional = false; + if (parameterSymbol == null) + { + Console.WriteLine($"Invalid XML doc parameter name {argName} not found on method {method}"); + } + else + { + isOptional = parameterSymbol.IsOptional; + } + + desc.Params.Add(new KalkParamDescriptor(argName, text) { IsOptional = isOptional, }); + } + } + else if (element.Name == "returns") + { + desc.Returns = text; + } + else if (element.Name == "remarks") + { + desc.Remarks = text; + } + else if (element.Name == "example") + { + text = _removeCode.Replace(text, string.Empty); + desc.Example += text; + // var test = TryParseTest(text); + // if (test != null) + // { + // desc.Tests.Add(test.Value); + // } + } + else if (element.Name == "test") + { + text = _removeCode.Replace(text, string.Empty); + // var test = TryParseTest(text); + // if (test != null) + // { + // desc.Tests.Add(test.Value); + // } + } + } + } + } + catch (Exception ex) + { + throw new InvalidOperationException($"Error while processing `{symbol}` with XML doc `{xmlStr}", ex); + } + } + + + static string GetTypeName(ITypeSymbol typeSymbol) + { + //if (typeSymbol is IArrayTypeSymbol arrayTypeSymbol) + //{ + // return GetTypeName(arrayTypeSymbol.ElementType) + "[]"; + //} + + //if (typeSymbol.Name == typeof(Nullable).Name) + //{ + // return typeSymbol.ToDisplayString(); + //} + + //if (typeSymbol.Name == "String") return "string"; + //if (typeSymbol.Name == "Object") return "object"; + //if (typeSymbol.Name == "Boolean") return "bool"; + //if (typeSymbol.Name == "Single") return "float"; + //if (typeSymbol.Name == "Double") return "double"; + //if (typeSymbol.Name == "Int32") return "int"; + //if (typeSymbol.Name == "Int64") return "long"; + return typeSymbol.ToDisplayString(); + } + + private static string GetCleanedString(XNode node) + { + if (node.NodeType == XmlNodeType.Text) + { + return node.ToString(); + } + + var element = (XElement) node; + string text; + if (element.Name == "paramref") + { + text = element.Attribute("name")?.Value ?? string.Empty; + } + else + { + + var builder = new StringBuilder(); + foreach (var subElement in element.Nodes()) + { + builder.Append(GetCleanedString(subElement)); + } + + text = builder.ToString(); + } + + if (element.Name == "para") + { + text += "\n"; + } + return HttpUtility.HtmlDecode(text); + } + + [GeneratedRegex("^\\s*```\\w*[ \\t]*[\\r\\n]*", RegexOptions.Multiline)] + private static partial Regex RemoveCodeRegex(); + + [GeneratedRegex("^(\\s*)>>>\\s")] + private static partial Regex PromptRegex(); +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.CodeGen/RepoM.ActionMenu.CodeGen.csproj b/src/RepoM.ActionMenu.CodeGen/RepoM.ActionMenu.CodeGen.csproj new file mode 100644 index 00000000..c0e20c4a --- /dev/null +++ b/src/RepoM.ActionMenu.CodeGen/RepoM.ActionMenu.CodeGen.csproj @@ -0,0 +1,28 @@ + + + + Exe + net7.0 + + + + + + + + + + + + + + + + Always + + + Always + + + + diff --git a/src/RepoM.ActionMenu.CodeGen/Templates/Docs.scriban-cs b/src/RepoM.ActionMenu.CodeGen/Templates/Docs.scriban-cs new file mode 100644 index 00000000..6c1c6f75 --- /dev/null +++ b/src/RepoM.ActionMenu.CodeGen/Templates/Docs.scriban-cs @@ -0,0 +1,58 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable + +using System; +{{~ func GenerateDocDescriptor(item) ~}} + { + var descriptor = {{ if item.IsModule }}Descriptor{{ else }}Descriptors["{{ item.Name }}"]{{ end }}; + descriptor.Category = "{{ item.Category }}"; + descriptor.Description = @"{{ item.Description | string.replace '"' '""' }}"; + descriptor.IsCommand = {{ item.IsCommand }}; + {{~ for param in item.Params ~}} + descriptor.Params.Add(new KalkParamDescriptor("{{ param.Name }}", @"{{ param.Description | string.replace '"' '""' }}") { IsOptional = {{ param.IsOptional }} }); + {{~ end ~}} + {{~ if item.Returns ~}} + descriptor.Returns = @"{{ item.Returns | string.replace '"' '""' }}"; + {{~ end ~}} + {{~ if item.Remarks ~}} + descriptor.Remarks = @"{{ item.Remarks | string.replace '"' '""' }}"; + {{~ end ~}} + {{~ if item.Example ~}} + descriptor.Example = @"{{ item.Example | string.replace '"' '""' }}"; + {{~ end ~}} + } +{{~ end ~}} +{{~ for module in modules ~}} + +namespace {{ module.Namespace }} +{ + partial class {{ module.ClassName }} + { + {{~ if module.Name != 'All' ~}} + {{~ if module.ClassName == 'KalkEngine' ~}} + protected void RegisterFunctions() + {{~ else ~}} + protected sealed override void RegisterFunctions() + {{~ end ~}} + { + {{~ for member in module.Members ~}} + {{~ if member.IsConst ~}} + RegisterConstant("{{ member.Name }}", {{ member.CSharpName }}); + {{~ else if member.IsFunc ~}} + RegisterFunction("{{ member.Name }}", {{member.Cast}}{{ member.CSharpName }}); + {{~ else if member.IsAction ~}} + RegisterAction("{{ member.Name }}", {{member.Cast}}{{ member.CSharpName }}); + {{~ end ~}} + {{~ end ~}} + } + {{~ end ~}} + } +} +{{~ end ~}} diff --git a/src/RepoM.ActionMenu.CodeGen/Templates/Module.scriban-cs b/src/RepoM.ActionMenu.CodeGen/Templates/Module.scriban-cs new file mode 100644 index 00000000..6c1c6f75 --- /dev/null +++ b/src/RepoM.ActionMenu.CodeGen/Templates/Module.scriban-cs @@ -0,0 +1,58 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable + +using System; +{{~ func GenerateDocDescriptor(item) ~}} + { + var descriptor = {{ if item.IsModule }}Descriptor{{ else }}Descriptors["{{ item.Name }}"]{{ end }}; + descriptor.Category = "{{ item.Category }}"; + descriptor.Description = @"{{ item.Description | string.replace '"' '""' }}"; + descriptor.IsCommand = {{ item.IsCommand }}; + {{~ for param in item.Params ~}} + descriptor.Params.Add(new KalkParamDescriptor("{{ param.Name }}", @"{{ param.Description | string.replace '"' '""' }}") { IsOptional = {{ param.IsOptional }} }); + {{~ end ~}} + {{~ if item.Returns ~}} + descriptor.Returns = @"{{ item.Returns | string.replace '"' '""' }}"; + {{~ end ~}} + {{~ if item.Remarks ~}} + descriptor.Remarks = @"{{ item.Remarks | string.replace '"' '""' }}"; + {{~ end ~}} + {{~ if item.Example ~}} + descriptor.Example = @"{{ item.Example | string.replace '"' '""' }}"; + {{~ end ~}} + } +{{~ end ~}} +{{~ for module in modules ~}} + +namespace {{ module.Namespace }} +{ + partial class {{ module.ClassName }} + { + {{~ if module.Name != 'All' ~}} + {{~ if module.ClassName == 'KalkEngine' ~}} + protected void RegisterFunctions() + {{~ else ~}} + protected sealed override void RegisterFunctions() + {{~ end ~}} + { + {{~ for member in module.Members ~}} + {{~ if member.IsConst ~}} + RegisterConstant("{{ member.Name }}", {{ member.CSharpName }}); + {{~ else if member.IsFunc ~}} + RegisterFunction("{{ member.Name }}", {{member.Cast}}{{ member.CSharpName }}); + {{~ else if member.IsAction ~}} + RegisterAction("{{ member.Name }}", {{member.Cast}}{{ member.CSharpName }}); + {{~ end ~}} + {{~ end ~}} + } + {{~ end ~}} + } +} +{{~ end ~}} diff --git a/src/RepoM.ActionMenu.Core/Misc/FastStack.cs b/src/RepoM.ActionMenu.Core/Misc/FastStack.cs new file mode 100644 index 00000000..34addc35 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Misc/FastStack.cs @@ -0,0 +1,84 @@ +namespace RepoM.ActionMenu.Core.Misc; + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +/// +/// Lightweight stack object. +/// +/// Type of the object +internal struct FastStack +{ + private const int DEFAULT_CAPACITY = 4; + private T[] _array; // Storage for stack elements. + private int _size; // Number of items in the stack. + + // Create a stack with a specific initial capacity. The initial capacity + // must be a non-negative number. + public FastStack(int capacity) + { + if (capacity <= 0) + { + throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be > 0"); + } + + _array = new T[capacity]; + _size = 0; + } + + public int Count => _size; + + public T[] Items => _array; + + // Removes all Objects from the Stack. + public void Clear() + { + // Don't need to doc this but we clear the elements so that the gc can reclaim the references. + Array.Clear(_array, 0, _size); + _size = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T Peek() + { + if (_size == 0) + { + ThrowForEmptyStack(); + } + + return _array[_size - 1]; + } + + // Pops an item from the top of the stack. If the stack is empty, Pop + // throws an InvalidOperationException. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T Pop() + { + if (_size == 0) + { + ThrowForEmptyStack(); + } + + T item = _array[--_size]; + _array[_size] = default(T); // Free memory quicker. + return item; + } + + // Pushes an item to the top of the stack. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Push(T item) + { + if (_size == _array.Length) + { + Array.Resize(ref _array, (_array.Length == 0) ? DEFAULT_CAPACITY : 2 * _array.Length); + } + _array[_size++] = item; + } + + private void ThrowForEmptyStack() + { + Debug.Assert(_size == 0); + throw new InvalidOperationException("Stack is empty"); + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Misc/FixedTemplateParser.cs b/src/RepoM.ActionMenu.Core/Misc/FixedTemplateParser.cs new file mode 100644 index 00000000..40b6c702 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Misc/FixedTemplateParser.cs @@ -0,0 +1,29 @@ +namespace RepoM.ActionMenu.Core.Misc; + +using RepoM.ActionMenu.Core.Model; +using Scriban; +using Scriban.Parsing; + +internal class FixedTemplateParser : ITemplateParser +{ + private static readonly ParserOptions _parserOptions = new() + { + ExpressionDepthLimit = 100, + LiquidFunctionsToScriban = false, + ParseFloatAsDecimal = default, + }; + + public Template ParseScriptOnly(string text) + { + var template = Template.Parse(text, sourceFilePath: null!, _parserOptions, Lexers.ScriptOnly); + template.ThrowOnError(); + return template; + } + + public Template ParseMixed(string text) + { + var template = Template.Parse(text, sourceFilePath: null!, _parserOptions, Lexers.Mixed); + template.ThrowOnError(); + return template; + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Misc/ITemplateParser.cs b/src/RepoM.ActionMenu.Core/Misc/ITemplateParser.cs new file mode 100644 index 00000000..8af338b2 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Misc/ITemplateParser.cs @@ -0,0 +1,10 @@ +namespace RepoM.ActionMenu.Core.Misc; + +using Scriban; + +internal interface ITemplateParser +{ + Template ParseScriptOnly(string text); + + Template ParseMixed(string text); +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Misc/TemplateExtensions.cs b/src/RepoM.ActionMenu.Core/Misc/TemplateExtensions.cs new file mode 100644 index 00000000..8abdae76 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Misc/TemplateExtensions.cs @@ -0,0 +1,18 @@ +// ReSharper disable once CheckNamespace, Justification: Extension methods +namespace Scriban; + +using System; + +internal static class TemplateExtensions +{ + public static void ThrowOnError(this Template template) + { + if (template.HasErrors) + { + throw new Exception("Template has errors"); + } + + // todo + // string.Join(',', template.Messages) + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Model/ActionMenuGenerationContext.cs b/src/RepoM.ActionMenu.Core/Model/ActionMenuGenerationContext.cs new file mode 100644 index 00000000..cb0e2b55 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Model/ActionMenuGenerationContext.cs @@ -0,0 +1,244 @@ +namespace RepoM.ActionMenu.Core.Model; + +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Linq; +using System.Threading.Tasks; +using RepoM.ActionMenu.Core.Misc; +using RepoM.ActionMenu.Core.Model.Env; +using RepoM.ActionMenu.Core.Model.Functions; +using RepoM.ActionMenu.Core.Yaml.Model; +using RepoM.ActionMenu.Core.Yaml.Model.ActionMenus; +using RepoM.ActionMenu.Core.Yaml.Model.ActionMenus.AssociateFile; +using RepoM.ActionMenu.Core.Yaml.Model.ActionMenus.BrowseRepository; +using RepoM.ActionMenu.Core.Yaml.Model.ActionMenus.Command; +using RepoM.ActionMenu.Core.Yaml.Model.ActionMenus.Folder; +using RepoM.ActionMenu.Core.Yaml.Model.ActionMenus.ForEach; +using RepoM.ActionMenu.Core.Yaml.Model.ActionMenus.JustText; +using RepoM.ActionMenu.Core.Yaml.Model.Ctx; +using RepoM.ActionMenu.Core.Yaml.Model.Ctx.EvaluateVariable; +using RepoM.ActionMenu.Core.Yaml.Model.Ctx.ExecuteScript; +using RepoM.ActionMenu.Core.Yaml.Model.Ctx.LoadFile; +using RepoM.ActionMenu.Core.Yaml.Model.Ctx.RendererVariable; +using RepoM.ActionMenu.Core.Yaml.Model.Ctx.SetVariable; +using RepoM.ActionMenu.Core.Yaml.Model.Tags; +using RepoM.ActionMenu.Core.Yaml.Serialization; +using RepoM.ActionMenu.Interface.ActionMenuFactory; +using RepoM.ActionMenu.Interface.Scriban; +using RepoM.ActionMenu.Interface.UserInterface; +using RepoM.ActionMenu.Interface.YamlModel; +using RepoM.Core.Plugin.Repository; +using Scriban; + +internal class ActionMenuGenerationContext : TemplateContext, IActionMenuGenerationContext, IContextMenuActionMenuGenerationContext +{ + private readonly ITemplateParser _templateParser; + private readonly ITemplateContextRegistration[] _functionsArray; + private readonly ActionMenuDeserializer _deserializer = new(); + private readonly List _repositoryActionMappers; + private readonly List _contextActionMappers; + + public ActionMenuGenerationContext( + IRepository repository, + ITemplateParser templateParser, + IFileSystem fileSystem, + ITemplateContextRegistration[] functionsArray) + { + _templateParser = templateParser ?? throw new ArgumentNullException(nameof(templateParser)); + _functionsArray = functionsArray ?? throw new ArgumentNullException(nameof(functionsArray)); + FileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + Repository = repository ?? throw new ArgumentNullException(nameof(repository)); + + _repositoryActionMappers = new List() + { + new ActionAssociateFileV1Mapper(), + new RepositoryActionJustTextV1Mapper(), + new RepositoryActionFolderV1Mapper(), + new BrowseRepositoryV1Mapper(), + new ActionCommandV1Mapper(), + new RepositoryActionForEachV1Mapper(), + }; + + var rootScriptObject = new RepoMScriptObject(); + + rootScriptObject.SetValue("file", new FileFunctions(fileSystem), true); + + + rootScriptObject.Add("repository", new RepositoryFunctions(Repository)); + rootScriptObject.SetReadOnly("repository", true); + + Env = new EnvSetScriptObject(EnvScriptObject.Create()); + rootScriptObject.Add("env", Env); + rootScriptObject.SetReadOnly("env", true); + + foreach (var x in _functionsArray) + { + x.RegisterFunctionsAuto(rootScriptObject); + } + + + PushGlobal(rootScriptObject); + + _contextActionMappers = new List + { + new ExecuteScriptContextActionMapper(), + new SetVariableActionContextActionMapper(), + new EvaluateVariableActionContextActionMapper(), + new RenderVariableActionContextActionMapper(), + new LoadFileContextActionContextActionMapper(FileSystem, _deserializer), + }; + + RepositoryActionsScriptContext = new DisposableContextScriptObject(this, Env, _contextActionMappers); + PushGlobal(RepositoryActionsScriptContext); + } + + public IFileSystem FileSystem { private set; get; } + + public DisposableContextScriptObject RepositoryActionsScriptContext { get; private set; } + + public IRepository Repository { get; } + + public EnvSetScriptObject Env { get; private set; } + + public async Task AddRepositoryContextAsync(Context? reposContext) + { + if (reposContext == null) + { + return; + } + + foreach (var contextAction in reposContext) + { + await RepositoryActionsScriptContext.AddContextActionAsync(contextAction).ConfigureAwait(false); + } + } + + public async Task> AddActionMenusAsync(List? menus) + { + if (menus == null) + { + return Array.Empty(); + } + + var items = new List(); + + using var disposable = CreateGlobalScope(); + + foreach (var action in menus) + { + foreach (var item in await AddMenuActionAsync(action).ConfigureAwait(false)) + { + items.Add(item); + } + } + + return items; + } + + public IActionMenuGenerationContext Clone() + { + throw new NotImplementedException("Not a full clone."); + var result = new ActionMenuGenerationContext(Repository, _templateParser, FileSystem, _functionsArray) + { + Env = (EnvSetScriptObject)Env.Clone(true), + RepositoryActionsScriptContext = (DisposableContextScriptObject)RepositoryActionsScriptContext.Clone(true) + }; + // todo more + return result; + } + + private async Task> AddMenuActionAsync(IMenuAction menuAction) + { + if (!await IsMenuItemActiveAsync(menuAction)) + { + return Array.Empty(); + } + + using var variableContext = PushNewContext(); + + if (menuAction is IContext { Context: not null } c) + { + foreach (var ctx in c.Context) + { + await variableContext.AddContextActionAsync(ctx).ConfigureAwait(false); + } + } + + var mapper = _repositoryActionMappers.Find(mapper => mapper.CanMap(menuAction)); + if (mapper == null) + { + // throw? + return Array.Empty(); + } + + var items = new List(); + await foreach (var item in mapper.MapAsync(menuAction, this, Repository).ConfigureAwait(false)) + { + items.Add(item); + } + + return items; + } + + public async Task RenderStringAsync(string text) + { + var template = _templateParser.ParseMixed(text); + return await template.RenderAsync(this).ConfigureAwait(false); + } + + private DisposableContextScriptObject PushNewContext() + { + return new DisposableContextScriptObject(this, Env, _contextActionMappers); + } + + public async Task EvaluateAsync(string? text) + { + if (text == null) + { + return null!; + } + + var template = _templateParser.ParseScriptOnly(text); + return await template.EvaluateAsync(this).ConfigureAwait(false); + } + + private Task IsMenuItemActiveAsync(IMenuAction menuAction) + { + return this.EvaluateToBooleanAsync(menuAction.Active, true); + } + + public IScope CreateGlobalScope() + { + return PushNewContext(); + } + + internal async Task LoadAsync(string filename) + { + var yaml = await FileSystem.File.ReadAllTextAsync(filename).ConfigureAwait(false); + var actions = _deserializer.DeserializeRoot(yaml); + return actions ?? throw new NotImplementedException("Could not deserialize file"); + } + + public async Task> GetTagsAsync(Tags taqs) + { + if (taqs == null) + { + return Array.Empty(); + } + + var items = new List(Tags.Count); + + using var disposable = CreateGlobalScope(); + + foreach (var tag in taqs) + { + if (await tag.When.EvaluateAsync(this).ConfigureAwait(false)) + { + items.Add(tag.Tag); + } + } + + return items.Distinct(); + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Model/DisposableContextScriptObject.cs b/src/RepoM.ActionMenu.Core/Model/DisposableContextScriptObject.cs new file mode 100644 index 00000000..2c9ec2f8 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Model/DisposableContextScriptObject.cs @@ -0,0 +1,78 @@ +namespace RepoM.ActionMenu.Core.Model; + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using RepoM.ActionMenu.Core.Model.Env; +using RepoM.ActionMenu.Core.Yaml.Model.Ctx; +using RepoM.ActionMenu.Interface.ActionMenuFactory; +using RepoM.ActionMenu.Interface.YamlModel; +using Scriban.Runtime; + +internal sealed class DisposableContextScriptObject : ScriptObject, IScope +{ + private readonly ActionMenuGenerationContext _context; + private readonly EnvSetScriptObject _envSetScriptObject; + private readonly List _mappers; + private int _envCounter; + + internal DisposableContextScriptObject(ActionMenuGenerationContext context, EnvSetScriptObject envSetScriptObject, List mappers) + { + _envCounter = 0; + _context = context ?? throw new ArgumentNullException(nameof(context)); + _envSetScriptObject = envSetScriptObject ?? throw new ArgumentNullException(nameof(envSetScriptObject)); + _mappers = mappers; + _context.PushGlobal(this); + } + + public async Task AddContextActionAsync(IContextAction contextItem) + { + var enabled = await IsActionEnabled(contextItem).ConfigureAwait(false); + if (!enabled) + { + return; + } + + var mapper = _mappers.Find(mapper => mapper.CanMap(contextItem)); + if (mapper == null) + { + throw new Exception("Cannot find mapper"); + } + + await mapper.MapAsync(contextItem, _context, this).ConfigureAwait(false); + } + + public void Dispose() + { + if (_envCounter != 0) + { + while (_envCounter > 0) + { + _ = _envSetScriptObject.Pop(); + _envCounter--; + } + } + + if (_context.PopGlobal() != this) + { + throw new Exception("Popped wrong script object"); + } + } + + public void PushEnvironmentVariable(Dictionary envVars) + { + _envSetScriptObject.Push(new EnvScriptObject(envVars)); + _envCounter++; + } + + private async Task IsActionEnabled(IContextAction contextItem) + { + // action does not implement interface and is therfore always enabled. + if (contextItem is not IEnabled ea) + { + return true; + } + + return await _context.EvaluateToBooleanAsync(ea.Enabled, true).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Model/Env/EnvScriptObject.cs b/src/RepoM.ActionMenu.Core/Model/Env/EnvScriptObject.cs new file mode 100644 index 00000000..6deccfda --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Model/Env/EnvScriptObject.cs @@ -0,0 +1,97 @@ +namespace RepoM.ActionMenu.Core.Model.Env; + +using System; +using System.Collections; +using System.Collections.Generic; +using Scriban; +using Scriban.Parsing; +using Scriban.Runtime; + +internal sealed class EnvScriptObject : IScriptObject +{ + private readonly Dictionary _env; + + public static EnvScriptObject Create() + { + var env = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (DictionaryEntry item in Environment.GetEnvironmentVariables()) + { + if (item.Key is not string key || string.IsNullOrEmpty(key)) + { + continue; + } + + if (item.Value is not string value) + { + continue; + } + + env.Add(key.Trim(), value); + } + + return new EnvScriptObject(env); + } + + public EnvScriptObject(Dictionary envVars) + { + _env = envVars; + } + + public IEnumerable GetMembers() + { + return _env.Keys; + } + + public bool Contains(string member) + { + return _env.ContainsKey(member); + } + + public bool TryGetValue(TemplateContext context, SourceSpan span, string member, out object value) + { + if (_env.TryGetValue(member, out string? s)) + { + value = s; + return true; + } + else + { + value = new object(); + return false; + } + } + + public bool CanWrite(string member) + { + return false; + } + + public bool TrySetValue(TemplateContext context, SourceSpan span, string member, object value, bool readOnly) + { + return false; + } + + public bool Remove(string member) + { + return false; + } + + public void SetReadOnly(string member, bool readOnly) + { + // intentionally do nothing + } + + public IScriptObject Clone(bool deep) + { + return new EnvScriptObject(new Dictionary(_env)); + } + + public int Count => _env.Count; + + public bool IsReadOnly + { + get => true; + set => _ = value; + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Model/Env/EnvSetScriptObject.cs b/src/RepoM.ActionMenu.Core/Model/Env/EnvSetScriptObject.cs new file mode 100644 index 00000000..c8f9d018 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Model/Env/EnvSetScriptObject.cs @@ -0,0 +1,117 @@ +namespace RepoM.ActionMenu.Core.Model.Env; + +using System; +using System.Collections.Generic; +using System.Linq; +using RepoM.ActionMenu.Core.Misc; +using Scriban; +using Scriban.Parsing; +using Scriban.Runtime; + +internal sealed class EnvSetScriptObject : IScriptObject, IDisposable +{ + private FastStack _stack = new(10); + + public EnvSetScriptObject(EnvScriptObject @base) + { + _ = @base ?? throw new ArgumentNullException(nameof(@base)); + _stack.Push(@base); + } + + public void Push(EnvScriptObject item) + { + _stack.Push(item); + } + + public EnvScriptObject Pop() + { + return _stack.Pop(); + } + + public IEnumerable GetMembers() + { + return _stack.Items.SelectMany(x => x.GetMembers()).Distinct(); + } + + public bool Contains(string member) + { + return GetMembers().Contains(member); + } + + public bool TryGetValue(TemplateContext context, SourceSpan span, string member, out object value) + { + EnvScriptObject? result = GetFirstForMember(member); + + if (result == null) + { + value = new object(); + return false; + } + + return result.TryGetValue(context, span, member, out value); + } + + public bool CanWrite(string member) + { + return false; + } + + public bool TrySetValue(TemplateContext context, SourceSpan span, string member, object value, bool readOnly) + { + return false; + } + + public bool Remove(string member) + { + return false; + } + + public void SetReadOnly(string member, bool readOnly) + { + // intentionally do nothing + } + + public IScriptObject Clone(bool deep) + { + var items = _stack.Items; + var result = new EnvSetScriptObject((EnvScriptObject)items[0].Clone(true)); + + if (items.Length > 1) + { + for (int i = 1; i < items.Length; i++) + { + result.Push((EnvScriptObject)items[i].Clone(true)); + } + } + + return result; + } + + public int Count => GetMembers().Count(); + + public bool IsReadOnly + { + get => true; + set => _ = value; + } + + public void Dispose() + { + _stack.Clear(); + } + + private EnvScriptObject? GetFirstForMember(string member) + { + var count = _stack.Count; + var items = _stack.Items; + for (var i = count - 1; i >= 0; i--) + { + if (items[i].Contains(member)) + { + return items[i]; + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Model/Functions/FileFunctions.cs b/src/RepoM.ActionMenu.Core/Model/Functions/FileFunctions.cs new file mode 100644 index 00000000..5fa5885c --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Model/Functions/FileFunctions.cs @@ -0,0 +1,79 @@ +namespace RepoM.ActionMenu.Core.Model.Functions; + +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.Linq; +using RepoM.ActionMenu.Interface.ActionMenuFactory; +using RepoM.ActionMenu.Interface.Attributes; +using Scriban.Parsing; +using Scriban.Syntax; + +[ActionMenuModule("File")] +internal partial class FileFunctions : ScribanModuleWithFunctions +{ + private readonly IFileSystem _fileSystem; + + public FileFunctions(IFileSystem fileSystem) + { + _fileSystem = fileSystem; + RegisterFunctions(); + } + + + /// + /// Find files in a given directory based on the search pattern. Resulting filenames are absolute path based. + /// + /// The root folder. + /// The search string to match against the names of directories. This parameter can contain a combination of valid literal path and wildcard (`*` and `?`) characters, but it doesn't support regular expressions. + /// Returns an enumerable collection of full paths of the files or directories that matches the specified search pattern. + /// first + /// + /// second + /// + /// find_files 'C:\Users\coenm\RepoM\src' '*.sln' + /// + /// + /// xxx + /// sdf + /// + /// ```scribanhtml + /// find_files 'C:\Users\coenm\RepoM\src' '*.csproj' + /// ``` + /// ```html + /// [1, 2, 3, 4] + /// ``` + /// + [ActionMenuMember("find_files")] + public static string[] FindFiles(ActionMenuGenerationContext context, SourceSpan span, string rootPath, string searchPattern) + { + return FindFilesUsingInterface(context, span, rootPath, searchPattern); + } + + /// + [ActionMenuMember("find_files_interface")] + public static string[] FindFilesUsingInterface(IMenuContext context, SourceSpan span, string rootPath, string searchPattern) + { + try + { + return GetFileEnumerator(context.FileSystem, rootPath, searchPattern).ToArray(); + } + catch (Exception e) + { + throw new ScriptRuntimeException(span, "Could not get files.", e); + } + } + + private static IEnumerable GetFileEnumerator(IFileSystem fileSystem, string path, string searchPattern) + { + // prefer EnumerateFileSystemInfos() over EnumerateFiles() to include packaged folders like + // .app or .xcodeproj on macOS + IDirectoryInfo directory = fileSystem.DirectoryInfo.New(path); + + return directory + .EnumerateFileSystemInfos(searchPattern, SearchOption.AllDirectories) + .Select(f => f.FullName) + .Where(f => !f.StartsWith('.')); + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Model/Functions/FileFunctionsKalk.cs b/src/RepoM.ActionMenu.Core/Model/Functions/FileFunctionsKalk.cs new file mode 100644 index 00000000..cee2d161 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Model/Functions/FileFunctionsKalk.cs @@ -0,0 +1,76 @@ +namespace RepoM.ActionMenu.Core.Model.Functions; + +using System; +using RepoM.ActionMenu.Interface.ActionMenuFactory; +using RepoM.ActionMenu.Interface.Attributes; + +internal partial class FileFunctions +{ + /// + /// Checks if the specified file path exists on the disk. + /// + /// Absolute path to a file. + /// `true` if the specified file path exists on the disk, `false` otherwise. + /// + /// ```kalk + /// rm "test.txt" + /// file_exists "test.txt" + /// # file_exists("test.txt") + /// out = false + /// save_text("content", "test.txt") + /// file_exists "test.txt" + /// # file_exists("test.txt") + /// out = true + /// ``` + /// + /// + /// dsf + /// + [ActionMenuMember("file_exists")] + public static bool FileExists(IMenuContext context, string path) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + if (string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentOutOfRangeException(nameof(path), "Path cannot be null or empty."); + } + + return context.FileSystem.File.Exists(path); + } + + /// + /// Checks if the specified directory path exists on the disk. + /// + /// Absolute path to a directory. + /// `true` if the specified directory path exists on the disk, `false` otherwise. + /// + /// ```kalk + /// dir_exists "testdir" + /// # dir_exists("testdir") + /// out = true + /// rmdir "testdir" + /// dir_exists "testdir" + /// # dir_exists("testdir") + /// out = false + /// ``` + /// + [ActionMenuMember("dir_exists")] + public static bool DirectoryExists(IMenuContext context, string path) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + if (string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentOutOfRangeException(nameof(path), "Path cannot be null or empty."); + } + + return context.FileSystem.Directory.Exists(path); + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Model/Functions/RepositoryFunctions.cs b/src/RepoM.ActionMenu.Core/Model/Functions/RepositoryFunctions.cs new file mode 100644 index 00000000..21ba6987 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Model/Functions/RepositoryFunctions.cs @@ -0,0 +1,53 @@ +namespace RepoM.ActionMenu.Core.Model.Functions; + +using System; +using System.Collections; +using RepoM.ActionMenu.Interface.Attributes; +using RepoM.Core.Plugin.Repository; + +[ActionMenuModule("Repository")] +internal partial class RepositoryFunctions : ScribanModuleWithFunctions +{ + private readonly IRepository _repository; + + public RepositoryFunctions(IRepository repository) + { + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + RegisterFunctions(); + } + + /// + /// Gets the name of the repository. + /// + /// The name of the repository. + [ActionMenuMember("name", "repo-cat")] + public string Name => _repository.Name; + + /// + /// Gets the path of the repository. + /// + /// The path of the repository. + [ActionMenuMember("path", "repo-cat")] + public string Path => _repository.Path; + + /// + /// Gets the current branch of the repository + /// + /// The name of the current branch. + [ActionMenuMember("branch", "repo-cat")] + public string CurrentBranch => _repository.CurrentBranch; + + /// + /// Gets the current branch of the repository + /// + /// The name of the current branch. + [ActionMenuMember("branches", "repo-cat")] + public IEnumerable Branches => _repository.Branches; + + /// + /// Gets the local branches + /// + /// All local branches. + [ActionMenuMember("local-branches", "repo-cat")] + public IEnumerable LocalBranches => _repository.LocalBranches; +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Model/Functions/StringModule.cs b/src/RepoM.ActionMenu.Core/Model/Functions/StringModule.cs new file mode 100644 index 00000000..dc8260fa --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Model/Functions/StringModule.cs @@ -0,0 +1,387 @@ +namespace RepoM.ActionMenu.Core.Model.Functions +{ + using System.Collections; + using RepoM.ActionMenu.Interface.Attributes; + using Scriban; + using Scriban.Functions; + using Scriban.Runtime; + + /// + /// Modules that provides string functions (e.g `upcase`, `downcase`, `regex_escape`...). + /// + [ActionMenuModule(ModuleName)] + internal partial class StringModule : ScribanModuleWithFunctions + { + private const string ModuleName = "Strings"; + public const string CategoryString = "Text Functions"; + + public StringModule() /*: base(ModuleName)*/ + { + RegisterFunctions(); + } + + /// Escapes a string with escape characters. + /// The input string + /// The two strings concatenated + /// + /// ```kalk + /// >>> "Hel\tlo\n\"W\\orld" |> escape + /// # "Hel\tlo\n\"W\\orld" |> escape + /// out = "Hel\\tlo\\n\\\"W\\\\orld" + /// ``` + /// + [ActionMenuMember("escape", CategoryString)] + public string StringEscape(string text) => StringFunctions.Escape(text); + + /// + /// Converts the first character of the passed string to a upper case character. + /// + /// The input string + /// The capitalized input string + /// + /// ```kalk + /// >>> "test" |> capitalize + /// # "test" |> capitalize + /// out = "Test" + /// ``` + /// + [ActionMenuMember("capitalize", CategoryString)] + public string StringCapitalize(string text) => StringFunctions.Capitalize(text); + + /// + /// Converts the first character of each word in the passed string to a upper case character. + /// + /// The input string + /// The capitalized input string + /// + /// ```kalk + /// >>> "This is easy" |> capitalize_words + /// # "This is easy" |> capitalize_words + /// out = "This Is Easy" + /// ``` + /// + [ActionMenuMember("capitalize_words", CategoryString)] + public string StringCapitalizeWords(string text) => StringFunctions.Capitalizewords(text); + + /// Converts the string to lower case. + /// The input string + /// The input string lower case + /// + /// ```kalk + /// >>> "TeSt" |> downcase + /// # "TeSt" |> downcase + /// out = "test" + /// ``` + /// + [ActionMenuMember("downcase", CategoryString)] + public string StringDowncase(string text) => StringFunctions.Downcase(text); + + /// Converts the string to uppercase + /// The input string + /// The input string upper case + /// + /// ```kalk + /// >>> "test" |> upcase + /// # "test" |> upcase + /// out = "TEST" + /// ``` + /// + [ActionMenuMember("upcase", CategoryString)] + public string StringUpcase(string text) => StringFunctions.Upcase(text); + + /// + /// Returns a boolean indicating whether the input string ends with the specified string `value`. + /// + /// The input string + /// The string to look for + /// true if `text` ends with the specified string `value` + /// + /// ```kalk + /// >>> "This is easy" |> endswith "easy" + /// # "This is easy" |> endswith("easy") + /// out = true + /// >>> "This is easy" |> endswith "none" + /// # "This is easy" |> endswith("none") + /// out = false + /// ``` + /// + [ActionMenuMember("endswith", CategoryString)] + public bool StringEndsWith(string text, string end) => StringFunctions.EndsWith(text, end); + + /// Returns a url handle from the input string. + /// The input string + /// A url handle + /// + /// ```kalk + /// >>> '100% M @ Ms!!!' |> handleize + /// # '100% M @ Ms!!!' |> handleize + /// out = "100-m-ms" + /// ``` + /// + [ActionMenuMember("handleize", CategoryString)] + public string StringHandleize(string text) => StringFunctions.Handleize(text); + + /// + /// Removes any whitespace characters on the **left** side of the input string. + /// + /// The input string + /// The input string without any left whitespace characters + /// + /// ```kalk + /// >>> ' too many spaces' |> lstrip + /// # ' too many spaces' |> lstrip + /// out = "too many spaces" + /// ``` + /// + [ActionMenuMember("lstrip", CategoryString)] + public string StringLeftStrip(string text) => StringFunctions.LStrip(text); + + /// + /// Outputs the singular or plural version of a string based on the value of a number. + /// + /// The number to check + /// The singular string to return if number is == 1 + /// The plural string to return if number is != 1 + /// The singular or plural string based on number + /// + /// ```kalk + /// >>> 3 |> pluralize('product', 'products') + /// # 3 |> pluralize('product', 'products') + /// out = "products" + /// ``` + /// + [ActionMenuMember("pluralize", CategoryString)] + public string StringPluralize(int number, string singular, string plural) => StringFunctions.Pluralize(number, singular, plural); + + /// + /// Removes any whitespace characters on the **right** side of the input string. + /// + /// The input string + /// The input string without any left whitespace characters + /// + /// ```kalk + /// >>> ' too many spaces ' |> rstrip + /// # ' too many spaces ' |> rstrip + /// out = " too many spaces" + /// ``` + /// + [ActionMenuMember("rstrip", CategoryString)] + public string StringRightStrip(string text) => StringFunctions.RStrip(text); + + /// + /// The `split` function takes on a substring as a parameter. + /// The substring is used as a delimiter to divide a string into an array. You can output different parts of an array using `array` functions. + /// + /// The input string + /// The string used to split the input `text` string + /// An enumeration of the substrings + /// + /// ```kalk + /// >>> "Hi, how are you today?" |> split ' ' + /// # "Hi, how are you today?" |> split(' ') + /// out = ["Hi,", "how", "are", "you", "today?"] + /// ``` + /// + [ActionMenuMember("split", CategoryString)] + public IEnumerable StringSplit(string text, string match) => StringFunctions.Split(text, match); + + /// + /// Returns a boolean indicating whether the input string starts with the specified string `value`. + /// + /// The input string + /// The string to look for + /// true if `text` starts with the specified string `value` + /// + /// ```kalk + /// >>> "This is easy" |> startswith "This" + /// # "This is easy" |> startswith("This") + /// out = true + /// >>> "This is easy" |> startswith "easy" + /// # "This is easy" |> startswith("easy") + /// out = false + /// ``` + /// + [ActionMenuMember("startswith", CategoryString)] + public bool StringStartsWith(string text, string start) => StringFunctions.StartsWith(text, start); + + /// + /// Removes any whitespace characters on the **left** and **right** side of the input string. + /// + /// The input string + /// The input string without any left and right whitespace characters + /// + /// ```kalk + /// >>> ' too many spaces ' |> strip + /// # ' too many spaces ' |> strip + /// out = "too many spaces" + /// ``` + /// + [ActionMenuMember("strip", CategoryString)] + public string StringStrip(string text) => StringFunctions.Strip(text); + + /// Removes any line breaks/newlines from a string. + /// The input string + /// The input string without any breaks/newlines characters + /// + /// ```kalk + /// >>> "This is a string.\r\n With \nanother \rstring" |> strip_newlines + /// # "This is a string.\r\n With \nanother \rstring" |> strip_newlines + /// out = "This is a string. With another string" + /// ``` + /// + [ActionMenuMember("strip_newlines", CategoryString)] + public string StringStripNewlines(string text) => StringFunctions.StripNewlines(text); + + /// + /// Pads a string with leading spaces to a specified total length. + /// + /// The input string + /// The number of characters in the resulting string + /// The input string padded + /// + /// ```kalk + /// >>> "world" |> pad_left 10 + /// # "world" |> pad_left(10) + /// out = " world" + /// ``` + /// + [ActionMenuMember("pad_left", CategoryString)] + public string StringPadLeft(string text, int width) => StringFunctions.PadLeft(text, width); + + /// + /// Pads a string with trailing spaces to a specified total length. + /// + /// The input string + /// The number of characters in the resulting string + /// The input string padded + /// + /// ```kalk + /// >>> "hello" |> pad_right 10 + /// # "hello" |> pad_right(10) + /// out = "hello " + /// ``` + /// + [ActionMenuMember("pad_right", CategoryString)] + public string StringPadRight(string text, int width) => StringFunctions.PadRight(text, width); + + /// + /// Escapes a minimal set of characters (`\`, `*`, `+`, `?`, `|`, `{`, `[`, `(`,`)`, `^`, `$`,`.`, `#`, and white space) + /// by replacing them with their escape codes. + /// This instructs the regular expression engine to interpret these characters literally rather than as metacharacters. + /// + /// The input string that contains the text to convert. + /// A string of characters with metacharacters converted to their escaped form. + /// + /// ```kalk + /// >>> "(abc.*)" |> regex_escape + /// # "(abc.*)" |> regex_escape + /// out = "\\(abc\\.\\*\\)" + /// ``` + /// + [ActionMenuMember("regex_escape", CategoryString)] + public string RegexEscape(string text) => RegexFunctions.Escape(text); + + /// + /// Searches an input string for a substring that matches a regular expression pattern and returns an array with the match occurences. + /// + /// The string to search for a match. + /// The regular expression pattern to match. + /// A string with regex options, that can contain the following option characters (default is `null`): + /// - `i`: Specifies case-insensitive matching. + /// - `m`: Multiline mode. Changes the meaning of `^` and `$` so they match at the beginning and end, respectively, of any line, and not just the beginning and end of the entire string. + /// - `s`: Specifies single-line mode. Changes the meaning of the dot `.` so it matches every character (instead of every character except `\n`). + /// - `x`: Eliminates unescaped white space from the pattern and enables comments marked with `#`. + /// + /// An array that contains all the match groups. The first group contains the entire match. The other elements contain regex matched groups `(..)`. An empty array returned means no match. + /// + /// ```kalk + /// >>> "this is a text123" |> regex_match `(\w+) a ([a-z]+\d+)` + /// # "this is a text123" |> regex_match(`(\w+) a ([a-z]+\d+)`) + /// out = ["is a text123", "is", "text123"] + /// ``` + /// + [ActionMenuMember("regex_match", CategoryString)] + public ScriptArray RegexMatch(TemplateContext context, string text, string pattern, string options = null) => RegexFunctions.Match(context, text, pattern, options); + + /// + /// Searches an input string for multiple substrings that matches a regular expression pattern and returns an array with the match occurences. + /// + /// The template context (to fetch the timeout configuration) + /// The string to search for a match. + /// The regular expression pattern to match. + /// A string with regex options, that can contain the following option characters (default is `null`): + /// - `i`: Specifies case-insensitive matching. + /// - `m`: Multiline mode. Changes the meaning of `^` and `$` so they match at the beginning and end, respectively, of any line, and not just the beginning and end of the entire string. + /// - `s`: Specifies single-line mode. Changes the meaning of the dot `.` so it matches every character (instead of every character except `\n`). + /// - `x`: Eliminates unescaped white space from the pattern and enables comments marked with `#`. + /// + /// An array of matches that contains all the match groups. The first group contains the entire match. The other elements contain regex matched groups `(..)`. An empty array returned means no match. + /// + /// ```kalk + /// >>> "this is a text123" |> regex_matches `(\w+)` + /// # "this is a text123" |> regex_matches(`(\w+)`) + /// out = [["this", "this"], ["is", "is"], ["a", "a"], ["text123", "text123"]] + /// ``` + /// + [ActionMenuMember("regex_matches", CategoryString)] + public ScriptArray RegexMatches(TemplateContext context, string text, string pattern, string? options = null) => RegexFunctions.Matches(context, text, pattern, options); + + /// + /// In a specified input string, replaces strings that match a regular expression pattern with a specified replacement string. + /// + /// The string to search for a match. + /// The regular expression pattern to match. + /// The replacement string. + /// A string with regex options, that can contain the following option characters (default is `null`): + /// - `i`: Specifies case-insensitive matching. + /// - `m`: Multiline mode. Changes the meaning of `^` and `$` so they match at the beginning and end, respectively, of any line, and not just the beginning and end of the entire string. + /// - `s`: Specifies single-line mode. Changes the meaning of the dot `.` so it matches every character (instead of every character except `\n`). + /// - `x`: Eliminates unescaped white space from the pattern and enables comments marked with `#`. + /// + /// A new string that is identical to the input string, except that the replacement string takes the place of each matched string. If pattern is not matched in the current instance, the method returns the current instance unchanged. + /// + /// ```kalk + /// >>> "abbbbcccd" |> regex_replace("b+c+","-Yo-") + /// # "abbbbcccd" |> regex_replace("b+c+", "-Yo-") + /// out = "a-Yo-d" + /// ``` + /// + [ActionMenuMember("regex_replace", CategoryString)] + public string RegexReplace(TemplateContext context, string text, string pattern, string replace, string options = null) => RegexFunctions.Replace(context, text, pattern, replace, options); + + /// + /// Splits an input string into an array of substrings at the positions defined by a regular expression match. + /// + /// The string to split. + /// The regular expression pattern to match. + /// A string with regex options, that can contain the following option characters (default is `null`): + /// - `i`: Specifies case-insensitive matching. + /// - `m`: Multiline mode. Changes the meaning of `^` and `$` so they match at the beginning and end, respectively, of any line, and not just the beginning and end of the entire string. + /// - `s`: Specifies single-line mode. Changes the meaning of the dot `.` so it matches every character (instead of every character except `\n`). + /// - `x`: Eliminates unescaped white space from the pattern and enables comments marked with `#`. + /// + /// A string array. + /// + /// ```kalk + /// >>> "a, b , c, d" |> regex_split `\s*,\s*` + /// # "a, b , c, d" |> regex_split(`\s*,\s*`) + /// out = ["a", "b", "c", "d"] + /// ``` + /// + [ActionMenuMember("regex_split", CategoryString)] + public ScriptArray RegexSplit(TemplateContext context, string text, string pattern, string options = null) => RegexFunctions.Split(context, text, pattern, options); + + /// Converts any escaped characters in the input string. + /// The input string containing the text to convert. + /// A string of characters with any escaped characters converted to their unescaped form. + /// + /// ```kalk + /// >>> "\\(abc\\.\\*\\)" |> regex_unescape + /// # "\\(abc\\.\\*\\)" |> regex_unescape + /// out = "(abc.*)" + /// ``` + /// + [ActionMenuMember("regex_unescape", CategoryString)] + public string RegexUnescape(string text) => RegexFunctions.Unescape(text); + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Model/IActionMenuDeserializer.cs b/src/RepoM.ActionMenu.Core/Model/IActionMenuDeserializer.cs new file mode 100644 index 00000000..af2c6b7b --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Model/IActionMenuDeserializer.cs @@ -0,0 +1,10 @@ +namespace RepoM.ActionMenu.Core.Model; + +using RepoM.ActionMenu.Core.Yaml.Model; + +internal interface IActionMenuDeserializer +{ + Root DeserializeRoot(string content); + + ContextRoot DeserializeContextRoot(string content); +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Model/Lexers.cs b/src/RepoM.ActionMenu.Core/Model/Lexers.cs new file mode 100644 index 00000000..dce79d7f --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Model/Lexers.cs @@ -0,0 +1,19 @@ +namespace RepoM.ActionMenu.Core.Model; + +using Scriban.Parsing; + +internal static class Lexers +{ + public static readonly LexerOptions ScriptOnly = new() + { + Lang = ScriptLang.Default, + Mode = ScriptMode.ScriptOnly, + }; + + public static readonly LexerOptions Mixed = new() + { + FrontMatterMarker = LexerOptions.DefaultFrontMatterMarker, + Lang = ScriptLang.Default, + Mode = ScriptMode.Default, + }; +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Model/RepoMScriptObject.cs b/src/RepoM.ActionMenu.Core/Model/RepoMScriptObject.cs new file mode 100644 index 00000000..c452eb56 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Model/RepoMScriptObject.cs @@ -0,0 +1,131 @@ +namespace RepoM.ActionMenu.Core.Model; + +using System; +using RepoM.ActionMenu.Interface.Scriban; +using Scriban.Runtime; + +internal class RepoMScriptObject : ScriptObject, IContextRegistration +{ + IContextRegistration IContextRegistration.CreateOrGetSubRegistration(string key) + { + if (this.TryGetValue(key, out object value)) + { + if (value is IContextRegistration cr) + { + return cr; + } + + throw new Exception($"Object registered as '{key}' is does not implement '{nameof(IContextRegistration)}'"); + } + + var sub = new RepoMScriptObject(); + SetValue(key, sub, false); + return sub; + } + + void IContextRegistration.SetValue(string member, object value, bool readOnly) + { + SetValue(member, value, readOnly); + } + + void IContextRegistration.Add(string key, object value) + { + Add(key, value); + } + + bool IContextRegistration.ContainsKey(string key) + { + return ContainsKey(key); + } + + void IContextRegistration.RegisterConstant(string name, object value) + { + RegisterVariableInner(name, value); + } + + void IContextRegistration.RegisterAction(string name, Action action) + { + RegisterCustomFunction(name, DelegateCustomFunction.Create(action)); + } + + void IContextRegistration.RegisterAction(string name, Action action) + { + RegisterCustomFunction(name, DelegateCustomFunction.Create(action)); + } + + void IContextRegistration.RegisterAction(string name, Action action) + { + RegisterCustomFunction(name, DelegateCustomFunction.Create(action)); + } + + void IContextRegistration.RegisterAction(string name, Action action) + { + RegisterCustomFunction(name, DelegateCustomFunction.Create(action)); + } + + void IContextRegistration.RegisterAction(string name, Action action) + { + RegisterCustomFunction(name, DelegateCustomFunction.Create(action)); + } + + void IContextRegistration.RegisterFunction(string name, Func func) + { + RegisterCustomFunction(name, DelegateCustomFunction.CreateFunc(func)); + } + + void IContextRegistration.RegisterFunction(string name, Func func) + { + RegisterCustomFunction(name, DelegateCustomFunction.CreateFunc(func)); + } + + void IContextRegistration.RegisterFunction(string name, Func func) + { + RegisterCustomFunction(name, DelegateCustomFunction.CreateFunc(func)); + } + + void IContextRegistration.RegisterFunction(string name, Func func) + { + RegisterCustomFunction(name, DelegateCustomFunction.CreateFunc(func)); + } + + void IContextRegistration.RegisterFunction(string name, Func func) + { + RegisterCustomFunction(name, DelegateCustomFunction.CreateFunc(func)); + } + + void IContextRegistration.RegisterFunction(string name, Func func) + { + RegisterCustomFunction(name, DelegateCustomFunction.CreateFunc(func)); + } + + void IContextRegistration.RegisterVariable(string name, object value) + { + RegisterVariableInner(name, value); + } + + private void RegisterCustomFunction(string name, IScriptCustomFunction func) + { + RegisterVariableInner(name, func); + } + + private void RegisterVariableInner(string name, object value) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + var names = name.Split(','); + + + foreach (var subName in names) + { + SetValue(subName, value, true); + } + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Model/ScribanModuleWithFunctions.cs b/src/RepoM.ActionMenu.Core/Model/ScribanModuleWithFunctions.cs new file mode 100644 index 00000000..442eeced --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Model/ScribanModuleWithFunctions.cs @@ -0,0 +1,180 @@ +namespace RepoM.ActionMenu.Core.Model; + +using System; +using RepoM.ActionMenu.Interface.ActionMenuFactory; +using Scriban; +using Scriban.Parsing; +using Scriban.Runtime; + +internal abstract class ScribanModuleWithFunctions : RepoMScriptObject +{ + // protected KalkModuleWithFunctions() : this(null) + // { + // } + // + // protected KalkModuleWithFunctions(string? name) + // { + // // Content = new ScriptObject(); + // } + + // public ScriptObject Content { get; } + + // protected override void Import() + // { + // base.Import(); + // + // // Feed the engine with our new builtins + // Engine.Builtins.Import(Content); + // + // DisplayImport(); + // } + // + // protected virtual void DisplayImport() + // { + // if (!IsBuiltin && Content.Count > 0) + // { + // Engine.WriteHighlightLine($"# {Content.Count} functions successfully imported from module `{Name}`."); + // } + // } + + protected virtual void RegisterFunctions() + { + // intentionally do nothing. + } + + protected void RegisterConstant(string name, object value) + { + RegisterVariable(name, value); + } + protected void RegisterAction(string name, Action action) + { + RegisterCustomFunction(name, DelegateCustomFunction.Create(action)); + } + + protected void RegisterAction(string name, Action action) + { + RegisterCustomFunction(name, DelegateCustomFunction.Create(action)); + } + + protected void RegisterAction(string name, Action action) + { + RegisterCustomFunction(name, DelegateCustomFunction.Create(action)); + } + + protected void RegisterAction(string name, Action action) + { + RegisterCustomFunction(name, DelegateCustomFunction.Create(action)); + } + + protected void RegisterAction(string name, Action action) + { + RegisterCustomFunction(name, DelegateCustomFunction.Create(action)); + } + + protected void RegisterFunction(string name, Func func) + { + RegisterCustomFunction(name, DelegateCustomFunction.CreateFunc(func)); + } + + protected void RegisterFunction(string name, Func func) + { + RegisterCustomFunction(name, DelegateCustomFunction.CreateFunc(func)); + } + + protected void RegisterFunction(string name, Func func) + { + RegisterCustomFunction(name, DelegateCustomFunction.CreateFunc(func)); + } + + protected void RegisterFunction(string name, Func func) + { + RegisterCustomFunction(name, DelegateCustomFunction.CreateFunc(func)); + } + + protected void RegisterFunction(string name, Func func) + { + static DelegateCustomFunction CreateFunc(Func func) + { + if (func == null) throw new ArgumentNullException(nameof(func)); + return new InternalDelegateCustomFunctionWithInterfaceContext(func); + } + + RegisterCustomFunction(name, CreateFunc(func)); + } + + + /// + /// A custom function taking 4 arguments. + /// + /// Func 0 arg type + /// Func 1 arg type + /// Func 2 arg type + /// Func 3 arg type + /// Type result + public class InternalDelegateCustomFunctionWithInterfaceContext : DelegateCustomFunction + { + public InternalDelegateCustomFunctionWithInterfaceContext(Func func) + : base(RewriteFunc(func)) + { + Func = func; + } + + private static Delegate RewriteFunc(Func func) + where TTemplateContext : TemplateContext, TContextInterface + { + if (typeof(T1) == typeof(TContextInterface)) + { + return (TTemplateContext arg1, T2 arg2, T3 arg3, T4 arg4) => + { + // Probably does not matter as Func is used to invoke. + throw new Exception(); + // return func((T1)(object)arg1, arg2, arg3, arg4); + }; + } + + return func; + } + + public Func Func { get; } + + protected override object InvokeImpl(TemplateContext context, SourceSpan span, object[] arguments) + { + var arg1 = (T1)arguments[0]; + var arg2 = (T2)arguments[1]; + var arg3 = (T3)arguments[2]; + var arg4 = (T4)arguments[3]; + return Func(arg1, arg2, arg3, arg4); + } + } + + protected void RegisterFunction(string name, Func func) + { + RegisterCustomFunction(name, DelegateCustomFunction.CreateFunc(func)); + } + + protected void RegisterCustomFunction(string name, IScriptCustomFunction func) + { + RegisterVariable(name, func); + } + + protected void RegisterVariable(string name, object value) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + var names = name.Split(','); + + + foreach (var subName in names) + { + SetValue(subName, value, true); + } + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Model/TemplateEvaluatorExtensions.cs b/src/RepoM.ActionMenu.Core/Model/TemplateEvaluatorExtensions.cs new file mode 100644 index 00000000..dcea3f7f --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Model/TemplateEvaluatorExtensions.cs @@ -0,0 +1,73 @@ +namespace RepoM.ActionMenu.Core.Model; + +using System; +using System.Threading.Tasks; +using RepoM.ActionMenu.Interface.ActionMenuFactory; +using RepoM.ActionMenu.Interface.YamlModel.Templating; + +internal static class TemplateEvaluatorExtensions +{ + public static Task RenderStringAsync(this ITemplateEvaluator instance, RenderString renderString) + { + return renderString.RenderAsync(instance); + } + + public static Task TranslateAsync(this ITemplateEvaluator instance, string text) + { + // todo method + return instance.RenderStringAsync("{{ translate('" + text + ")}}"); + } + + public static async Task RenderNullableString(this ITemplateEvaluator instance, string? text) + { + if (text == null) + { + return string.Empty; + } + + return await instance.RenderStringAsync(text).ConfigureAwait(false); + } + + public static async Task EvaluateToBooleanAsync(this ITemplateEvaluator instance, string? text, bool defaultValue) + { + if (string.IsNullOrWhiteSpace(text)) + { + return defaultValue; + } + + var result = await instance.EvaluateAsync(text); + + if (result == null) + { + // log + return defaultValue; + } + + if (result is bool b) + { + return b; + } + + if (result is 0) + { + return false; + } + + if (result is int) + { + return true; + } + + if (result is "true") + { + return true; + } + + if (result is "false") + { + return false; + } + + throw new Exception(); + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/PublicApi/Factory.cs b/src/RepoM.ActionMenu.Core/PublicApi/Factory.cs new file mode 100644 index 00000000..3ca6c99e --- /dev/null +++ b/src/RepoM.ActionMenu.Core/PublicApi/Factory.cs @@ -0,0 +1,28 @@ +namespace RepoM.ActionMenu.Core.PublicApi; + +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Linq; +using RepoM.ActionMenu.Core.Misc; +using RepoM.ActionMenu.Core.Services; +using RepoM.ActionMenu.Interface.Scriban; + +public class Factory +{ + private readonly IFileSystem _fileSystem; + private readonly ITemplateContextRegistration[] _plugins; + private readonly ITemplateParser _templateParser = new FixedTemplateParser(); + + public Factory(IFileSystem fileSystem, IEnumerable plugins) + { + _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + _plugins = plugins?.ToArray() ?? throw new ArgumentNullException(nameof(plugins)); + } + + public IUserInterfaceActionMenuFactory Create(string filename) + { + var result = new UserInterfaceActionMenuFactory(_fileSystem, _templateParser, _plugins, filename); + return result; + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/PublicApi/IUserInterfaceActionMenuFactory.cs b/src/RepoM.ActionMenu.Core/PublicApi/IUserInterfaceActionMenuFactory.cs new file mode 100644 index 00000000..c277e4c4 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/PublicApi/IUserInterfaceActionMenuFactory.cs @@ -0,0 +1,13 @@ +namespace RepoM.ActionMenu.Core.PublicApi; + +using System.Collections.Generic; +using System.Threading.Tasks; +using RepoM.ActionMenu.Interface.UserInterface; +using RepoM.Core.Plugin.Repository; + +public interface IUserInterfaceActionMenuFactory +{ + Task> CreateMenuAsync(IRepository repository); + + Task> GetTagsAsync(IRepository repository); +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/RepoM.ActionMenu.Core.csproj b/src/RepoM.ActionMenu.Core/RepoM.ActionMenu.Core.csproj new file mode 100644 index 00000000..e07b92bf --- /dev/null +++ b/src/RepoM.ActionMenu.Core/RepoM.ActionMenu.Core.csproj @@ -0,0 +1,18 @@ + + + + net7.0 + + + + + + + + + + + + + + diff --git a/src/RepoM.ActionMenu.Core/RepoMCodeGen.generated.cs b/src/RepoM.ActionMenu.Core/RepoMCodeGen.generated.cs new file mode 100644 index 00000000..558bbb1e --- /dev/null +++ b/src/RepoM.ActionMenu.Core/RepoMCodeGen.generated.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable + +using System; + +namespace RepoM.ActionMenu.Core.Model.Functions +{ + partial class FileFunctions + { + protected sealed override void RegisterFunctions() + { + RegisterFunction("find_files", (Func)FindFiles); + RegisterFunction("find_files_interface", (Func)FindFilesUsingInterface); + RegisterFunction("file_exists", (Func)FileExists); + RegisterFunction("dir_exists", (Func)DirectoryExists); + } + } +} + +namespace RepoM.ActionMenu.Core.Model.Functions +{ + partial class RepositoryFunctions + { + protected sealed override void RegisterFunctions() + { + RegisterConstant("name", Name); + RegisterConstant("path", Path); + RegisterConstant("branch", CurrentBranch); + RegisterConstant("branches", Branches); + RegisterConstant("local-branches", LocalBranches); + } + } +} + +namespace RepoM.ActionMenu.Core.Model.Functions +{ + partial class StringModule + { + protected sealed override void RegisterFunctions() + { + RegisterFunction("escape", (Func)StringEscape); + RegisterFunction("capitalize", (Func)StringCapitalize); + RegisterFunction("capitalize_words", (Func)StringCapitalizeWords); + RegisterFunction("downcase", (Func)StringDowncase); + RegisterFunction("upcase", (Func)StringUpcase); + RegisterFunction("endswith", (Func)StringEndsWith); + RegisterFunction("handleize", (Func)StringHandleize); + RegisterFunction("lstrip", (Func)StringLeftStrip); + RegisterFunction("pluralize", (Func)StringPluralize); + RegisterFunction("rstrip", (Func)StringRightStrip); + RegisterFunction("split", (Func)StringSplit); + RegisterFunction("startswith", (Func)StringStartsWith); + RegisterFunction("strip", (Func)StringStrip); + RegisterFunction("strip_newlines", (Func)StringStripNewlines); + RegisterFunction("pad_left", (Func)StringPadLeft); + RegisterFunction("pad_right", (Func)StringPadRight); + RegisterFunction("regex_escape", (Func)RegexEscape); + RegisterFunction("regex_match", (Func)RegexMatch); + RegisterFunction("regex_matches", (Func)RegexMatches); + RegisterFunction("regex_replace", (Func)RegexReplace); + RegisterFunction("regex_split", (Func)RegexSplit); + RegisterFunction("regex_unescape", (Func)RegexUnescape); + } + } +} diff --git a/src/RepoM.ActionMenu.Core/Services/UserInterfaceActionMenuFactory.cs b/src/RepoM.ActionMenu.Core/Services/UserInterfaceActionMenuFactory.cs new file mode 100644 index 00000000..218c53c9 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Services/UserInterfaceActionMenuFactory.cs @@ -0,0 +1,70 @@ +namespace RepoM.ActionMenu.Core.Services; + +using System; +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Threading.Tasks; +using RepoM.ActionMenu.Core.Misc; +using RepoM.ActionMenu.Core.Model; +using RepoM.ActionMenu.Core.PublicApi; +using RepoM.ActionMenu.Interface.Scriban; +using RepoM.ActionMenu.Interface.UserInterface; +using RepoM.Core.Plugin.Repository; + +internal class UserInterfaceActionMenuFactory : IUserInterfaceActionMenuFactory +{ + private readonly IFileSystem _fileSystem; + private readonly ITemplateParser _templateParser; + private readonly ITemplateContextRegistration[] _plugins; + private readonly string _filename; + + public UserInterfaceActionMenuFactory( + IFileSystem fileSystem, + ITemplateParser templateParser, + ITemplateContextRegistration[] plugins, + string filename) + { + _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + _templateParser = templateParser ?? throw new ArgumentNullException(nameof(templateParser)); + _plugins = plugins ?? throw new ArgumentNullException(nameof(plugins)); + _filename = filename ?? throw new ArgumentNullException(nameof(filename)); + } + + public async Task> CreateMenuAsync(IRepository repository) + { + var context = new ActionMenuGenerationContext(repository, _templateParser, _fileSystem, _plugins); + + // context.Repository.IsStarred = false; + + // load yaml + var actions = await context.LoadAsync(_filename).ConfigureAwait(false); + + // process context (vars + methods) + await context.AddRepositoryContextAsync(actions.Context).ConfigureAwait(false); + + // process tags + + // process actions + return await context.AddActionMenusAsync(actions.ActionMenu).ConfigureAwait(false); + } + + + public async Task> GetTagsAsync(IRepository repository) + { + var context = new ActionMenuGenerationContext(repository, _templateParser, _fileSystem, _plugins); + + // load yaml + var actions = await context.LoadAsync(_filename).ConfigureAwait(false); + + if (actions.Tags == null) + { + return Array.Empty(); + } + + // process context (vars + methods) + await context.AddRepositoryContextAsync(actions.Context).ConfigureAwait(false); + + // process tags + return await context.GetTagsAsync(actions.Tags).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/UserInterface/DeferredSubActionsUserInterfaceRepositoryAction.cs b/src/RepoM.ActionMenu.Core/UserInterface/DeferredSubActionsUserInterfaceRepositoryAction.cs new file mode 100644 index 00000000..9a8e709a --- /dev/null +++ b/src/RepoM.ActionMenu.Core/UserInterface/DeferredSubActionsUserInterfaceRepositoryAction.cs @@ -0,0 +1,36 @@ +namespace RepoM.ActionMenu.Core.UserInterface; + +using System; +using System.Threading.Tasks; +using RepoM.ActionMenu.Interface.ActionMenuFactory; +using RepoM.ActionMenu.Interface.UserInterface; +using RepoM.Core.Plugin.Repository; + +public sealed class DeferredSubActionsUserInterfaceRepositoryAction : UserInterfaceRepositoryAction +{ + private readonly Func>? _action; + private readonly IActionMenuGenerationContext _context; + + public DeferredSubActionsUserInterfaceRepositoryAction(string name, IRepository repository, IActionMenuGenerationContext actionMenuGenerationContext, bool captureScope) + : base(name, repository) + { + _context = captureScope + ? actionMenuGenerationContext.Clone() + : actionMenuGenerationContext; + } + + public Func> DeferredFunc + { + init => _action = value; + } + + public async Task GetAsync() + { + if (_action == null) + { + return Array.Empty(); + } + + return await _action(_context).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/UserInterface/UserInterfaceRepositoryAction.cs b/src/RepoM.ActionMenu.Core/UserInterface/UserInterfaceRepositoryAction.cs new file mode 100644 index 00000000..a2b4f020 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/UserInterface/UserInterfaceRepositoryAction.cs @@ -0,0 +1,15 @@ +namespace RepoM.ActionMenu.Core.UserInterface; + +using RepoM.ActionMenu.Interface.UserInterface; +using RepoM.Core.Plugin.Repository; + +public class UserInterfaceRepositoryAction : UserInterfaceRepositoryActionBase +{ + public UserInterfaceRepositoryAction(string name, IRepository repository) : + base(repository) + { + Name = name; + } + + public string Name { get; } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/ActionMenu.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/ActionMenu.cs new file mode 100644 index 00000000..69168a48 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/ActionMenu.cs @@ -0,0 +1,8 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.ActionMenus; + +using System.Collections.Generic; +using RepoM.ActionMenu.Interface.YamlModel; + +public class ActionMenu : List +{ +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/AssociateFile/ActionAssociateFileV1Mapper.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/AssociateFile/ActionAssociateFileV1Mapper.cs new file mode 100644 index 00000000..e8bca9c3 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/AssociateFile/ActionAssociateFileV1Mapper.cs @@ -0,0 +1,22 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.ActionMenus.AssociateFile; + +using System.Collections.Generic; +using RepoM.ActionMenu.Core.UserInterface; +using RepoM.ActionMenu.Interface.ActionMenuFactory; +using RepoM.ActionMenu.Interface.YamlModel; +using RepoM.Core.Plugin.Repository; + +internal class ActionAssociateFileV1Mapper : ActionToRepositoryActionMapperBase +{ + protected override async IAsyncEnumerable MapAsync(RepositoryActionAssociateFileV1 action, IActionMenuGenerationContext context, IRepository repository) + { + if (action.Extension is null) + { + yield break; + } + + var name = await context.RenderStringAsync(action.Name).ConfigureAwait(false); + + yield return new UserInterfaceRepositoryAction(name, repository); + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/AssociateFile/RepositoryActionAssociateFileV1.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/AssociateFile/RepositoryActionAssociateFileV1.cs new file mode 100644 index 00000000..2f95dd5e --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/AssociateFile/RepositoryActionAssociateFileV1.cs @@ -0,0 +1,25 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.ActionMenus.AssociateFile; + +using RepoM.ActionMenu.Interface.YamlModel; + +internal sealed class RepositoryActionAssociateFileV1 : IMenuAction, IName +{ + public const string TypeValue = "associate-file@1"; + + public string Type + { + get => TypeValue; + set => _ = value; + } + + public string Name { get; init; } = string.Empty; + + public string? Extension { get; init; } + + public string? Active { get; init; } + + public override string ToString() + { + return $"({TypeValue}) {Name} : {Extension}"; + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/BrowseRepository/BrowseRepositoryV1Mapper.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/BrowseRepository/BrowseRepositoryV1Mapper.cs new file mode 100644 index 00000000..b3f62e1c --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/BrowseRepository/BrowseRepositoryV1Mapper.cs @@ -0,0 +1,59 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.ActionMenus.BrowseRepository; + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using RepoM.ActionMenu.Core.Model; +using RepoM.ActionMenu.Core.UserInterface; +using RepoM.ActionMenu.Interface.ActionMenuFactory; +using RepoM.ActionMenu.Interface.Commands; +using RepoM.ActionMenu.Interface.UserInterface; +using RepoM.ActionMenu.Interface.YamlModel; +using RepoM.Core.Plugin.Repository; + +internal class BrowseRepositoryV1Mapper : ActionToRepositoryActionMapperBase +{ + protected override async IAsyncEnumerable MapAsync(RepositoryActionBrowseRepositoryV1 action, IActionMenuGenerationContext context, IRepository repository) + { + if (repository.Remotes.Count == 0) + { + yield break; + } + + var name = await context.RenderStringAsync(action.Name).ConfigureAwait(false); + if (string.IsNullOrWhiteSpace(name)) + { + name = await context.TranslateAsync("Browse remote").ConfigureAwait(false); + } + + var forceSingle = await context.EvaluateToBooleanAsync(action.FirstOnly, false); + + if (repository.Remotes.Count == 1 || forceSingle) + { + yield return new UserInterfaceRepositoryAction(name, repository) + { + RepositoryCommand = new BrowseRepositoryCommand(repository.Remotes[0].Url), + }; + } + else + { + yield return new DeferredSubActionsUserInterfaceRepositoryAction(name, repository, context, captureScope: false) + { + CanExecute = true, + DeferredFunc = async ctx => await EnumerateRemotes(ctx.Repository), + }; + } + } + + private static Task EnumerateRemotes(IRepository repository) + { + return Task.FromResult(repository.Remotes + .Take(50) + .Select(remote => new UserInterfaceRepositoryAction(remote.Name, repository) + { + RepositoryCommand = new BrowseRepositoryCommand(remote.Url), + }) + .Cast() + .ToArray()); + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/BrowseRepository/RepositoryActionBrowseRepositoryV1.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/BrowseRepository/RepositoryActionBrowseRepositoryV1.cs new file mode 100644 index 00000000..e6034906 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/BrowseRepository/RepositoryActionBrowseRepositoryV1.cs @@ -0,0 +1,25 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.ActionMenus.BrowseRepository; + +using RepoM.ActionMenu.Interface.YamlModel; + +internal sealed class RepositoryActionBrowseRepositoryV1 : IMenuAction, IName +{ + public const string TypeValue = "browse-repository@1"; + + public string Type + { + get => TypeValue; + set => _ = value; + } + + public string Name { get; init; } = string.Empty; + + public string? Active { get; init; } + + public string? FirstOnly { get; set; } + + public override string ToString() + { + return $"({TypeValue}) {Name}"; + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/Command/RepositoryActionCommandV1.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/Command/RepositoryActionCommandV1.cs new file mode 100644 index 00000000..88ed39a4 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/Command/RepositoryActionCommandV1.cs @@ -0,0 +1,35 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.ActionMenus.Command; + +using System.ComponentModel.DataAnnotations; +using RepoM.ActionMenu.Interface.YamlModel; + +internal sealed class RepositoryActionCommandV1 : IMenuAction, IName +{ + public const string TypeValue = "command@1"; + + public string Type + { + get => TypeValue; + set => _ = value; + } + + public string Name { get; init; } = string.Empty; + + public string? Active { get; init; } + + /// + /// The command to execute. + /// + [Required] + public string? Command { get; set; } + + /// + /// Arguments for the command. + /// + public string? Arguments { get; set; } + + public override string ToString() + { + return $"({TypeValue}) {Name}"; + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/Command/RepositoryActionCommandV1Mapper.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/Command/RepositoryActionCommandV1Mapper.cs new file mode 100644 index 00000000..1d8dbdda --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/Command/RepositoryActionCommandV1Mapper.cs @@ -0,0 +1,24 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.ActionMenus.Command; + +using System.Collections.Generic; +using RepoM.ActionMenu.Core.Model; +using RepoM.ActionMenu.Core.UserInterface; +using RepoM.ActionMenu.Interface.ActionMenuFactory; +using RepoM.ActionMenu.Interface.Commands; +using RepoM.ActionMenu.Interface.YamlModel; +using RepoM.Core.Plugin.Repository; + +internal class ActionCommandV1Mapper : ActionToRepositoryActionMapperBase +{ + protected override async IAsyncEnumerable MapAsync(RepositoryActionCommandV1 action, IActionMenuGenerationContext context, IRepository repository) + { + var name = await context.RenderStringAsync(action.Name).ConfigureAwait(false); + var command = await context.RenderNullableString(action.Command).ConfigureAwait(false); + var arguments = await context.RenderNullableString(action.Arguments).ConfigureAwait(false); + + yield return new UserInterfaceRepositoryAction(name, repository) + { + RepositoryCommand = new StartProcessRepositoryCommand(command, arguments), + }; + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/Folder/RepositoryActionFolderV1.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/Folder/RepositoryActionFolderV1.cs new file mode 100644 index 00000000..88b19cea --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/Folder/RepositoryActionFolderV1.cs @@ -0,0 +1,33 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.ActionMenus.Folder; + +using RepoM.ActionMenu.Core.Yaml.Model.Ctx; +using RepoM.ActionMenu.Interface.YamlModel; + +internal sealed class RepositoryActionFolderV1 : IMenuAction, IName, IMenuActions, IContext, IDeferred +{ + /// + /// RepositoryAction type. + /// + public const string TypeValue = "folder@1"; + + public string Type + { + get => TypeValue; + set => _ = value; + } + + public ActionMenu? Actions { get; init; } + + public string Name { get; init; } = string.Empty; + + public string? Active { get; init; } + + public Context? Context { get; init; } + + public string? IsDeferred { get; init; } + + public override string ToString() + { + return $"({TypeValue}) {Name} : #actions: {Actions?.Count ?? 0}"; + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/Folder/RepositoryActionFolderV1Mapper.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/Folder/RepositoryActionFolderV1Mapper.cs new file mode 100644 index 00000000..54f52246 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/Folder/RepositoryActionFolderV1Mapper.cs @@ -0,0 +1,42 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.ActionMenus.Folder; + +using System.Collections.Generic; +using System.Linq; +using RepoM.ActionMenu.Core.Model; +using RepoM.ActionMenu.Core.UserInterface; +using RepoM.ActionMenu.Interface.ActionMenuFactory; +using RepoM.ActionMenu.Interface.YamlModel; +using RepoM.Core.Plugin.Repository; + +internal class RepositoryActionFolderV1Mapper : ActionToRepositoryActionMapperBase +{ + protected override async IAsyncEnumerable MapAsync(RepositoryActionFolderV1 action, IActionMenuGenerationContext context, IRepository repository) + { + var name = await context.RenderStringAsync(action.Name).ConfigureAwait(false); + + if (action.Actions == null) + { + yield return new UserInterfaceRepositoryAction(name, repository); + yield break; + } + + var isDeferred = await context.EvaluateToBooleanAsync(action.IsDeferred, false).ConfigureAwait(false); + + if (isDeferred) + { + yield return new DeferredSubActionsUserInterfaceRepositoryAction(name, repository, context, action.Actions != null) + { + CanExecute = true, + DeferredFunc = async ctx => (await ctx.AddActionMenusAsync(action.Actions)).ToArray(), + }; + } + else + { + yield return new UserInterfaceRepositoryAction(name, repository) + { + CanExecute = true, + SubActions = await context.AddActionMenusAsync(action.Actions), + }; + } + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/ForEach/RepositoryActionForEachV1.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/ForEach/RepositoryActionForEachV1.cs new file mode 100644 index 00000000..b9f12881 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/ForEach/RepositoryActionForEachV1.cs @@ -0,0 +1,47 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.ActionMenus.ForEach; + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using RepoM.ActionMenu.Interface.YamlModel; + +internal sealed class RepositoryActionForEachV1 : IMenuAction +{ + public const string TypeValue = "foreach@1"; + + public string Type + { + get => TypeValue; + set => _ = value; + } + + public string? Active { get; init; } + + + /// + /// The list of items to enumerate on. + /// + [Required] + public string? Enumerable { get; set; } + + /// + /// The name of the variable to access to current enumeration of the items. For each iteration, the variable `{var.name}` has the value of the current iteration. + /// + [Required] + public string? Variable { get; set; } + + /// + /// Predicate to skip the current item. + /// + public string? Skip { get; set; } + + /// + /// List of repeated actions. + /// + [Required] + public List Actions { get; set; } = new List(); + + public override string ToString() + { + return $"({TypeValue})"; + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/ForEach/RepositoryActionForEachV1Mapper.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/ForEach/RepositoryActionForEachV1Mapper.cs new file mode 100644 index 00000000..bd343dc3 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/ForEach/RepositoryActionForEachV1Mapper.cs @@ -0,0 +1,71 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.ActionMenus.ForEach; + +using System.Collections; +using System.Collections.Generic; +using RepoM.ActionMenu.Core.Model; +using RepoM.ActionMenu.Interface.ActionMenuFactory; +using RepoM.ActionMenu.Interface.UserInterface; +using RepoM.ActionMenu.Interface.YamlModel; +using RepoM.Core.Plugin.Repository; + +internal class RepositoryActionForEachV1Mapper : ActionToRepositoryActionMapperBase +{ + protected override async IAsyncEnumerable MapAsync(RepositoryActionForEachV1 action, IActionMenuGenerationContext context, IRepository repository) + { + if (string.IsNullOrWhiteSpace(action.Enumerable)) + { + yield break; + } + + if (string.IsNullOrWhiteSpace(action.Variable)) + { + yield break; + } + + var enumerable = await context.EvaluateAsync(action.Enumerable).ConfigureAwait(false); + if (enumerable is null) + { + yield break; + } + + if (enumerable is not IList list) + { + yield break; + } + + if (list.Count == 0) + { + yield break; + } + + foreach (var item in list) + { + if (item is null) + { + continue; + } + + using var scope = context.CreateGlobalScope(); + + // todo evaluate to string or not? + scope.SetValue(action.Variable, item, true); + + if (await context.EvaluateToBooleanAsync(action.Skip, false).ConfigureAwait(false)) + { + continue; + } + + var items = await context.AddActionMenusAsync(action.Actions).ConfigureAwait(false); + + if (items is null) + { + continue; + } + + foreach (var menuItem in items) + { + yield return menuItem; + } + } + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/Git/Checkout/RepositoryActionGitCheckoutV1.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/Git/Checkout/RepositoryActionGitCheckoutV1.cs new file mode 100644 index 00000000..b415aef7 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/Git/Checkout/RepositoryActionGitCheckoutV1.cs @@ -0,0 +1,23 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.ActionMenus.Git.Checkout; + +using RepoM.ActionMenu.Interface.YamlModel; + +internal sealed class RepositoryActionGitCheckoutV1 : IMenuAction, IOptionalName +{ + public const string TypeValue = "git-checkout@1"; + + public string Type + { + get => TypeValue; + set => _ = value; + } + + public string? Name { get; } + + public string? Active { get; init; } + + public override string ToString() + { + return $"({TypeValue}) {Name}"; + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/Git/Checkout/RepositoryActionJustTextV1Mapper.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/Git/Checkout/RepositoryActionJustTextV1Mapper.cs new file mode 100644 index 00000000..e483944c --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/Git/Checkout/RepositoryActionJustTextV1Mapper.cs @@ -0,0 +1,26 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.ActionMenus.Git.Checkout; + +using System; +using System.Collections.Generic; +using RepoM.ActionMenu.Core.UserInterface; +using RepoM.ActionMenu.Interface.ActionMenuFactory; +using RepoM.ActionMenu.Interface.YamlModel; +using RepoM.Core.Plugin.Repository; + +internal class RepositoryActionGitCheckoutV1Mapper : ActionToRepositoryActionMapperBase +{ + protected override IAsyncEnumerable MapAsync(RepositoryActionGitCheckoutV1 action, IActionMenuGenerationContext context, IRepository repository) + { + //var name = await context.RenderNullableString(action.Name).ConfigureAwait(false); + + //if (string.IsNullOrWhiteSpace(name)) + //{ + // name = await context.TranslateAsync("Checkout").ConfigureAwait(false); + //} + + //var text = await context.RenderStringAsync(action.Name).ConfigureAwait(false); + + // todo coenm + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/IContext.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/IContext.cs new file mode 100644 index 00000000..98a06fb9 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/IContext.cs @@ -0,0 +1,8 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.ActionMenus; + +using RepoM.ActionMenu.Core.Yaml.Model.Ctx; + +internal interface IContext +{ + Context? Context { get; } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/IDeferred.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/IDeferred.cs new file mode 100644 index 00000000..9aa491d2 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/IDeferred.cs @@ -0,0 +1,6 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.ActionMenus; + +internal interface IDeferred +{ + string? IsDeferred { get; } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/IMenuActions.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/IMenuActions.cs new file mode 100644 index 00000000..95adb603 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/IMenuActions.cs @@ -0,0 +1,6 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.ActionMenus; + +internal interface IMenuActions +{ + ActionMenu? Actions { get; } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/IName.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/IName.cs new file mode 100644 index 00000000..b733d185 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/IName.cs @@ -0,0 +1,11 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.ActionMenus; + +internal interface IOptionalName +{ + string Name { get; } +} + +internal interface IName +{ + string Name { get; } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/JustText/RepositoryActionJustTextV1.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/JustText/RepositoryActionJustTextV1.cs new file mode 100644 index 00000000..07b932cd --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/JustText/RepositoryActionJustTextV1.cs @@ -0,0 +1,31 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.ActionMenus.JustText; + +using RepoM.ActionMenu.Core.Yaml.Model.Ctx; +using RepoM.ActionMenu.Interface.YamlModel; + +internal sealed class RepositoryActionJustTextV1 : IMenuAction, IContext +{ + public const string TypeValue = "just-text@1"; + + public string Type + { + get => TypeValue; + set => _ = value; + } + + public string Text { get; init; } + + public string? Active { get; init; } + + /// + /// Show the menu as enabled (clickable) or disabled. + /// + public string? Enabled { get; init; } + + public Context? Context { get; init; } + + public override string ToString() + { + return $"({TypeValue}) {Text}"; + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/JustText/RepositoryActionJustTextV1Mapper.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/JustText/RepositoryActionJustTextV1Mapper.cs new file mode 100644 index 00000000..fb4d4caa --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/ActionMenus/JustText/RepositoryActionJustTextV1Mapper.cs @@ -0,0 +1,16 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.ActionMenus.JustText; + +using System.Collections.Generic; +using RepoM.ActionMenu.Core.UserInterface; +using RepoM.ActionMenu.Interface.ActionMenuFactory; +using RepoM.ActionMenu.Interface.YamlModel; +using RepoM.Core.Plugin.Repository; + +internal class RepositoryActionJustTextV1Mapper : ActionToRepositoryActionMapperBase +{ + protected override async IAsyncEnumerable MapAsync(RepositoryActionJustTextV1 action, IActionMenuGenerationContext context, IRepository repository) + { + var text = await context.RenderStringAsync(action.Text).ConfigureAwait(false); + yield return new UserInterfaceRepositoryAction(text, repository); + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/ContextRoot.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/ContextRoot.cs new file mode 100644 index 00000000..ecda8b9a --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/ContextRoot.cs @@ -0,0 +1,8 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model; + +using RepoM.ActionMenu.Core.Yaml.Model.Ctx; + +public class ContextRoot +{ + public Context? Context { get; set; } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/Context.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/Context.cs new file mode 100644 index 00000000..78aba315 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/Context.cs @@ -0,0 +1,8 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.Ctx; + +using System.Collections.Generic; +using RepoM.ActionMenu.Interface.YamlModel; + +public class Context : List +{ +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/ContextActionMapperBase.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/ContextActionMapperBase.cs new file mode 100644 index 00000000..7aa9f0c4 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/ContextActionMapperBase.cs @@ -0,0 +1,20 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.Ctx; + +using System.Threading.Tasks; +using RepoM.ActionMenu.Interface.ActionMenuFactory; +using RepoM.ActionMenu.Interface.YamlModel; + +public abstract class ContextActionMapperBase : IContextActionMapper where T : IContextAction +{ + public bool CanMap(IContextAction action) + { + return action is T; + } + + public Task MapAsync(IContextAction action, IContextMenuActionMenuGenerationContext context, IScope scope) + { + return MapAsync((T)action, context, scope); + } + + protected abstract Task MapAsync(T contextAction, IContextMenuActionMenuGenerationContext context, IScope scope); +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/EvaluateVariable/EvaluateVariableActionContextActionMapper.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/EvaluateVariable/EvaluateVariableActionContextActionMapper.cs new file mode 100644 index 00000000..ae2df3f3 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/EvaluateVariable/EvaluateVariableActionContextActionMapper.cs @@ -0,0 +1,19 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.Ctx.EvaluateVariable; + +using System.Threading.Tasks; +using RepoM.ActionMenu.Interface.ActionMenuFactory; + +internal class EvaluateVariableActionContextActionMapper : ContextActionMapperBase +{ + protected override async Task MapAsync(EvaluateVariableContextAction contextContextAction, IContextMenuActionMenuGenerationContext context, IScope scope) + { + object? result; + + using (var _ = context.CreateGlobalScope()) + { + result = await context.EvaluateAsync(contextContextAction.Value).ConfigureAwait(false); + } + + scope.SetValue(contextContextAction.Name, result, false); + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/EvaluateVariable/EvaluateVariableContextAction.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/EvaluateVariable/EvaluateVariableContextAction.cs new file mode 100644 index 00000000..d896382e --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/EvaluateVariable/EvaluateVariableContextAction.cs @@ -0,0 +1,29 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.Ctx.EvaluateVariable; + +using RepoM.ActionMenu.Interface.YamlModel; + +public class EvaluateVariableContextAction : NamedContextAction, IContextAction, IEnabled +{ + public const string TypeValue = "evaluate-variable@1"; + + public override string Type + { + get => TypeValue; + set => _ = value; + } + + public string Value { get; init; } + + public string? Enabled { get; init; } + + public override string ToString() + { + var value = Value; + if (value.Length > 10) + { + value = value[..10] + ".."; + } + + return $"({TypeValue}) {Name} : {value}"; + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/ExecuteScript/ExecuteScript.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/ExecuteScript/ExecuteScript.cs new file mode 100644 index 00000000..495e2d97 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/ExecuteScript/ExecuteScript.cs @@ -0,0 +1,29 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.Ctx.ExecuteScript; + +using RepoM.ActionMenu.Interface.YamlModel; + +public sealed class ExecuteScript : IContextAction, IEnabled +{ + public const string TypeValue = "evaluate-script@1"; + + public string Type + { + get => TypeValue; + set => _ = value; + } + + public string Content { get; init; } + + public string? Enabled { get; init; } + + public override string ToString() + { + var value = Content; + if (value.Length > 10) + { + value = value[..10] + ".."; + } + + return $"({TypeValue}) : {value}"; + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/ExecuteScript/ExecuteScriptContextActionMapper.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/ExecuteScript/ExecuteScriptContextActionMapper.cs new file mode 100644 index 00000000..4487d6ee --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/ExecuteScript/ExecuteScriptContextActionMapper.cs @@ -0,0 +1,17 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.Ctx.ExecuteScript; + +using System.Threading.Tasks; +using RepoM.ActionMenu.Interface.ActionMenuFactory; + +internal class ExecuteScriptContextActionMapper : ContextActionMapperBase +{ + protected override async Task MapAsync(ExecuteScript contextAction, IContextMenuActionMenuGenerationContext context, IScope scope) + { + var result = await context.EvaluateAsync(contextAction.Content).ConfigureAwait(false); + + if (result is not null) + { + // log warning. + } + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/IContextActionMapper.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/IContextActionMapper.cs new file mode 100644 index 00000000..9c21a923 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/IContextActionMapper.cs @@ -0,0 +1,12 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.Ctx; + +using System.Threading.Tasks; +using RepoM.ActionMenu.Interface.ActionMenuFactory; +using RepoM.ActionMenu.Interface.YamlModel; + +internal interface IContextActionMapper +{ + bool CanMap(IContextAction action); + + Task MapAsync(IContextAction action, IContextMenuActionMenuGenerationContext context, IScope scope); +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/IEnabled.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/IEnabled.cs new file mode 100644 index 00000000..06350414 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/IEnabled.cs @@ -0,0 +1,6 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.Ctx; + +public interface IEnabled +{ + string? Enabled { get; } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/INamedContextAction.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/INamedContextAction.cs new file mode 100644 index 00000000..c97c0068 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/INamedContextAction.cs @@ -0,0 +1,8 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.Ctx; + +using RepoM.ActionMenu.Interface.YamlModel; + +public interface INamedContextAction : IContextAction +{ + string Name { get; } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/LoadFile/LoadFileContextAction.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/LoadFile/LoadFileContextAction.cs new file mode 100644 index 00000000..0be5f049 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/LoadFile/LoadFileContextAction.cs @@ -0,0 +1,29 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.Ctx.LoadFile; + +using RepoM.ActionMenu.Interface.YamlModel; + +public class LoadFileContextAction : NamedContextAction, IContextAction, IEnabled +{ + public const string TypeValue = "load-file@1"; + + public override string Type + { + get => TypeValue; + set => _ = value; + } + + public string? Filename { get; init; } + + public string? Enabled { get; init; } + + public override string ToString() + { + var value = Filename; + if (value.Length > 10) + { + value = value[..10] + ".."; + } + + return $"({TypeValue}) {Name} : {value}"; + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/LoadFile/LoadFileContextActionContextActionMapper.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/LoadFile/LoadFileContextActionContextActionMapper.cs new file mode 100644 index 00000000..9344ab80 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/LoadFile/LoadFileContextActionContextActionMapper.cs @@ -0,0 +1,74 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.Ctx.LoadFile; + +using System; +using System.IO.Abstractions; +using System.Threading.Tasks; +using DotNetEnv; +using RepoM.ActionMenu.Core.Model; +using RepoM.ActionMenu.Interface.ActionMenuFactory; + +internal class LoadFileContextActionContextActionMapper : ContextActionMapperBase +{ + private readonly IFileSystem _fileSystem; + private readonly IActionMenuDeserializer _deserializer; + + public LoadFileContextActionContextActionMapper(IFileSystem fileSystem, IActionMenuDeserializer deserializer) + { + _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + _deserializer = deserializer ?? throw new ArgumentNullException(nameof(deserializer)); + } + + protected override async Task MapAsync(LoadFileContextAction contextContextAction, IContextMenuActionMenuGenerationContext context, IScope scope) + { + var filename = await context.RenderNullableString(contextContextAction.Filename).ConfigureAwait(false); + + if (string.IsNullOrWhiteSpace(filename)) + { + return; + } + + if (!_fileSystem.File.Exists(filename)) + { + return; + } + + if (filename.EndsWith(".yml", StringComparison.CurrentCultureIgnoreCase) || + filename.EndsWith(".yaml", StringComparison.CurrentCultureIgnoreCase)) + { + var yaml = await _fileSystem.File.ReadAllTextAsync(filename).ConfigureAwait(false); + var contextRoot = _deserializer.DeserializeContextRoot(yaml); + + if (contextRoot.Context is not null) + { + foreach (var item in contextRoot.Context) + { + await scope.AddContextActionAsync(item).ConfigureAwait(false); + } + } + + return; + } + + if (filename.EndsWith(".env", StringComparison.CurrentCultureIgnoreCase)) + { + var envContent = await _fileSystem.File.ReadAllTextAsync(filename).ConfigureAwait(false); + var envResult = Env.LoadContents(envContent, new LoadOptions(setEnvVars: false)); + + if (envResult == null) + { + return; + } + + var fileContents = envResult.ToDictionary(); + if (fileContents.Count == 0) + { + return; + } + + scope.PushEnvironmentVariable(fileContents); + } + + // invalid extension. + return; + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/NamedContextAction.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/NamedContextAction.cs new file mode 100644 index 00000000..d27d7a13 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/NamedContextAction.cs @@ -0,0 +1,13 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.Ctx; + +public abstract class NamedContextAction : INamedContextAction +{ + public abstract string Type { get; set; } + + public string Name { get; init; } = string.Empty; + + public override string ToString() + { + return $"({Type}) {Name}"; + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/RendererVariable/RenderVariableActionContextActionMapper.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/RendererVariable/RenderVariableActionContextActionMapper.cs new file mode 100644 index 00000000..05d16fa1 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/RendererVariable/RenderVariableActionContextActionMapper.cs @@ -0,0 +1,20 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.Ctx.RendererVariable; + +using System.Threading.Tasks; +using RepoM.ActionMenu.Core.Model; +using RepoM.ActionMenu.Interface.ActionMenuFactory; + +internal class RenderVariableActionContextActionMapper : ContextActionMapperBase +{ + protected override async Task MapAsync(RenderVariableContextAction contextContextAction, IContextMenuActionMenuGenerationContext context, IScope scope) + { + string? result; + + using (var _ = context.CreateGlobalScope()) + { + result = await context.RenderStringAsync(contextContextAction.Value).ConfigureAwait(false); + } + + scope.SetValue(contextContextAction.Name, result, false); + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/RendererVariable/RenderVariableContextAction.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/RendererVariable/RenderVariableContextAction.cs new file mode 100644 index 00000000..a4afedce --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/RendererVariable/RenderVariableContextAction.cs @@ -0,0 +1,31 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.Ctx.RendererVariable; + +using RepoM.ActionMenu.Interface.YamlModel; +using RepoM.ActionMenu.Interface.YamlModel.Templating; + +public class RenderVariableContextAction : NamedContextAction, IContextAction, IEnabled +{ + public const string TypeValue = "render-variable@1"; + + public override string Type + { + get => TypeValue; + set => _ = value; + } + + [RenderToString("coen")] + public RenderString Value { get; set; } = new() { Value = string.Empty}; + + public string? Enabled { get; init; } + + public override string ToString() + { + var value = Value.Value; + if (value.Length > 10) + { + value = value[..10] + ".."; + } + + return $"{base.ToString()} : {value}"; + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/SetVariable/SetVariableActionContextActionMapper.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/SetVariable/SetVariableActionContextActionMapper.cs new file mode 100644 index 00000000..3428c314 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/SetVariable/SetVariableActionContextActionMapper.cs @@ -0,0 +1,13 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.Ctx.SetVariable; + +using System.Threading.Tasks; +using RepoM.ActionMenu.Interface.ActionMenuFactory; + +internal class SetVariableActionContextActionMapper : ContextActionMapperBase +{ + protected override Task MapAsync(SetVariableContextAction contextContextAction, IContextMenuActionMenuGenerationContext context, IScope scope) + { + scope.SetValue(contextContextAction.Name, contextContextAction.Value, false); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/SetVariable/SetVariableContextAction.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/SetVariable/SetVariableContextAction.cs new file mode 100644 index 00000000..b0151279 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/Ctx/SetVariable/SetVariableContextAction.cs @@ -0,0 +1,38 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.Ctx.SetVariable; + +using RepoM.ActionMenu.Interface.YamlModel; + +public class SetVariableContextAction : NamedContextAction, IContextAction, IEnabled +{ + public const string TypeValue = "set-variable@1"; + + public override string Type + { + get => TypeValue; + set => _ = value; + } + + public object? Value { get; init; } + + public string? Enabled { get; init; } + + public static SetVariableContextAction Create(string name, object? value) + { + return new SetVariableContextAction() + { + Name = name, + Value = value, + }; + } + + public override string ToString() + { + var value = Value?.ToString() ?? "null"; + if (value.Length > 10) + { + value = value[..10] + ".."; + } + + return $"({TypeValue}) {Name} : {value}"; + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/Root.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/Root.cs new file mode 100644 index 00000000..6e1e0134 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/Root.cs @@ -0,0 +1,10 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model; + +using RepoM.ActionMenu.Core.Yaml.Model.ActionMenus; + +public class Root : ContextRoot +{ + public Tags.Tags? Tags { get; set; } + + public ActionMenu? ActionMenu { get; set; } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/Tags/ITag.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/Tags/ITag.cs new file mode 100644 index 00000000..12ee9c13 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/Tags/ITag.cs @@ -0,0 +1,10 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.Tags; + +using RepoM.ActionMenu.Interface.YamlModel.Templating; + +public interface ITag +{ + string Tag { get; set; } + + EvaluateBoolean When { get; set; } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/Tags/TagObject.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/Tags/TagObject.cs new file mode 100644 index 00000000..8e04d71c --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/Tags/TagObject.cs @@ -0,0 +1,13 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.Tags; + +using System.ComponentModel; +using RepoM.ActionMenu.Interface.YamlModel.Templating; + +internal class TagObject : ITag +{ + public string Tag { get; set; } + + [EvaluateToBoolean(true)] + [DefaultValue(true)] // todo + public EvaluateBoolean When { get; set; } = new EvaluateBoolean(); +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/Tags/Tags.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/Tags/Tags.cs new file mode 100644 index 00000000..5caefe93 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/Tags/Tags.cs @@ -0,0 +1,7 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.Tags; + +using System.Collections.Generic; + +public class Tags : List +{ +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/Templating/ICreateTemplate.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/Templating/ICreateTemplate.cs new file mode 100644 index 00000000..bb5b1eb0 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/Templating/ICreateTemplate.cs @@ -0,0 +1,8 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.Templating; + +using RepoM.ActionMenu.Core.Misc; + +internal interface ICreateTemplate +{ + void CreateTemplate(ITemplateParser templateParser); +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/Templating/ScribanEvaluateBoolean.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/Templating/ScribanEvaluateBoolean.cs new file mode 100644 index 00000000..f1f19feb --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/Templating/ScribanEvaluateBoolean.cs @@ -0,0 +1,28 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.Templating; + +using System.Threading.Tasks; +using RepoM.ActionMenu.Core.Misc; +using RepoM.ActionMenu.Interface.ActionMenuFactory; +using RepoM.ActionMenu.Interface.YamlModel.Templating; +using Scriban; + +internal class ScribanEvaluateBoolean : EvaluateBoolean, ICreateTemplate +{ + private Template? _template = null; + + void ICreateTemplate.CreateTemplate(ITemplateParser templateParser) + { + _template ??= templateParser.ParseScriptOnly(Value); + } + + public override async Task EvaluateAsync(ITemplateEvaluator instance) + { + if (instance is TemplateContext tc && _template != null) + { + var result = await _template.EvaluateAsync(tc).ConfigureAwait(false); + return ToBool(result); + } + + return await base.EvaluateAsync(instance).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/Templating/ScribanEvaluateInt.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/Templating/ScribanEvaluateInt.cs new file mode 100644 index 00000000..cfac7fde --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/Templating/ScribanEvaluateInt.cs @@ -0,0 +1,43 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.Templating; + +using System.Threading.Tasks; +using RepoM.ActionMenu.Core.Misc; +using RepoM.ActionMenu.Interface.ActionMenuFactory; +using RepoM.ActionMenu.Interface.YamlModel.Templating; +using Scriban; + +internal class ScribanEvaluateInt : EvaluateInt, ICreateTemplate +{ + private Template? _template = null; + + void ICreateTemplate.CreateTemplate(ITemplateParser templateParser) + { + _template ??= templateParser.ParseScriptOnly(Value); + } + + public override async Task EvaluateAsync(ITemplateEvaluator instance) + { + if (instance is not TemplateContext tc || _template == null) + { + return await base.EvaluateAsync(instance).ConfigureAwait(false); + } + + var result = await _template.EvaluateAsync(tc).ConfigureAwait(false); + if (result is null) + { + return DefaultValue; + } + + if (result is int i) + { + return i; + } + + if (result is string s && int.TryParse(s, out int r)) + { + return r; + } + + return DefaultValue; + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Model/Templating/ScribanRenderString.cs b/src/RepoM.ActionMenu.Core/Yaml/Model/Templating/ScribanRenderString.cs new file mode 100644 index 00000000..b70a7898 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Model/Templating/ScribanRenderString.cs @@ -0,0 +1,27 @@ +namespace RepoM.ActionMenu.Core.Yaml.Model.Templating; + +using System.Threading.Tasks; +using RepoM.ActionMenu.Core.Misc; +using RepoM.ActionMenu.Interface.ActionMenuFactory; +using RepoM.ActionMenu.Interface.YamlModel.Templating; +using Scriban; + +internal class ScribanRenderString : RenderString, ICreateTemplate +{ + private Template? _template; + + void ICreateTemplate.CreateTemplate(ITemplateParser templateParser) + { + _template ??= templateParser.ParseMixed(Value); + } + + public override async Task RenderAsync(ITemplateEvaluator instance) + { + if (instance is TemplateContext tc && _template != null) + { + return await _template.RenderAsync(tc).ConfigureAwait(false); + } + + return await base.RenderAsync(instance).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Serialization/ActionMenuDeserializer.cs b/src/RepoM.ActionMenu.Core/Yaml/Serialization/ActionMenuDeserializer.cs new file mode 100644 index 00000000..ae245ce1 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Serialization/ActionMenuDeserializer.cs @@ -0,0 +1,118 @@ +namespace RepoM.ActionMenu.Core.Yaml.Serialization; + +using System; +using System.Collections.Generic; +using RepoM.ActionMenu.Core.Model; +using RepoM.ActionMenu.Core.Yaml.Model; +using RepoM.ActionMenu.Core.Yaml.Model.ActionMenus.AssociateFile; +using RepoM.ActionMenu.Core.Yaml.Model.ActionMenus.BrowseRepository; +using RepoM.ActionMenu.Core.Yaml.Model.ActionMenus.Command; +using RepoM.ActionMenu.Core.Yaml.Model.ActionMenus.Folder; +using RepoM.ActionMenu.Core.Yaml.Model.ActionMenus.ForEach; +using RepoM.ActionMenu.Core.Yaml.Model.ActionMenus.JustText; +using RepoM.ActionMenu.Core.Yaml.Model.Ctx.EvaluateVariable; +using RepoM.ActionMenu.Core.Yaml.Model.Ctx.ExecuteScript; +using RepoM.ActionMenu.Core.Yaml.Model.Ctx.LoadFile; +using RepoM.ActionMenu.Core.Yaml.Model.Ctx.RendererVariable; +using RepoM.ActionMenu.Core.Yaml.Model.Ctx.SetVariable; +using RepoM.ActionMenu.Core.Yaml.Model.Tags; +using RepoM.ActionMenu.Core.Yaml.Model.Templating; +using RepoM.ActionMenu.Interface.YamlModel; +using RepoM.ActionMenu.Interface.YamlModel.Templating; +using YamlDotNet.Core; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.BufferedDeserialization; +using YamlDotNet.Serialization.NamingConventions; +using YamlDotNet.Serialization.NodeDeserializers; + +internal class ActionMenuDeserializer : IActionMenuDeserializer +{ + private readonly IDeserializer _deserializer; + private readonly ISerializer _serializer; + + public ActionMenuDeserializer() + { + var factoryMethods = new Dictionary> + { + { typeof(EvaluateBoolean), () => new ScribanEvaluateBoolean() }, + { typeof(RenderString), () => new ScribanRenderString() }, + { typeof(EvaluateInt), () => new ScribanEvaluateInt() }, + }; + + _serializer = new SerializerBuilder() + .WithNamingConvention(HyphenatedNamingConvention.Instance) // CamelCaseNamingConvention.Instance + .WithDefaultScalarStyle(ScalarStyle.Any) + .WithTypeConverter(new EvaluateObjectConverter(factoryMethods)) + .Build(); + + _deserializer = new DeserializerBuilder() + .WithNamingConvention(HyphenatedNamingConvention.Instance) + .WithTypeConverter(new EvaluateObjectConverter(factoryMethods)) + .WithTypeConverter(new DefaultContextActionTypeConverter()) + .WithTypeDiscriminatingNodeDeserializer(options => + { + options.AddTypeDiscriminator( + new KeyValueTypeDiscriminatorWithDefaultType( + "type", + new Dictionary + { + { EvaluateVariableContextAction.TypeValue, typeof(EvaluateVariableContextAction)}, + { RenderVariableContextAction.TypeValue, typeof(RenderVariableContextAction)}, + { ExecuteScript.TypeValue, typeof(ExecuteScript)}, + { SetVariableContextAction.TypeValue, typeof(SetVariableContextAction)}, + { LoadFileContextAction.TypeValue, typeof(LoadFileContextAction)}, + })); + + options.AddKeyValueTypeDiscriminator( + "type", + new Dictionary + { + { RepositoryActionAssociateFileV1.TypeValue, typeof(RepositoryActionAssociateFileV1) }, + { RepositoryActionJustTextV1.TypeValue, typeof(RepositoryActionJustTextV1) }, + { RepositoryActionFolderV1.TypeValue, typeof(RepositoryActionFolderV1) }, + { RepositoryActionBrowseRepositoryV1.TypeValue, typeof(RepositoryActionBrowseRepositoryV1) }, + { RepositoryActionCommandV1.TypeValue, typeof(RepositoryActionCommandV1) }, + { RepositoryActionForEachV1.TypeValue, typeof(RepositoryActionForEachV1) }, + }); + }, + maxDepth: -1, + maxLength: -1) + .WithTypeMapping() + .WithNodeDeserializer(inner => new TemplateUpdatingNodeDeserializer(inner), s => s.InsteadOf()) + .WithNodeDeserializer(inner => new TemplateUpdatingNodeDeserializer(inner), s => s.InsteadOf()) + .WithNodeDeserializer(inner => new TemplateUpdatingNodeDeserializer(inner), s => s.InsteadOf()) + .WithNodeDeserializer(inner => new TemplateUpdatingNodeDeserializer(inner), s => s.InsteadOf()) + .Build(); + } + + public Root DeserializeRoot(string content) + { + try + { + return _deserializer.Deserialize(content); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + public ContextRoot DeserializeContextRoot(string content) + { + try + { + return _deserializer.Deserialize(content); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + public string Serialize(Root root) + { + return _serializer.Serialize(root, typeof(Root)); + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Serialization/DefaultContextActionType.cs b/src/RepoM.ActionMenu.Core/Yaml/Serialization/DefaultContextActionType.cs new file mode 100644 index 00000000..edaf80a5 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Serialization/DefaultContextActionType.cs @@ -0,0 +1,7 @@ +namespace RepoM.ActionMenu.Core.Yaml.Serialization; + +using RepoM.ActionMenu.Core.Yaml.Model.Ctx.SetVariable; + +internal class DefaultContextActionType : SetVariableContextAction +{ +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Serialization/DefaultContextActionTypeConverter.cs b/src/RepoM.ActionMenu.Core/Yaml/Serialization/DefaultContextActionTypeConverter.cs new file mode 100644 index 00000000..58eb5560 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Serialization/DefaultContextActionTypeConverter.cs @@ -0,0 +1,46 @@ +namespace RepoM.ActionMenu.Core.Yaml.Serialization; + +using System; +using RepoM.ActionMenu.Core.Yaml.Model.Ctx.SetVariable; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; + +internal class DefaultContextActionTypeConverter : IYamlTypeConverter +{ + public bool Accepts(Type type) + { + return type == typeof(DefaultContextActionType); + } + + public object ReadYaml(IParser parser, Type type) + { + /* + parser.Current.GetType().Name "MappingStart" + parser.MoveNext(); + parser.Current.GetType().Name "Scalar" + parser.MoveNext(); + parser.Current.GetType().Name "Scalar" + parser.MoveNext(); + parser.Current.GetType().Name "MappingEnd" + */ + + // expect key value + parser.MoveNext(); + var key = ((Scalar)parser.Current).Value; + parser.MoveNext(); + var value = ((Scalar)parser.Current).Value; + parser.MoveNext(); + + return new SetVariableContextAction() + { + Name = key, + Value = value, + }; + } + + public void WriteYaml(IEmitter emitter, object? value, Type type) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Serialization/EvaluateObjectConverter.cs b/src/RepoM.ActionMenu.Core/Yaml/Serialization/EvaluateObjectConverter.cs new file mode 100644 index 00000000..a3d3e70f --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Serialization/EvaluateObjectConverter.cs @@ -0,0 +1,51 @@ +namespace RepoM.ActionMenu.Core.Yaml.Serialization; + +using System; +using System.Collections.Generic; +using System.Reflection; +using RepoM.ActionMenu.Interface.YamlModel.Templating; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; + +internal class EvaluateObjectConverter : IYamlTypeConverter +{ + private readonly Dictionary> _factory; + + public EvaluateObjectConverter(Dictionary> factory) + { + _factory = factory; + } + + public bool Accepts(Type type) + { + return typeof(EvaluateObject).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo()); + } + + public object ReadYaml(IParser parser, Type type) + { + var value = ((Scalar)parser.Current).Value; + parser.MoveNext(); + + EvaluateObject? obj; + if (_factory.TryGetValue(type, out Func? factoryMethod)) + { + obj = (EvaluateObject)factoryMethod.Invoke(); + } + else + { + obj = (EvaluateObject)Activator.CreateInstance(type)!; + } + + obj!.Value = value; + return obj; + } + + public void WriteYaml(IEmitter emitter, object? value, Type type) + { + var stringValue = (value as EvaluateObject)?.Value; + emitter.Emit(string.IsNullOrEmpty(stringValue) + ? new Scalar(AnchorName.Empty, TagName.Empty, string.Empty) + : new Scalar(stringValue!)); + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Serialization/KeyValueTypeDiscriminatorWithDefaultType.cs b/src/RepoM.ActionMenu.Core/Yaml/Serialization/KeyValueTypeDiscriminatorWithDefaultType.cs new file mode 100644 index 00000000..0bc7e047 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Serialization/KeyValueTypeDiscriminatorWithDefaultType.cs @@ -0,0 +1,33 @@ +namespace RepoM.ActionMenu.Core.Yaml.Serialization; + +using System; +using System.Collections.Generic; +using YamlDotNet.Core; +using YamlDotNet.Serialization.BufferedDeserialization.TypeDiscriminators; + +internal class KeyValueTypeDiscriminatorWithDefaultType : ITypeDiscriminator + where TInterface : class + where TDefaultImpl : class, TInterface +{ + private readonly KeyValueTypeDiscriminator _discriminator; + + public KeyValueTypeDiscriminatorWithDefaultType(string key, IDictionary mapping) + { + _discriminator = new KeyValueTypeDiscriminator(typeof(TInterface), key, mapping); + BaseType = typeof(TInterface); + } + + public bool TryDiscriminate(IParser buffer, out Type? suggestedType) + { + var result = _discriminator.TryDiscriminate(buffer, out suggestedType); + + if (!result) + { + suggestedType = typeof(TDefaultImpl); + } + + return true; + } + + public Type BaseType { get; } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Serialization/TemplateUpdatingNodeDeserializer.cs b/src/RepoM.ActionMenu.Core/Yaml/Serialization/TemplateUpdatingNodeDeserializer.cs new file mode 100644 index 00000000..6aa13748 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Serialization/TemplateUpdatingNodeDeserializer.cs @@ -0,0 +1,140 @@ +namespace RepoM.ActionMenu.Core.Yaml.Serialization; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using RepoM.ActionMenu.Core.Misc; +using RepoM.ActionMenu.Core.Yaml.Model.Templating; +using RepoM.ActionMenu.Interface.YamlModel.Templating; +using YamlDotNet.Core; +using YamlDotNet.Serialization; + +// ReSharper disable once UnusedTypeParameter, Justification: intentionally +internal class TemplateUpdatingNodeDeserializer : INodeDeserializer where T : class, INodeDeserializer +{ + private readonly INodeDeserializer _nodeDeserializer; + + public TemplateUpdatingNodeDeserializer(INodeDeserializer nodeDeserializer) + { + _nodeDeserializer = nodeDeserializer; + } + + public bool Deserialize(IParser reader, Type expectedType, Func nestedObjectDeserializer, out object? value) + { + if (!_nodeDeserializer.Deserialize(reader, expectedType, nestedObjectDeserializer, out value)) + { + return false; + } + + if (value == null) + { + return true; + } + + var props = expectedType + .GetProperties(true) + .Where(x => + x is { CanWrite: true, CanRead: true } && + typeof(EvaluateObject).GetTypeInfo().IsAssignableFrom(x.PropertyType.GetTypeInfo()) + ) + .ToArray(); + + var props2 = value.GetType().GetProperties(true).Where(x => + x is { CanWrite: true, CanRead: true } && + typeof(EvaluateObject).GetTypeInfo().IsAssignableFrom(x.PropertyType.GetTypeInfo()) + ) + .ToArray(); + + foreach (var prop in props.Concat(props2)) + { + var y = prop.GetMethod!.Invoke(value, null); + if (y == null) + { + continue; + } + + if (prop.PropertyType == typeof(EvaluateBoolean)) + { + var attribute = prop.GetCustomAttributesData().SingleOrDefault(a => + a.AttributeType.FullName == typeof(EvaluateToBooleanAttribute).FullName); + + if (attribute != null) + { + IList constructorArguments = attribute.ConstructorArguments; + + if (constructorArguments.Count == 1) + { + var defaultValue = (bool)constructorArguments[0].Value; + (y as EvaluateBoolean)!.DefaultValue = defaultValue; + } + } + } + + if (prop.PropertyType == typeof(RenderString)) + { + var attribute = prop.GetCustomAttributesData().SingleOrDefault(a => + a.AttributeType.FullName == typeof(RenderToStringAttribute).FullName); + + if (attribute != null) + { + var constructorArguments = attribute.ConstructorArguments; + + if (constructorArguments.Count == 1) + { + var defaultValue = (string)constructorArguments[0].Value; + (y as RenderString)!.DefaultValue = defaultValue; + } + } + } + + // if (Nullable.GetUnderlyingType(prop.PropertyType) == typeof(RenderString)) + if (prop.PropertyType == typeof(RenderString)) + { + var attribute = prop.GetCustomAttributesData().SingleOrDefault(a => + a.AttributeType.FullName == typeof(RenderToNullableStringAttribute).FullName); + + if (attribute != null) + { + var constructorArguments = attribute.ConstructorArguments; + + if (constructorArguments.Count == 1) + { + var defaultValue = (string?)constructorArguments[0].Value; + + if (defaultValue is not null) + { + if (y == null) + { + var newValue = new RenderString + { + Value = string.Empty + }; + y = newValue; + prop.SetMethod!.Invoke(value, new[] { newValue, }); + } + } + + if (y != null) + { + (y as RenderString)!.DefaultValue = defaultValue; + } + } + } + } + + if (prop.PropertyType == typeof(EvaluateInt)) + { + (y as EvaluateInt)!.DefaultValue = 1; + } + + + if (y is ICreateTemplate createTemplate) + { + createTemplate.CreateTemplate(new FixedTemplateParser()); + } + } + + return true; + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Core/Yaml/Serialization/YamlDotNetExtensions.cs b/src/RepoM.ActionMenu.Core/Yaml/Serialization/YamlDotNetExtensions.cs new file mode 100644 index 00000000..495d31a9 --- /dev/null +++ b/src/RepoM.ActionMenu.Core/Yaml/Serialization/YamlDotNetExtensions.cs @@ -0,0 +1,25 @@ +namespace RepoM.ActionMenu.Core.Yaml.Serialization; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +internal static class YamlDotNetExtensions +{ + public static IEnumerable GetProperties(this Type type, bool includeNonPublic) + { + BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public; + + if (includeNonPublic) + { + bindingFlags |= BindingFlags.NonPublic; + } + + return type.IsInterface + ? (new Type[] { type }) + .Concat(type.GetInterfaces()) + .SelectMany(i => i.GetProperties(bindingFlags)) + : type.GetProperties(bindingFlags); + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Interface/ActionMenuFactory/IActionMenuGenerationContext.cs b/src/RepoM.ActionMenu.Interface/ActionMenuFactory/IActionMenuGenerationContext.cs new file mode 100644 index 00000000..92c9bf5d --- /dev/null +++ b/src/RepoM.ActionMenu.Interface/ActionMenuFactory/IActionMenuGenerationContext.cs @@ -0,0 +1,24 @@ +namespace RepoM.ActionMenu.Interface.ActionMenuFactory; + +using System.Collections.Generic; +using System.IO.Abstractions; +using System.Threading.Tasks; +using RepoM.ActionMenu.Interface.UserInterface; +using RepoM.ActionMenu.Interface.YamlModel; +using RepoM.Core.Plugin.Repository; + +public interface IMenuContext +{ + IRepository Repository { get; } + + IFileSystem FileSystem { get; } +} + +public interface IActionMenuGenerationContext : ITemplateEvaluator, IMenuContext +{ + Task> AddActionMenusAsync(List? actionActions); + + IScope CreateGlobalScope(); + + IActionMenuGenerationContext Clone(); +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Interface/ActionMenuFactory/IContextMenuActionMenuGenerationContext.cs b/src/RepoM.ActionMenu.Interface/ActionMenuFactory/IContextMenuActionMenuGenerationContext.cs new file mode 100644 index 00000000..174200e1 --- /dev/null +++ b/src/RepoM.ActionMenu.Interface/ActionMenuFactory/IContextMenuActionMenuGenerationContext.cs @@ -0,0 +1,6 @@ +namespace RepoM.ActionMenu.Interface.ActionMenuFactory; + +public interface IContextMenuActionMenuGenerationContext : ITemplateEvaluator +{ + IScope CreateGlobalScope(); +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Interface/ActionMenuFactory/IScope.cs b/src/RepoM.ActionMenu.Interface/ActionMenuFactory/IScope.cs new file mode 100644 index 00000000..761fd89e --- /dev/null +++ b/src/RepoM.ActionMenu.Interface/ActionMenuFactory/IScope.cs @@ -0,0 +1,15 @@ +namespace RepoM.ActionMenu.Interface.ActionMenuFactory; + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using RepoM.ActionMenu.Interface.YamlModel; + +public interface IScope : IDisposable +{ + void SetValue(string member, object? value, bool @readonly); + + void PushEnvironmentVariable(Dictionary envVars); + + Task AddContextActionAsync(IContextAction contextItem); +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Interface/ActionMenuFactory/ITemplateEvaluator.cs b/src/RepoM.ActionMenu.Interface/ActionMenuFactory/ITemplateEvaluator.cs new file mode 100644 index 00000000..4b894e96 --- /dev/null +++ b/src/RepoM.ActionMenu.Interface/ActionMenuFactory/ITemplateEvaluator.cs @@ -0,0 +1,10 @@ +namespace RepoM.ActionMenu.Interface.ActionMenuFactory; + +using System.Threading.Tasks; + +public interface ITemplateEvaluator +{ + Task RenderStringAsync(string text); + + Task EvaluateAsync(string text); +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Interface/Attributes/ActionMenuMemberAttribute.cs b/src/RepoM.ActionMenu.Interface/Attributes/ActionMenuMemberAttribute.cs new file mode 100644 index 00000000..e47cb587 --- /dev/null +++ b/src/RepoM.ActionMenu.Interface/Attributes/ActionMenuMemberAttribute.cs @@ -0,0 +1,23 @@ +namespace RepoM.ActionMenu.Interface.Attributes; + +using System; + +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] +public class ActionMenuMemberAttribute : Attribute +{ + public ActionMenuMemberAttribute(string alias) : this(alias, string.Empty) + { + } + + public ActionMenuMemberAttribute(string alias, string category) + { + Alias = alias; + Category = category; + } + + public string Alias { get; } + + public string Category { get; } + + public bool Functor { get; set; } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Interface/Attributes/ActionMenuModuleAttribute.cs b/src/RepoM.ActionMenu.Interface/Attributes/ActionMenuModuleAttribute.cs new file mode 100644 index 00000000..f49797e9 --- /dev/null +++ b/src/RepoM.ActionMenu.Interface/Attributes/ActionMenuModuleAttribute.cs @@ -0,0 +1,14 @@ +namespace RepoM.ActionMenu.Interface.Attributes; + +using System; + +[AttributeUsage(AttributeTargets.Class)] +public class ActionMenuModuleAttribute : Attribute +{ + public ActionMenuModuleAttribute(string alias) + { + Alias = alias; + } + + public string Alias { get; } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Interface/Commands/BrowseRepositoryCommand.cs b/src/RepoM.ActionMenu.Interface/Commands/BrowseRepositoryCommand.cs new file mode 100644 index 00000000..a85bde90 --- /dev/null +++ b/src/RepoM.ActionMenu.Interface/Commands/BrowseRepositoryCommand.cs @@ -0,0 +1,11 @@ +namespace RepoM.ActionMenu.Interface.Commands; + +public sealed class BrowseRepositoryCommand : IRepositoryCommand +{ + public BrowseRepositoryCommand(string url) + { + Url = url; + } + + public string Url { get; } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Interface/Commands/IRepositoryCommand.cs b/src/RepoM.ActionMenu.Interface/Commands/IRepositoryCommand.cs new file mode 100644 index 00000000..d5ee6faa --- /dev/null +++ b/src/RepoM.ActionMenu.Interface/Commands/IRepositoryCommand.cs @@ -0,0 +1,8 @@ +namespace RepoM.ActionMenu.Interface.Commands; + +/// +/// Command to be executed on click. +/// +public interface IRepositoryCommand +{ +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Interface/Commands/NullRepositoryCommand.cs b/src/RepoM.ActionMenu.Interface/Commands/NullRepositoryCommand.cs new file mode 100644 index 00000000..68b47443 --- /dev/null +++ b/src/RepoM.ActionMenu.Interface/Commands/NullRepositoryCommand.cs @@ -0,0 +1,10 @@ +namespace RepoM.ActionMenu.Interface.Commands; + +public sealed class NullRepositoryCommand : IRepositoryCommand +{ + private NullRepositoryCommand() + { + } + + public static NullRepositoryCommand Instance { get; } = new(); +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Interface/Commands/StartProcessRepositoryCommand.cs b/src/RepoM.ActionMenu.Interface/Commands/StartProcessRepositoryCommand.cs new file mode 100644 index 00000000..4b032c2c --- /dev/null +++ b/src/RepoM.ActionMenu.Interface/Commands/StartProcessRepositoryCommand.cs @@ -0,0 +1,15 @@ +namespace RepoM.ActionMenu.Interface.Commands; + +public sealed class StartProcessRepositoryCommand : IRepositoryCommand +{ + // ProcessHelper.StartProcess(command, arguments) + public StartProcessRepositoryCommand(string command, string arguments) + { + Command = command; + Arguments = arguments; + } + + public string Command { get; } + + public string Arguments { get; } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Interface/RepoM.ActionMenu.Interface.csproj b/src/RepoM.ActionMenu.Interface/RepoM.ActionMenu.Interface.csproj new file mode 100644 index 00000000..fd331573 --- /dev/null +++ b/src/RepoM.ActionMenu.Interface/RepoM.ActionMenu.Interface.csproj @@ -0,0 +1,11 @@ + + + + net7.0 + + + + + + + diff --git a/src/RepoM.ActionMenu.Interface/Scriban/IContextRegistration.cs b/src/RepoM.ActionMenu.Interface/Scriban/IContextRegistration.cs new file mode 100644 index 00000000..ff4af1fe --- /dev/null +++ b/src/RepoM.ActionMenu.Interface/Scriban/IContextRegistration.cs @@ -0,0 +1,40 @@ +namespace RepoM.ActionMenu.Interface.Scriban; + +using System; + +public interface IContextRegistration +{ + IContextRegistration CreateOrGetSubRegistration(string key); + + void SetValue(string member, object value, bool readOnly); + + void Add(string key, object value); + + bool ContainsKey(string key); + + void RegisterConstant(string name, object value); + + void RegisterAction(string name, Action action); + + void RegisterAction(string name, Action action); + + void RegisterAction(string name, Action action); + + void RegisterAction(string name, Action action); + + void RegisterAction(string name, Action action); + + void RegisterFunction(string name, Func func); + + void RegisterFunction(string name, Func func); + + void RegisterFunction(string name, Func func); + + void RegisterFunction(string name, Func func); + + void RegisterFunction(string name, Func func); + + void RegisterFunction(string name, Func func); + + void RegisterVariable(string name, object value); +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Interface/Scriban/ITemplateContextRegistration.cs b/src/RepoM.ActionMenu.Interface/Scriban/ITemplateContextRegistration.cs new file mode 100644 index 00000000..ca280c4e --- /dev/null +++ b/src/RepoM.ActionMenu.Interface/Scriban/ITemplateContextRegistration.cs @@ -0,0 +1,13 @@ +namespace RepoM.ActionMenu.Interface.Scriban; + +public abstract class TemplateContextRegistrationBase : ITemplateContextRegistration +{ + public virtual void RegisterFunctionsAuto(IContextRegistration contextRegistration) + { + } +} + +public interface ITemplateContextRegistration +{ + void RegisterFunctionsAuto(IContextRegistration contextRegistration); +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Interface/UserInterface/UserInterfaceRepositoryActionBase.cs b/src/RepoM.ActionMenu.Interface/UserInterface/UserInterfaceRepositoryActionBase.cs new file mode 100644 index 00000000..401a3582 --- /dev/null +++ b/src/RepoM.ActionMenu.Interface/UserInterface/UserInterfaceRepositoryActionBase.cs @@ -0,0 +1,24 @@ +namespace RepoM.ActionMenu.Interface.UserInterface; + +using System; +using System.Collections.Generic; +using RepoM.ActionMenu.Interface.Commands; +using RepoM.Core.Plugin.Repository; + +public abstract class UserInterfaceRepositoryActionBase +{ + protected UserInterfaceRepositoryActionBase(IRepository repository) + { + Repository = repository ?? throw new ArgumentNullException(nameof(repository)); + } + + public IRepositoryCommand RepositoryCommand { get; init; } = NullRepositoryCommand.Instance; + + public IRepository Repository { get; } + + public bool ExecutionCausesSynchronizing { get; init; } + + public bool CanExecute { get; init; } = true; + + public IEnumerable? SubActions { get; init; } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Interface/YamlModel/ActionToRepositoryActionMapperBase.cs b/src/RepoM.ActionMenu.Interface/YamlModel/ActionToRepositoryActionMapperBase.cs new file mode 100644 index 00000000..48833509 --- /dev/null +++ b/src/RepoM.ActionMenu.Interface/YamlModel/ActionToRepositoryActionMapperBase.cs @@ -0,0 +1,21 @@ +namespace RepoM.ActionMenu.Interface.YamlModel; + +using System.Collections.Generic; +using RepoM.ActionMenu.Interface.ActionMenuFactory; +using RepoM.ActionMenu.Interface.UserInterface; +using RepoM.Core.Plugin.Repository; + +public abstract class ActionToRepositoryActionMapperBase : IActionToRepositoryActionMapper where T : IMenuAction +{ + public bool CanMap(IMenuAction action) + { + return action is T; + } + + public IAsyncEnumerable MapAsync(IMenuAction action, IActionMenuGenerationContext context, IRepository repository) + { + return MapAsync((T)action, context, repository); + } + + protected abstract IAsyncEnumerable MapAsync(T action, IActionMenuGenerationContext context, IRepository repository); +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Interface/YamlModel/IActionToRepositoryActionMapper.cs b/src/RepoM.ActionMenu.Interface/YamlModel/IActionToRepositoryActionMapper.cs new file mode 100644 index 00000000..b0bcb602 --- /dev/null +++ b/src/RepoM.ActionMenu.Interface/YamlModel/IActionToRepositoryActionMapper.cs @@ -0,0 +1,13 @@ +namespace RepoM.ActionMenu.Interface.YamlModel; + +using System.Collections.Generic; +using RepoM.ActionMenu.Interface.ActionMenuFactory; +using RepoM.ActionMenu.Interface.UserInterface; +using RepoM.Core.Plugin.Repository; + +public interface IActionToRepositoryActionMapper +{ + bool CanMap(IMenuAction action); + + IAsyncEnumerable MapAsync(IMenuAction action, IActionMenuGenerationContext context, IRepository repository); +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Interface/YamlModel/IContextAction.cs b/src/RepoM.ActionMenu.Interface/YamlModel/IContextAction.cs new file mode 100644 index 00000000..4ce09513 --- /dev/null +++ b/src/RepoM.ActionMenu.Interface/YamlModel/IContextAction.cs @@ -0,0 +1,6 @@ +namespace RepoM.ActionMenu.Interface.YamlModel; + +public interface IContextAction +{ + string Type { get; } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Interface/YamlModel/IMenuAction.cs b/src/RepoM.ActionMenu.Interface/YamlModel/IMenuAction.cs new file mode 100644 index 00000000..c5c39a38 --- /dev/null +++ b/src/RepoM.ActionMenu.Interface/YamlModel/IMenuAction.cs @@ -0,0 +1,8 @@ +namespace RepoM.ActionMenu.Interface.YamlModel; + +public interface IMenuAction +{ + string Type { get; } + + string? Active { get; } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Interface/YamlModel/Templating/EvaluateBoolean.cs b/src/RepoM.ActionMenu.Interface/YamlModel/Templating/EvaluateBoolean.cs new file mode 100644 index 00000000..eedb16fc --- /dev/null +++ b/src/RepoM.ActionMenu.Interface/YamlModel/Templating/EvaluateBoolean.cs @@ -0,0 +1,59 @@ +namespace RepoM.ActionMenu.Interface.YamlModel.Templating; + +using System; +using System.Threading.Tasks; +using RepoM.ActionMenu.Interface.ActionMenuFactory; + +public class EvaluateBoolean : EvaluateObject +{ + public bool DefaultValue { get; set; } + + public static implicit operator EvaluateBoolean(string content) + { + return new EvaluateBoolean { Value = content }; + } + + public override string ToString() + { + return $"EvaluateBoolean {base.ToString()} : {DefaultValue}"; + } + + public virtual async Task EvaluateAsync(ITemplateEvaluator instance) + { + var result = await instance.EvaluateAsync(Value).ConfigureAwait(false); + return ToBool(result); + } + + protected bool ToBool(object value) + { + if (value == null) + { + return DefaultValue; + } + + if (value is bool boolValue) + { + return boolValue; + } + + if (value is int intValue) + { + return Convert.ToBoolean(intValue); + } + + if (value is string stringValue) + { + if (bool.TryParse(stringValue, out var stringBoolValue)) + { + return stringBoolValue; + } + + if (int.TryParse(stringValue, out int stringIntValue)) + { + return Convert.ToBoolean(stringIntValue); + } + } + + return DefaultValue; + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Interface/YamlModel/Templating/EvaluateInt.cs b/src/RepoM.ActionMenu.Interface/YamlModel/Templating/EvaluateInt.cs new file mode 100644 index 00000000..f5137b23 --- /dev/null +++ b/src/RepoM.ActionMenu.Interface/YamlModel/Templating/EvaluateInt.cs @@ -0,0 +1,36 @@ +namespace RepoM.ActionMenu.Interface.YamlModel.Templating; + +using System.Threading.Tasks; +using RepoM.ActionMenu.Interface.ActionMenuFactory; + +public class EvaluateInt : EvaluateObject +{ + public int DefaultValue { get; set; } + + public static implicit operator EvaluateInt(string content) + { + return new EvaluateInt { Value = content }; + } + + public override string ToString() + { + return $"EvaluateInt {base.ToString()} : {DefaultValue}"; + } + + public virtual async Task EvaluateAsync(ITemplateEvaluator instance) + { + var result = await instance.EvaluateAsync(Value).ConfigureAwait(false); + + if (result == null) + { + return DefaultValue; + } + + if (result is int i) + { + return i; + } + + return DefaultValue; + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Interface/YamlModel/Templating/EvaluateObject.cs b/src/RepoM.ActionMenu.Interface/YamlModel/Templating/EvaluateObject.cs new file mode 100644 index 00000000..213b3feb --- /dev/null +++ b/src/RepoM.ActionMenu.Interface/YamlModel/Templating/EvaluateObject.cs @@ -0,0 +1,17 @@ +namespace RepoM.ActionMenu.Interface.YamlModel.Templating; + +public abstract class EvaluateObject +{ + public string Value { get; set; } + + public override string ToString() + { + var value = Value; + if (value.Length > 10) + { + value = value[..10] + ".."; + } + + return $"{value}"; + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Interface/YamlModel/Templating/EvaluateToAttribute.cs b/src/RepoM.ActionMenu.Interface/YamlModel/Templating/EvaluateToAttribute.cs new file mode 100644 index 00000000..ec8a06bf --- /dev/null +++ b/src/RepoM.ActionMenu.Interface/YamlModel/Templating/EvaluateToAttribute.cs @@ -0,0 +1,7 @@ +namespace RepoM.ActionMenu.Interface.YamlModel.Templating; + +using System; + +public abstract class EvaluateToAttribute : Attribute +{ +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Interface/YamlModel/Templating/EvaluateToBooleanAttribute.cs b/src/RepoM.ActionMenu.Interface/YamlModel/Templating/EvaluateToBooleanAttribute.cs new file mode 100644 index 00000000..ae326f8a --- /dev/null +++ b/src/RepoM.ActionMenu.Interface/YamlModel/Templating/EvaluateToBooleanAttribute.cs @@ -0,0 +1,14 @@ +namespace RepoM.ActionMenu.Interface.YamlModel.Templating; + +using System; + +[AttributeUsage(AttributeTargets.Property)] +public sealed class EvaluateToBooleanAttribute : EvaluateToAttribute +{ + public EvaluateToBooleanAttribute(bool defaultValue) + { + DefaultValue = defaultValue; + } + + public bool DefaultValue { get; } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Interface/YamlModel/Templating/EvaluateToObjectAttribute.cs b/src/RepoM.ActionMenu.Interface/YamlModel/Templating/EvaluateToObjectAttribute.cs new file mode 100644 index 00000000..d9c8b8bc --- /dev/null +++ b/src/RepoM.ActionMenu.Interface/YamlModel/Templating/EvaluateToObjectAttribute.cs @@ -0,0 +1,11 @@ +namespace RepoM.ActionMenu.Interface.YamlModel.Templating; + +using System; + +[AttributeUsage(AttributeTargets.Property)] +public sealed class EvaluateToObjectAttribute : EvaluateToAttribute +{ + public EvaluateToObjectAttribute(Type type) + { + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Interface/YamlModel/Templating/RenderString.cs b/src/RepoM.ActionMenu.Interface/YamlModel/Templating/RenderString.cs new file mode 100644 index 00000000..54f58f78 --- /dev/null +++ b/src/RepoM.ActionMenu.Interface/YamlModel/Templating/RenderString.cs @@ -0,0 +1,24 @@ +namespace RepoM.ActionMenu.Interface.YamlModel.Templating; + +using System.Threading.Tasks; +using RepoM.ActionMenu.Interface.ActionMenuFactory; + +public class RenderString : EvaluateObject +{ + public string DefaultValue { get; set; } = string.Empty; + + public static implicit operator RenderString(string content) + { + return new RenderString { Value = content }; + } + + public override string ToString() + { + return $"RenderString {base.ToString()} : {DefaultValue}"; + } + + public virtual async Task RenderAsync(ITemplateEvaluator instance) + { + return await instance.RenderStringAsync(Value).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Interface/YamlModel/Templating/RenderToNullableStringAttribute.cs b/src/RepoM.ActionMenu.Interface/YamlModel/Templating/RenderToNullableStringAttribute.cs new file mode 100644 index 00000000..6acac85f --- /dev/null +++ b/src/RepoM.ActionMenu.Interface/YamlModel/Templating/RenderToNullableStringAttribute.cs @@ -0,0 +1,14 @@ +namespace RepoM.ActionMenu.Interface.YamlModel.Templating; + +using System; + +[AttributeUsage(AttributeTargets.Property)] +public sealed class RenderToNullableStringAttribute : EvaluateToAttribute +{ + public RenderToNullableStringAttribute(string? defaultValue) + { + DefaultValue = defaultValue; + } + + public string? DefaultValue { get; } +} \ No newline at end of file diff --git a/src/RepoM.ActionMenu.Interface/YamlModel/Templating/RenderToStringAttribute.cs b/src/RepoM.ActionMenu.Interface/YamlModel/Templating/RenderToStringAttribute.cs new file mode 100644 index 00000000..dd0e4ea2 --- /dev/null +++ b/src/RepoM.ActionMenu.Interface/YamlModel/Templating/RenderToStringAttribute.cs @@ -0,0 +1,14 @@ +namespace RepoM.ActionMenu.Interface.YamlModel.Templating; + +using System; + +[AttributeUsage(AttributeTargets.Property)] +public sealed class RenderToStringAttribute : EvaluateToAttribute +{ + public RenderToStringAttribute(string defaultValue) + { + DefaultValue = defaultValue; + } + + public string DefaultValue { get; } +} \ No newline at end of file diff --git a/src/RepoM.Api/RepoM.Api.csproj b/src/RepoM.Api/RepoM.Api.csproj index 8f249321..c2f961f7 100644 --- a/src/RepoM.Api/RepoM.Api.csproj +++ b/src/RepoM.Api/RepoM.Api.csproj @@ -14,7 +14,7 @@ - + diff --git a/tests/RepoM.ActionMenu.Core.Tests/BooleanWithoutXTests.GetTags.verified.txt b/tests/RepoM.ActionMenu.Core.Tests/BooleanWithoutXTests.GetTags.verified.txt new file mode 100644 index 00000000..d9cce283 --- /dev/null +++ b/tests/RepoM.ActionMenu.Core.Tests/BooleanWithoutXTests.GetTags.verified.txt @@ -0,0 +1,5 @@ +[ + private, + github, + work +] \ No newline at end of file diff --git a/tests/RepoM.ActionMenu.Core.Tests/BooleanWithoutXTests.Serialize.verified.txt b/tests/RepoM.ActionMenu.Core.Tests/BooleanWithoutXTests.Serialize.verified.txt new file mode 100644 index 00000000..fe641542 --- /dev/null +++ b/tests/RepoM.ActionMenu.Core.Tests/BooleanWithoutXTests.Serialize.verified.txt @@ -0,0 +1,149 @@ +tags: +- tag: coen1 + when: 1 == 1 +- tag: coentje2 + when: 1 == 2 +- tag: coentje3 + when: 1 == 2 || true +- tag: rian + when: '' +action-menu: +- type: just-text@1 + text: repository.pwd2 {{ repository.pwd }} + active: + enabled: + context: +- type: foreach@1 + active: + enumerable: file.find_files("c:\\", "*.env") + variable: file + skip: + actions: + - type: just-text@1 + text: file name {{ file }} + active: + enabled: + context: +- type: foreach@1 + active: + enumerable: file.pwd2("c:\\", "*.env") + variable: file + skip: + actions: + - type: just-text@1 + text: file name {{ file }} + active: + enabled: + context: +- type: foreach@1 + active: + enumerable: devopsEnvironments + variable: environment + skip: + actions: + - type: just-text@1 + text: env name {{ environment.name }} env(DEF):`{{ env.DEF }}` + active: + enabled: + context: +- type: associate-file@1 + name: Open {{ name }} in {{ 'visual' | string.upcase }} Studio Code {{ sub1(10,3) }} + extension: .cs + active: 1 == 1 +- type: just-text@1 + text: Text {{ name }} in {{ 'visual' | string.upcase }} Studio Code {{ sub1(10,3) }} {{ link }} env(DEF) {{ env.DEF }} + active: 1 <= 3 + enabled: + context: + - type: evaluate-variable@1 + value: sonar_url "sf23-2" + enabled: + name: link + - type: load-file@1 + filename: C:\SubV2.yaml + enabled: + name: '' + - type: load-file@1 + filename: C:\file2.env + enabled: + name: '' +- type: folder@1 + actions: + - type: just-text@1 + text: Text {{ name }} in {{ 'visual' | string.upcase }} Studio Code {{ sub1(10,3) }} {{repository.is_starred}} + active: 1 <= 3 + enabled: + context: + - type: set-variable@1 + value: namenamename + enabled: + name: name2 + name: my-folder + active: + context: + - type: set-variable@1 + value: coenm23 + enabled: + name: name + is-deferred: +context: +- type: set-variable@1 + value: coenm + enabled: + name: name +- type: set-variable@1 + value: coenm1 + enabled: + name: name1 +- type: set-variable@1 + value: + - name: Develop + url: -d.bdodt.nl + - name: Test + url: -t.bdodt.nl + - name: Acceptation + url: -a.bdo.nl + - name: Production + url: .bdo.nl + enabled: + name: devopsEnvironments +- type: evaluate-script@1 + content: >- + a = 'beer'; + + b = 'wine'; + + name = name + ' drinks a lot of ' + a + ' and ' + b; + + + now2 = date.now; + + + my_age = 39; + + + func sub1 + ret $0 - $1 + end + + + func sub2(x,y) + ret x - y + 10 + end + + + func sonar_url(project_id) + ret 'https://sonarcloud.io/project/overview?id=' + project_id; + end + + + dummy_calc = sub2(19, 3); + enabled: +- type: load-file@1 + filename: C:\file1.env + enabled: + name: '' +- type: render-variable@1 + value: text `{{ name }}` text2 + enabled: 1 == 1 + name: my-text diff --git a/tests/RepoM.ActionMenu.Core.Tests/BooleanWithoutXTests.UseFactory.verified.txt b/tests/RepoM.ActionMenu.Core.Tests/BooleanWithoutXTests.UseFactory.verified.txt new file mode 100644 index 00000000..fc53b41f --- /dev/null +++ b/tests/RepoM.ActionMenu.Core.Tests/BooleanWithoutXTests.UseFactory.verified.txt @@ -0,0 +1,186 @@ +[ + { + Name: repository.pwd2 , + RepositoryCommand: {}, + Repository: { + SafePath: dummy safe path, + HasUnpushedChanges: false, + Name: dummy name, + Path: dummy path, + Location: dummy location, + CurrentBranch: dummy current branch + }, + ExecutionCausesSynchronizing: false, + CanExecute: true + }, + { + Name: file name C:\file1.env, + RepositoryCommand: {}, + Repository: { + SafePath: dummy safe path, + HasUnpushedChanges: false, + Name: dummy name, + Path: dummy path, + Location: dummy location, + CurrentBranch: dummy current branch + }, + ExecutionCausesSynchronizing: false, + CanExecute: true + }, + { + Name: file name C:\file2.env, + RepositoryCommand: {}, + Repository: { + SafePath: dummy safe path, + HasUnpushedChanges: false, + Name: dummy name, + Path: dummy path, + Location: dummy location, + CurrentBranch: dummy current branch + }, + ExecutionCausesSynchronizing: false, + CanExecute: true + }, + { + Name: file name C:\file1.env, + RepositoryCommand: {}, + Repository: { + SafePath: dummy safe path, + HasUnpushedChanges: false, + Name: dummy name, + Path: dummy path, + Location: dummy location, + CurrentBranch: dummy current branch + }, + ExecutionCausesSynchronizing: false, + CanExecute: true + }, + { + Name: file name C:\file2.env, + RepositoryCommand: {}, + Repository: { + SafePath: dummy safe path, + HasUnpushedChanges: false, + Name: dummy name, + Path: dummy path, + Location: dummy location, + CurrentBranch: dummy current branch + }, + ExecutionCausesSynchronizing: false, + CanExecute: true + }, + { + Name: env name Develop env(DEF):`my def 12`, + RepositoryCommand: {}, + Repository: { + SafePath: dummy safe path, + HasUnpushedChanges: false, + Name: dummy name, + Path: dummy path, + Location: dummy location, + CurrentBranch: dummy current branch + }, + ExecutionCausesSynchronizing: false, + CanExecute: true + }, + { + Name: env name Test env(DEF):`my def 12`, + RepositoryCommand: {}, + Repository: { + SafePath: dummy safe path, + HasUnpushedChanges: false, + Name: dummy name, + Path: dummy path, + Location: dummy location, + CurrentBranch: dummy current branch + }, + ExecutionCausesSynchronizing: false, + CanExecute: true + }, + { + Name: env name Acceptation env(DEF):`my def 12`, + RepositoryCommand: {}, + Repository: { + SafePath: dummy safe path, + HasUnpushedChanges: false, + Name: dummy name, + Path: dummy path, + Location: dummy location, + CurrentBranch: dummy current branch + }, + ExecutionCausesSynchronizing: false, + CanExecute: true + }, + { + Name: env name Production env(DEF):`my def 12`, + RepositoryCommand: {}, + Repository: { + SafePath: dummy safe path, + HasUnpushedChanges: false, + Name: dummy name, + Path: dummy path, + Location: dummy location, + CurrentBranch: dummy current branch + }, + ExecutionCausesSynchronizing: false, + CanExecute: true + }, + { + Name: Open coenm drinks a lot of beer and wine in VISUAL Studio Code 7, + RepositoryCommand: {}, + Repository: { + SafePath: dummy safe path, + HasUnpushedChanges: false, + Name: dummy name, + Path: dummy path, + Location: dummy location, + CurrentBranch: dummy current branch + }, + ExecutionCausesSynchronizing: false, + CanExecute: true + }, + { + Name: Text coenm drinks a lot of beer and winesub coen in VISUAL Studio Code 7 https://sonarcloud.io/project/overview?id=sf23-2 env(DEF) my def 12-second!, + RepositoryCommand: {}, + Repository: { + SafePath: dummy safe path, + HasUnpushedChanges: false, + Name: dummy name, + Path: dummy path, + Location: dummy location, + CurrentBranch: dummy current branch + }, + ExecutionCausesSynchronizing: false, + CanExecute: true + }, + { + Name: my-folder, + RepositoryCommand: {}, + Repository: { + SafePath: dummy safe path, + HasUnpushedChanges: false, + Name: dummy name, + Path: dummy path, + Location: dummy location, + CurrentBranch: dummy current branch + }, + ExecutionCausesSynchronizing: false, + CanExecute: true, + SubActions: [ + { + Name: Text coenm23 in VISUAL Studio Code 7 , + RepositoryCommand: {}, + Repository: { + SafePath: dummy safe path, + HasUnpushedChanges: false, + Name: dummy name, + Path: dummy path, + Location: dummy location, + CurrentBranch: dummy current branch + }, + ExecutionCausesSynchronizing: false, + CanExecute: true + } + ] + } +] \ No newline at end of file diff --git a/tests/RepoM.ActionMenu.Core.Tests/DummyRepository.cs b/tests/RepoM.ActionMenu.Core.Tests/DummyRepository.cs new file mode 100644 index 00000000..fd791128 --- /dev/null +++ b/tests/RepoM.ActionMenu.Core.Tests/DummyRepository.cs @@ -0,0 +1,19 @@ +namespace RepoM.ActionMenu.Core.Tests; + +using System; +using System.Collections.Generic; +using RepoM.Core.Plugin.Repository; + +public class DummyRepository : IRepository +{ + public string SafePath { get; } = "dummy safe path"; + public List Remotes { get; } = new List(); + public bool HasUnpushedChanges { get; } = false; + public string Name { get; } = "dummy name"; + public string Path { get; } = "dummy path"; + public string Location { get; } = "dummy location"; + public string CurrentBranch { get; } = "dummy current branch"; + public string[] Branches { get; } = Array.Empty(); + public string[] LocalBranches { get; } = Array.Empty(); + public string[] Tags { get; } +} \ No newline at end of file diff --git a/tests/RepoM.ActionMenu.Core.Tests/RepoM.ActionMenu.Core.Tests.csproj b/tests/RepoM.ActionMenu.Core.Tests/RepoM.ActionMenu.Core.Tests.csproj new file mode 100644 index 00000000..72cb89f7 --- /dev/null +++ b/tests/RepoM.ActionMenu.Core.Tests/RepoM.ActionMenu.Core.Tests.csproj @@ -0,0 +1,43 @@ + + + + net7.0 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/tests/RepoM.ActionMenu.Core.Tests/UnitTest1.cs b/tests/RepoM.ActionMenu.Core.Tests/UnitTest1.cs new file mode 100644 index 00000000..8896dcbb --- /dev/null +++ b/tests/RepoM.ActionMenu.Core.Tests/UnitTest1.cs @@ -0,0 +1,239 @@ +namespace RepoM.ActionMenu.Core.Tests +{ + using System.Collections.Generic; + using System.IO.Abstractions.TestingHelpers; + using System.Text; + using System.Threading.Tasks; + using RepoM.ActionMenu.Core.PublicApi; + using RepoM.ActionMenu.Core.Yaml.Model; + using RepoM.ActionMenu.Core.Yaml.Model.Ctx; + using RepoM.ActionMenu.Core.Yaml.Model.Ctx.ExecuteScript; + using RepoM.ActionMenu.Core.Yaml.Model.Ctx.RendererVariable; + using RepoM.ActionMenu.Core.Yaml.Model.Ctx.SetVariable; + using RepoM.ActionMenu.Core.Yaml.Serialization; + using RepoM.ActionMenu.Interface.Scriban; + using RepoM.Core.Plugin.Repository; + using VerifyXunit; + using Xunit; + + [UsesVerify] + public class BooleanWithoutXTests + { + private readonly IRepository _repository = new DummyRepository(); + + private const string File1Env = + """ + # just a comment + ABC=my abc + + # comment 2 + DEF=my def 12 + """; + + private const string File2Env = + """ + DEF=my def 12-second! + GHI=GHI GHI GHI-second + """; + + private const string Sub = + """ + context: + # - type: set-variable@1 + # name: name + # value: subcoen + + - type: evaluate-script@1 + content: |- + name = name + 'sub coen' + ax = 'beerx'; + bx = 'winex'; + """; + + private const string Yaml = + """ + context: + - name: coenm + - name1: coenm1 + + - type: set-variable@1 + name: devopsEnvironments + value: + - name: Develop + url: '-d.bdodt.nl' + - name: Test + url: '-t.bdodt.nl' + - name: Acceptation + url: '-a.bdo.nl' + - name: Production + url: '.bdo.nl' + + - type: evaluate-script@1 + content: |- + a = 'beer'; + b = 'wine'; + name = name + ' drinks a lot of ' + a + ' and ' + b; + + now2 = date.now; + + my_age = 39; + + func sub1 + ret $0 - $1 + end + + func sub2(x,y) + ret x - y + 10 + end + + func sonar_url(project_id) + ret 'https://sonarcloud.io/project/overview?id=' + project_id; + end + + dummy_calc = sub2(19, 3); + + - type: load-file@1 + filename: 'C:\file1.env' + + - type: render-variable@1 + name: my-text + value: text `{{ name }}` text2 + enabled: 1 == 1 + + tags: + - tag: private + when: 1 == 1 + + - tag: github + when: 1 == 2 + + - tag: github + when: 1 == 2 || true + + - tag: work + + action-menu: + - type: just-text@1 + text: 'repository.pwd2 {{ repository.pwd }}' + + - type: foreach@1 + enumerable: 'file.find_files("c:\\", "*.env")' + variable: file + actions: + - type: just-text@1 + text: 'file name {{ file }}' + + - type: foreach@1 + enumerable: 'file.pwd2("c:\\", "*.env")' + variable: file + actions: + - type: just-text@1 + text: 'file name {{ file }}' + + + - type: foreach@1 + enumerable: devopsEnvironments + variable: environment + actions: + - type: just-text@1 + text: 'env name {{ environment.name }} env(DEF):`{{ env.DEF }}`' + + - type: associate-file@1 + name: Open {{ name }} in {{ 'visual' | string.upcase }} Studio Code {{ sub1(10,3) }} + extension: .cs + active: 1 == 1 + + - type: just-text@1 + text: Text {{ name }} in {{ 'visual' | string.upcase }} Studio Code {{ sub1(10,3) }} {{ link }} env(DEF) {{ env.DEF }} + active: 1 <= 3 + context: + - type: evaluate-variable@1 + name: link + value: 'sonar_url "sf23-2"' + - type: load-file@1 + filename: 'C:\SubV2.yaml' + - type: load-file@1 + filename: 'C:\file2.env' + - type: folder@1 + name: my-folder + context: + - type: set-variable@1 + name: name + value: coenm23 + actions: + - type: just-text@1 + text: Text {{ name }} in {{ 'visual' | string.upcase }} Studio Code {{ sub1(10,3) }} {{repository.is_starred}} + active: 1 <= 3 + context: + - type: set-variable@1 + name: name2 + value: namenamename + """; + + [Fact] + public async Task UseFactory() + { + var fileSystem = new MockFileSystem( + new Dictionary() + { + { "C:\\RepositoryActionsV2.yaml", new MockFileData(Yaml, Encoding.UTF8) }, + { "C:\\SubV2.yaml", new MockFileData(Sub, Encoding.UTF8) }, + { "C:\\file1.env", new MockFileData(File1Env, Encoding.UTF8) }, + { "C:\\file2.env", new MockFileData(File2Env, Encoding.UTF8) }, + }); + var sut = new Factory(fileSystem, new ITemplateContextRegistration[0]); + var factory = sut.Create("C:\\RepositoryActionsV2.yaml"); + + var result = await factory.CreateMenuAsync(_repository); + + await Verifier.Verify(result); + } + + [Fact] + public async Task GetTags() + { + var fileSystem = new MockFileSystem( + new Dictionary() + { + { "C:\\RepositoryActionsV2.yaml", new MockFileData(Yaml, Encoding.UTF8) }, + { "C:\\SubV2.yaml", new MockFileData(Sub, Encoding.UTF8) }, + { "C:\\file1.env", new MockFileData(File1Env, Encoding.UTF8) }, + { "C:\\file2.env", new MockFileData(File2Env, Encoding.UTF8) }, + }); + var sut = new Factory(fileSystem, new ITemplateContextRegistration[0]); + var factory = sut.Create("C:\\RepositoryActionsV2.yaml"); + + var result = await factory.GetTagsAsync(_repository); + + await Verifier.Verify(result); + } + + [Fact] + public async Task Serialize() + { + var root = new Root() + { + Context = new Context + { + SetVariableContextAction.Create("name", "coenm"), + new ExecuteScript + { + Content = @"this is text +some more text" + }, + new RenderVariableContextAction + { + Name = "render", + Value = "text {{ name }} text2", + } + }, + }; + + var deserializer = new ActionMenuDeserializer(); + var result2 = deserializer.DeserializeRoot(Yaml); + var result = deserializer.Serialize(result2); + + await Verifier.Verify(result); + } + } +} \ No newline at end of file From 967b3f8a91c885bcf34218d0e9259d7313cc6ab2 Mon Sep 17 00:00:00 2001 From: Coen van den Munckhof Date: Sat, 14 Oct 2023 21:16:00 +0200 Subject: [PATCH 002/430] WIP --- docs_new/file.generated.md | 67 +++++++- docs_new/strings.generated.md | 22 +++ .../KalkDescriptor.cs | 5 +- .../KalkParamDescriptor.cs | 7 + src/RepoM.ActionMenu.CodeGen/Program.cs | 151 +++++++++++++++--- .../Model/ActionMenuGenerationContext.cs | 2 +- .../Model/Functions/FileFunctions.cs | 62 ++++--- 7 files changed, 269 insertions(+), 47 deletions(-) diff --git a/docs_new/file.generated.md b/docs_new/file.generated.md index a3d6cf93..a1b01f06 100644 --- a/docs_new/file.generated.md +++ b/docs_new/file.generated.md @@ -35,6 +35,7 @@ out = true out = false ``` + ## file_exists `file_exists(path)` @@ -60,6 +61,7 @@ out = false out = true ``` + ## find_files `find_files(rootPath,searchPattern)` @@ -75,18 +77,69 @@ Returns an enumerable collection of full paths of the files or directories that ### Example -```kalk -firstsecond +text without para +#### Input +```scriban find_files 'C:\Users\coenm\RepoM\src' '*.sln' +find_files('C:\Users\coenm\RepoM\src','*.sln') +``` + +#### Result + +``` +1aa349585ed7ecbd3b9c486a30067e395ca4b356 +``` +### Example -xxx - sdf -find_files 'C:\Users\coenm\RepoM\src' '*.csproj' -[1, 2, 3, 4] +text without para2 + +#### Input +```scriban +find_files 'C:\Users\coenm\RepoM\src' '*.txt' +find_files('C:\Users\coenm\RepoM\src','*.txt') +``` + +#### Result + +``` +[] ``` ## find_files_interface -`find_files_interface` +`find_files_interface(rootPath,searchPattern)` + +Find files in a given directory based on the search pattern. Resulting filenames are absolute path based. + +- `rootPath`: The root folder. +- `searchPattern`: The search string to match against the names of directories. This parameter can contain a combination of valid literal path and wildcard (`*` and `?`) characters, but it doesn't support regular expressions. + +### Returns + +Returns an enumerable collection of full paths of the files or directories that matches the specified search pattern. + +### Example + +text without para + +```scriban +find_files_interface 'C:\Users\coenm\RepoM\src' '*.sln' +find_files_interface('C:\Users\coenm\RepoM\src','*.sln') +``` + +### Example +text without para2 + +#### Input +```scriban +find_files_interface 'C:\Users\coenm\RepoM\src' '*.txt' +find_files_interface('C:\Users\coenm\RepoM\src','*.txt') +``` + +#### Result + +``` +[] +``` diff --git a/docs_new/strings.generated.md b/docs_new/strings.generated.md index e4f9611a..d3dc8e69 100644 --- a/docs_new/strings.generated.md +++ b/docs_new/strings.generated.md @@ -31,6 +31,7 @@ The capitalized input string out = "Test" ``` + ## capitalize_words `capitalize_words(text)` @@ -51,6 +52,7 @@ The capitalized input string out = "This Is Easy" ``` + ## downcase `downcase(text)` @@ -71,6 +73,7 @@ The input string lower case out = "test" ``` + ## endswith `endswith(text,end)` @@ -95,6 +98,7 @@ out = true out = false ``` + ## escape `escape(text)` @@ -115,6 +119,7 @@ The two strings concatenated out = "Hel\\tlo\\n\\\"W\\\\orld" ``` + ## handleize `handleize(text)` @@ -135,6 +140,7 @@ A url handle out = "100-m-ms" ``` + ## lstrip `lstrip(text)` @@ -155,6 +161,7 @@ The input string without any left whitespace characters out = "too many spaces" ``` + ## pad_left `pad_left(text,width)` @@ -176,6 +183,7 @@ The input string padded out = " world" ``` + ## pad_right `pad_right(text,width)` @@ -197,6 +205,7 @@ The input string padded out = "hello " ``` + ## pluralize `pluralize(number,singular,plural)` @@ -219,6 +228,7 @@ The singular or plural string based on number out = "products" ``` + ## regex_escape `regex_escape(text)` @@ -241,6 +251,7 @@ A string of characters with metacharacters converted to their escaped form. out = "\\(abc\\.\\*\\)" ``` + ## regex_match `regex_match(text,pattern,options?)` @@ -267,6 +278,7 @@ An array that contains all the match groups. The first group contains the entire out = ["is a text123", "is", "text123"] ``` + ## regex_matches `regex_matches(context,text,pattern,options?)` @@ -294,6 +306,7 @@ An array of matches that contains all the match groups. The first group contains out = [["this", "this"], ["is", "is"], ["a", "a"], ["text123", "text123"]] ``` + ## regex_replace `regex_replace(text,pattern,replace,options?)` @@ -321,6 +334,7 @@ A new string that is identical to the input string, except that the replacement out = "a-Yo-d" ``` + ## regex_split `regex_split(text,pattern,options?)` @@ -347,6 +361,7 @@ A string array. out = ["a", "b", "c", "d"] ``` + ## regex_unescape `regex_unescape(text)` @@ -367,6 +382,7 @@ A string of characters with any escaped characters converted to their unescaped out = "(abc.*)" ``` + ## rstrip `rstrip(text)` @@ -387,6 +403,7 @@ The input string without any left whitespace characters out = " too many spaces" ``` + ## split `split(text,match)` @@ -409,6 +426,7 @@ An enumeration of the substrings out = ["Hi,", "how", "are", "you", "today?"] ``` + ## startswith `startswith(text,start)` @@ -433,6 +451,7 @@ out = true out = false ``` + ## strip `strip(text)` @@ -453,6 +472,7 @@ The input string without any left and right whitespace characters out = "too many spaces" ``` + ## strip_newlines `strip_newlines(text)` @@ -473,6 +493,7 @@ The input string without any breaks/newlines characters out = "This is a string. With another string" ``` + ## upcase `upcase(text)` @@ -492,3 +513,4 @@ The input string upper case # "test" |> upcase out = "TEST" ``` + diff --git a/src/RepoM.ActionMenu.CodeGen/KalkDescriptor.cs b/src/RepoM.ActionMenu.CodeGen/KalkDescriptor.cs index 4743454b..c11a4673 100644 --- a/src/RepoM.ActionMenu.CodeGen/KalkDescriptor.cs +++ b/src/RepoM.ActionMenu.CodeGen/KalkDescriptor.cs @@ -1,4 +1,4 @@ -namespace Kalk.Core; +namespace Kalk.Core; using System.Collections.Generic; @@ -8,6 +8,7 @@ public KalkDescriptor() { Names = new List(); Params = new List(); + Examples = new List(); } public List Names { get; } @@ -26,5 +27,5 @@ public KalkDescriptor() public string Remarks { get; set; } - public string Example { get; set; } + public List Examples { get; } } \ No newline at end of file diff --git a/src/RepoM.ActionMenu.CodeGen/KalkParamDescriptor.cs b/src/RepoM.ActionMenu.CodeGen/KalkParamDescriptor.cs index 254afe6a..472dc1a1 100644 --- a/src/RepoM.ActionMenu.CodeGen/KalkParamDescriptor.cs +++ b/src/RepoM.ActionMenu.CodeGen/KalkParamDescriptor.cs @@ -18,4 +18,11 @@ public KalkParamDescriptor(string name, string description) public bool IsOptional { get; set; } } + + public class ExamplesDescriptor + { + public string? Description { get; set; } + public string? Input { get; set; } + public string? Output { get; set; } + } } \ No newline at end of file diff --git a/src/RepoM.ActionMenu.CodeGen/Program.cs b/src/RepoM.ActionMenu.CodeGen/Program.cs index c6198839..47ceee5c 100644 --- a/src/RepoM.ActionMenu.CodeGen/Program.cs +++ b/src/RepoM.ActionMenu.CodeGen/Program.cs @@ -14,10 +14,12 @@ namespace RepoM.ActionMenu.CodeGen; using Broslyn; using Kalk.CodeGen; using Kalk.Core; +using Microsoft.Build.Framework; using Microsoft.CodeAnalysis; using RepoM.ActionMenu.Interface.Attributes; using Scriban; using Scriban.Runtime; +using StructuredLogViewer; public partial class Program { @@ -349,27 +351,37 @@ private static async Task GenerateModuleSiteDocumentation(KalkModuleToGenerate m {{ member.Remarks | regex.replace `^\s{4}` '' 'm' | string.rstrip }} {{~ end ~}} -{{~ if member.Example ~}} +{{~ if member.Examples.size > 0 ~}} +{{~ for example in member.Examples ~}} ### Example -```kalk -{{ member.Example | regex.replace `^\s{4}` '' 'm' | string.rstrip }} +{{ example.Description | regex.replace `^\s{4}` '' 'm' | string.rstrip }} + +{{~ if example.Input ~}} +{{~ if example.Output ~}} +#### Input +{{~ end ~}} +```scriban +{{ example.Input | regex.replace `^\s{4}` '' 'm' | string.rstrip }} +``` + +{{~ end ~}} +{{~ if example.Output ~}} +#### Result + +``` +{{ example.Output | regex.replace `^\s{4}` '' 'm' | string.rstrip }} ``` {{~ end ~}} {{~ end ~}} +{{~ end ~}} +{{~ end ~}} "; var template = Template.Parse(templateText); var apiFolder = siteFolder; - // - // // Don't generate hardware.generated.md - // if (name == "hardware") - // { - // return; - // } - var context = new TemplateContext { LoopLimit = 0, @@ -424,10 +436,16 @@ private static (string, string)? TryParseTest(string text) private static void ExtractDocumentation(ISymbol symbol, KalkDescriptorToGenerate desc) { var xmlStr = symbol.GetDocumentationCommentXml(); - if (xmlStr.Contains("Find files in a given directory based on the search pattern. Resulting filenames are absolute path based.")) + if (xmlStr.Contains("Returns an enumerable collection of full paths of the files or directories that matches the specified search pattern.")) { xmlStr = xmlStr; } + + if (xmlStr.Contains("Checks if the specified file path exists on the disk.")) + { + xmlStr = xmlStr; + } + try { if (!string.IsNullOrEmpty(xmlStr)) @@ -471,13 +489,8 @@ private static void ExtractDocumentation(ISymbol symbol, KalkDescriptorToGenerat } else if (element.Name == "example") { - text = _removeCode.Replace(text, string.Empty); - desc.Example += text; - // var test = TryParseTest(text); - // if (test != null) - // { - // desc.Tests.Add(test.Value); - // } + ExamplesDescriptor examplesDescriptor = GetExampleData(element); + desc.Examples.Add(examplesDescriptor); } else if (element.Name == "test") { @@ -552,6 +565,108 @@ private static string GetCleanedString(XNode node) return HttpUtility.HtmlDecode(text); } + private static ExamplesDescriptor GetExampleData(XNode node) + { + var result = new ExamplesDescriptor(); + + if (node.NodeType != XmlNodeType.Element) + { + return result; + // expect example node + } + + var element = (XElement)node; + if (element.Name != "example") + { + return result; + // expect example node + } + + XNode[] nodes = element.Nodes().ToArray(); + if (nodes.Length == 0) + { + result.Description = element.Value.Trim(); + return result; + } + + string current = ""; + + foreach (var item in nodes) + { + if (item is XText xtext) + { + current += xtext.Value.Trim() + Environment.NewLine; + } + else if (item is XElement xelement) + { + if (xelement.Name == "code") + { + if (result.Input != null) + { + // switch next + result.Output = xelement.Value.Trim(); + } + else + { + // switch next + result.Description = current; + result.Input = xelement.Value.Trim(); + } + } + else if (xelement.Name == "para") + { + current += xelement.Value.Trim() + Environment.NewLine; + } + else + { + throw new Exception($"'{xelement.Name}' Not expected"); + } + } + } + + if (string.IsNullOrEmpty(result.Description)) + { + result.Description = current; + } + + return result; + // + // text = element.Attribute("name")?.Value ?? string.Empty; + // + // + // if (node.NodeType == XmlNodeType.Text) + // { + // result.Description = node.ToString(); + // return result; + // } + // + // var element = (XElement)node; + // string text; + // if (element.Name == "paramref") + // { + // text = element.Attribute("name")?.Value ?? string.Empty; + // } + // else + // { + // + // var builder = new StringBuilder(); + // foreach (var subElement in element.Nodes()) + // { + // builder.Append(GetCleanedString(subElement)); + // } + // + // text = builder.ToString(); + // } + // + // if (element.Name == "para") + // { + // text += "\n"; + // } + + // result.Description = HttpUtility.HtmlDecode(text); + // return result; + } + [GeneratedRegex("^\\s*```\\w*[ \\t]*[\\r\\n]*", RegexOptions.Multiline)] private static partial Regex RemoveCodeRegex(); diff --git a/src/RepoM.ActionMenu.Core/Model/ActionMenuGenerationContext.cs b/src/RepoM.ActionMenu.Core/Model/ActionMenuGenerationContext.cs index cb0e2b55..86442205 100644 --- a/src/RepoM.ActionMenu.Core/Model/ActionMenuGenerationContext.cs +++ b/src/RepoM.ActionMenu.Core/Model/ActionMenuGenerationContext.cs @@ -62,7 +62,7 @@ public ActionMenuGenerationContext( var rootScriptObject = new RepoMScriptObject(); - rootScriptObject.SetValue("file", new FileFunctions(fileSystem), true); + rootScriptObject.SetValue("file", new FileFunctions(), true); rootScriptObject.Add("repository", new RepositoryFunctions(Repository)); diff --git a/src/RepoM.ActionMenu.Core/Model/Functions/FileFunctions.cs b/src/RepoM.ActionMenu.Core/Model/Functions/FileFunctions.cs index 5fa5885c..603f915e 100644 --- a/src/RepoM.ActionMenu.Core/Model/Functions/FileFunctions.cs +++ b/src/RepoM.ActionMenu.Core/Model/Functions/FileFunctions.cs @@ -13,37 +13,36 @@ namespace RepoM.ActionMenu.Core.Model.Functions; [ActionMenuModule("File")] internal partial class FileFunctions : ScribanModuleWithFunctions { - private readonly IFileSystem _fileSystem; - - public FileFunctions(IFileSystem fileSystem) + public FileFunctions() { - _fileSystem = fileSystem; RegisterFunctions(); } - - + /// /// Find files in a given directory based on the search pattern. Resulting filenames are absolute path based. /// /// The root folder. /// The search string to match against the names of directories. This parameter can contain a combination of valid literal path and wildcard (`*` and `?`) characters, but it doesn't support regular expressions. /// Returns an enumerable collection of full paths of the files or directories that matches the specified search pattern. - /// first /// - /// second + /// text without para /// /// find_files 'C:\Users\coenm\RepoM\src' '*.sln' + /// find_files('C:\Users\coenm\RepoM\src','*.sln') + /// + /// + /// 1aa349585ed7ecbd3b9c486a30067e395ca4b356 + /// + /// + /// + /// text without para2 + /// + /// find_files 'C:\Users\coenm\RepoM\src' '*.txt' + /// find_files('C:\Users\coenm\RepoM\src','*.txt') + /// + /// + /// [] /// - /// - /// xxx - /// sdf - /// - /// ```scribanhtml - /// find_files 'C:\Users\coenm\RepoM\src' '*.csproj' - /// ``` - /// ```html - /// [1, 2, 3, 4] - /// ``` /// [ActionMenuMember("find_files")] public static string[] FindFiles(ActionMenuGenerationContext context, SourceSpan span, string rootPath, string searchPattern) @@ -51,7 +50,32 @@ public static string[] FindFiles(ActionMenuGenerationContext context, SourceSpan return FindFilesUsingInterface(context, span, rootPath, searchPattern); } - /// + /// + /// Find files in a given directory based on the search pattern. Resulting filenames are absolute path based. + /// + /// The root folder. + /// The search string to match against the names of directories. This parameter can contain a combination of valid literal path and wildcard (`*` and `?`) characters, but it doesn't support regular expressions. + /// Returns an enumerable collection of full paths of the files or directories that matches the specified search pattern. + /// + /// text without para + /// + /// find_files_interface 'C:\Users\coenm\RepoM\src' '*.sln' + /// find_files_interface('C:\Users\coenm\RepoM\src','*.sln') + /// + /// + /// The result is an enumerable of strings. + /// + /// + /// + /// text without para2 + /// + /// find_files_interface 'C:\Users\coenm\RepoM\src' '*.txt' + /// find_files_interface('C:\Users\coenm\RepoM\src','*.txt') + /// + /// + /// [] + /// + /// [ActionMenuMember("find_files_interface")] public static string[] FindFilesUsingInterface(IMenuContext context, SourceSpan span, string rootPath, string searchPattern) { From 9517beb0043ed505b9e274410c5376415aa5bdd3 Mon Sep 17 00:00:00 2001 From: Coen van den Munckhof Date: Sat, 14 Oct 2023 21:17:51 +0200 Subject: [PATCH 003/430] cleanup --- src/RepoM.ActionMenu.CodeGen/Program.cs | 84 +------------------------ 1 file changed, 3 insertions(+), 81 deletions(-) diff --git a/src/RepoM.ActionMenu.CodeGen/Program.cs b/src/RepoM.ActionMenu.CodeGen/Program.cs index 47ceee5c..3be07a09 100644 --- a/src/RepoM.ActionMenu.CodeGen/Program.cs +++ b/src/RepoM.ActionMenu.CodeGen/Program.cs @@ -278,8 +278,7 @@ void GetOrCreateModule(ITypeSymbol typeSymbol, string className, AttributeData m Console.WriteLine($"{functionWithMissingDoc} functions with missing doc."); Console.WriteLine($"{functionWithMissingTests} functions with missing tests."); } - - + private static async Task GenerateModuleSiteDocumentation(KalkModuleToGenerate module, string siteFolder) { if (module.Name == "KalkEngine") @@ -397,42 +396,6 @@ private static async Task GenerateModuleSiteDocumentation(KalkModuleToGenerate m await File.WriteAllTextAsync(Path.Combine(apiFolder, $"{name}.generated.md"), result); } - private static (string, string)? TryParseTest(string text) - { - var testLines = new StringReader(text); - string? line; - string input = null; - var output = string.Empty; - var startColumn = -1; - while ((line = testLines.ReadLine()) != null) - { - line = line.TrimEnd(); - var matchPrompt = _promptRegex.Match(line); - if (matchPrompt.Success) - { - startColumn = matchPrompt.Groups[1].Length; - input += line.Substring(matchPrompt.Length) + Environment.NewLine; - } - else - { - if (startColumn < 0) - { - throw new InvalidOperationException($"Expecting a previous prompt line >>> before `{line}`"); - } - - line = line.Length >= startColumn ? line[startColumn..] : line; - // If we have a result with ellipsis `...` we can't test this text. - if (line.StartsWith("...")) - { - return null; - } - output += line + Environment.NewLine; - } - } - - return input != null ? (input.TrimEnd(), output.TrimEnd()) : null; - } - private static void ExtractDocumentation(ISymbol symbol, KalkDescriptorToGenerate desc) { var xmlStr = symbol.GetDocumentationCommentXml(); @@ -494,12 +457,7 @@ private static void ExtractDocumentation(ISymbol symbol, KalkDescriptorToGenerat } else if (element.Name == "test") { - text = _removeCode.Replace(text, string.Empty); - // var test = TryParseTest(text); - // if (test != null) - // { - // desc.Tests.Add(test.Value); - // } + _ = _removeCode.Replace(text, string.Empty); } } } @@ -509,8 +467,7 @@ private static void ExtractDocumentation(ISymbol symbol, KalkDescriptorToGenerat throw new InvalidOperationException($"Error while processing `{symbol}` with XML doc `{xmlStr}", ex); } } - - + static string GetTypeName(ITypeSymbol typeSymbol) { //if (typeSymbol is IArrayTypeSymbol arrayTypeSymbol) @@ -630,41 +587,6 @@ private static ExamplesDescriptor GetExampleData(XNode node) } return result; - // - // text = element.Attribute("name")?.Value ?? string.Empty; - // - // - // if (node.NodeType == XmlNodeType.Text) - // { - // result.Description = node.ToString(); - // return result; - // } - // - // var element = (XElement)node; - // string text; - // if (element.Name == "paramref") - // { - // text = element.Attribute("name")?.Value ?? string.Empty; - // } - // else - // { - // - // var builder = new StringBuilder(); - // foreach (var subElement in element.Nodes()) - // { - // builder.Append(GetCleanedString(subElement)); - // } - // - // text = builder.ToString(); - // } - // - // if (element.Name == "para") - // { - // text += "\n"; - // } - - // result.Description = HttpUtility.HtmlDecode(text); - // return result; } [GeneratedRegex("^\\s*```\\w*[ \\t]*[\\r\\n]*", RegexOptions.Multiline)] From 8bb2709099fb0ab4bd9bf7ae07ae1ce16885998e Mon Sep 17 00:00:00 2001 From: Coen van den Munckhof Date: Sat, 14 Oct 2023 22:17:50 +0200 Subject: [PATCH 004/430] fix tests --- ...ooleanWithoutXTests.Serialize.verified.txt | 19 +++---------- ...oleanWithoutXTests.UseFactory.verified.txt | 28 ------------------- .../RepoM.ActionMenu.Core.Tests/UnitTest1.cs | 26 ++++++----------- 3 files changed, 13 insertions(+), 60 deletions(-) diff --git a/tests/RepoM.ActionMenu.Core.Tests/BooleanWithoutXTests.Serialize.verified.txt b/tests/RepoM.ActionMenu.Core.Tests/BooleanWithoutXTests.Serialize.verified.txt index fe641542..d06ce0da 100644 --- a/tests/RepoM.ActionMenu.Core.Tests/BooleanWithoutXTests.Serialize.verified.txt +++ b/tests/RepoM.ActionMenu.Core.Tests/BooleanWithoutXTests.Serialize.verified.txt @@ -1,11 +1,11 @@ tags: -- tag: coen1 +- tag: private when: 1 == 1 -- tag: coentje2 +- tag: github when: 1 == 2 -- tag: coentje3 +- tag: github when: 1 == 2 || true -- tag: rian +- tag: work when: '' action-menu: - type: just-text@1 @@ -24,17 +24,6 @@ action-menu: active: enabled: context: -- type: foreach@1 - active: - enumerable: file.pwd2("c:\\", "*.env") - variable: file - skip: - actions: - - type: just-text@1 - text: file name {{ file }} - active: - enabled: - context: - type: foreach@1 active: enumerable: devopsEnvironments diff --git a/tests/RepoM.ActionMenu.Core.Tests/BooleanWithoutXTests.UseFactory.verified.txt b/tests/RepoM.ActionMenu.Core.Tests/BooleanWithoutXTests.UseFactory.verified.txt index fc53b41f..f1951c70 100644 --- a/tests/RepoM.ActionMenu.Core.Tests/BooleanWithoutXTests.UseFactory.verified.txt +++ b/tests/RepoM.ActionMenu.Core.Tests/BooleanWithoutXTests.UseFactory.verified.txt @@ -41,34 +41,6 @@ ExecutionCausesSynchronizing: false, CanExecute: true }, - { - Name: file name C:\file1.env, - RepositoryCommand: {}, - Repository: { - SafePath: dummy safe path, - HasUnpushedChanges: false, - Name: dummy name, - Path: dummy path, - Location: dummy location, - CurrentBranch: dummy current branch - }, - ExecutionCausesSynchronizing: false, - CanExecute: true - }, - { - Name: file name C:\file2.env, - RepositoryCommand: {}, - Repository: { - SafePath: dummy safe path, - HasUnpushedChanges: false, - Name: dummy name, - Path: dummy path, - Location: dummy location, - CurrentBranch: dummy current branch - }, - ExecutionCausesSynchronizing: false, - CanExecute: true - }, { Name: env name Develop env(DEF):`my def 12`, RepositoryCommand: {}, diff --git a/tests/RepoM.ActionMenu.Core.Tests/UnitTest1.cs b/tests/RepoM.ActionMenu.Core.Tests/UnitTest1.cs index 8896dcbb..a912e677 100644 --- a/tests/RepoM.ActionMenu.Core.Tests/UnitTest1.cs +++ b/tests/RepoM.ActionMenu.Core.Tests/UnitTest1.cs @@ -21,7 +21,7 @@ public class BooleanWithoutXTests { private readonly IRepository _repository = new DummyRepository(); - private const string File1Env = + private const string FILE1_ENV = """ # just a comment ABC=my abc @@ -30,13 +30,13 @@ public class BooleanWithoutXTests DEF=my def 12 """; - private const string File2Env = + private const string FILE2_ENV = """ DEF=my def 12-second! GHI=GHI GHI GHI-second """; - private const string Sub = + private const string SUB = """ context: # - type: set-variable@1 @@ -123,14 +123,6 @@ func sonar_url(project_id) - type: just-text@1 text: 'file name {{ file }}' - - type: foreach@1 - enumerable: 'file.pwd2("c:\\", "*.env")' - variable: file - actions: - - type: just-text@1 - text: 'file name {{ file }}' - - - type: foreach@1 enumerable: devopsEnvironments variable: environment @@ -177,9 +169,9 @@ public async Task UseFactory() new Dictionary() { { "C:\\RepositoryActionsV2.yaml", new MockFileData(Yaml, Encoding.UTF8) }, - { "C:\\SubV2.yaml", new MockFileData(Sub, Encoding.UTF8) }, - { "C:\\file1.env", new MockFileData(File1Env, Encoding.UTF8) }, - { "C:\\file2.env", new MockFileData(File2Env, Encoding.UTF8) }, + { "C:\\SubV2.yaml", new MockFileData(SUB, Encoding.UTF8) }, + { "C:\\file1.env", new MockFileData(FILE1_ENV, Encoding.UTF8) }, + { "C:\\file2.env", new MockFileData(FILE2_ENV, Encoding.UTF8) }, }); var sut = new Factory(fileSystem, new ITemplateContextRegistration[0]); var factory = sut.Create("C:\\RepositoryActionsV2.yaml"); @@ -196,9 +188,9 @@ public async Task GetTags() new Dictionary() { { "C:\\RepositoryActionsV2.yaml", new MockFileData(Yaml, Encoding.UTF8) }, - { "C:\\SubV2.yaml", new MockFileData(Sub, Encoding.UTF8) }, - { "C:\\file1.env", new MockFileData(File1Env, Encoding.UTF8) }, - { "C:\\file2.env", new MockFileData(File2Env, Encoding.UTF8) }, + { "C:\\SubV2.yaml", new MockFileData(SUB, Encoding.UTF8) }, + { "C:\\file1.env", new MockFileData(FILE1_ENV, Encoding.UTF8) }, + { "C:\\file2.env", new MockFileData(FILE2_ENV, Encoding.UTF8) }, }); var sut = new Factory(fileSystem, new ITemplateContextRegistration[0]); var factory = sut.Create("C:\\RepositoryActionsV2.yaml"); From 214a95575a671dc8153166c8925276720562905f Mon Sep 17 00:00:00 2001 From: Coen van den Munckhof Date: Sat, 14 Oct 2023 22:44:28 +0200 Subject: [PATCH 005/430] Update --- .../KalkDescriptor.cs | 15 +- .../KalkParamDescriptor.cs | 2 +- .../KalkToGenerate.cs | 7 +- src/RepoM.ActionMenu.CodeGen/Program.cs | 214 ++++++------------ .../RepoM.ActionMenu.CodeGen.csproj | 7 +- .../Templates/Docs.scriban-cs | 58 ----- .../Templates/Docs.scriban-txt | 83 +++++++ .../RepoM.ActionMenu.Core.Tests/UnitTest1.cs | 8 +- 8 files changed, 169 insertions(+), 225 deletions(-) delete mode 100644 src/RepoM.ActionMenu.CodeGen/Templates/Docs.scriban-cs create mode 100644 src/RepoM.ActionMenu.CodeGen/Templates/Docs.scriban-txt diff --git a/src/RepoM.ActionMenu.CodeGen/KalkDescriptor.cs b/src/RepoM.ActionMenu.CodeGen/KalkDescriptor.cs index c11a4673..42b0b968 100644 --- a/src/RepoM.ActionMenu.CodeGen/KalkDescriptor.cs +++ b/src/RepoM.ActionMenu.CodeGen/KalkDescriptor.cs @@ -1,17 +1,10 @@ -namespace Kalk.Core; +namespace RepoM.ActionMenu.CodeGen; using System.Collections.Generic; public class KalkDescriptor { - public KalkDescriptor() - { - Names = new List(); - Params = new List(); - Examples = new List(); - } - - public List Names { get; } + public List Names { get; } = new(); public bool IsCommand { get; set; } @@ -19,7 +12,7 @@ public KalkDescriptor() public string Description { get; set; } - public List Params { get; } + public List Params { get; } = new(); public string Syntax { get; set; } @@ -27,5 +20,5 @@ public KalkDescriptor() public string Remarks { get; set; } - public List Examples { get; } + public List Examples { get; } = new(); } \ No newline at end of file diff --git a/src/RepoM.ActionMenu.CodeGen/KalkParamDescriptor.cs b/src/RepoM.ActionMenu.CodeGen/KalkParamDescriptor.cs index 472dc1a1..22e0c8b3 100644 --- a/src/RepoM.ActionMenu.CodeGen/KalkParamDescriptor.cs +++ b/src/RepoM.ActionMenu.CodeGen/KalkParamDescriptor.cs @@ -1,4 +1,4 @@ -namespace Kalk.Core +namespace RepoM.ActionMenu.CodeGen { public class KalkParamDescriptor { diff --git a/src/RepoM.ActionMenu.CodeGen/KalkToGenerate.cs b/src/RepoM.ActionMenu.CodeGen/KalkToGenerate.cs index 8f15960d..d1037e60 100644 --- a/src/RepoM.ActionMenu.CodeGen/KalkToGenerate.cs +++ b/src/RepoM.ActionMenu.CodeGen/KalkToGenerate.cs @@ -1,8 +1,7 @@ -using System.Collections.Generic; -using Kalk.Core; - -namespace Kalk.CodeGen +namespace RepoM.ActionMenu.CodeGen { + using System.Collections.Generic; + public abstract class KalkDescriptorToGenerate : KalkDescriptor { protected KalkDescriptorToGenerate() diff --git a/src/RepoM.ActionMenu.CodeGen/Program.cs b/src/RepoM.ActionMenu.CodeGen/Program.cs index 3be07a09..ec87cff5 100644 --- a/src/RepoM.ActionMenu.CodeGen/Program.cs +++ b/src/RepoM.ActionMenu.CodeGen/Program.cs @@ -12,20 +12,15 @@ namespace RepoM.ActionMenu.CodeGen; using System.Xml; using System.Xml.Linq; using Broslyn; -using Kalk.CodeGen; -using Kalk.Core; -using Microsoft.Build.Framework; using Microsoft.CodeAnalysis; using RepoM.ActionMenu.Interface.Attributes; using Scriban; using Scriban.Runtime; -using StructuredLogViewer; public partial class Program { private static readonly Regex _removeCode = RemoveCodeRegex(); - private static readonly Regex _promptRegex = PromptRegex(); - + static async Task Main(string[] args) { // not sure why Kalk has this. @@ -38,26 +33,14 @@ static async Task Main(string[] args) var pathToSolution = Path.Combine(srcFolder, projectName, $"{projectName}.csproj"); var pathToGeneratedCode = Path.Combine(srcFolder, projectName, "RepoMCodeGen.generated.cs"); - if (!Directory.Exists(Path.Combine(rootFolder, ".git"))) - { - throw new Exception("Wrong root folder"); - } - - if (!Directory.Exists(srcFolder)) - { - throw new Exception($"src folder `{srcFolder}` doesn't exist"); - } - - if (!Directory.Exists(docsFolder)) - { - throw new Exception($"docsFolder folder `{docsFolder}` doesn't exist"); - } - - if (!File.Exists(pathToSolution)) - { - throw new Exception($"File `{pathToSolution}` does not exist"); - } + CheckDirectory(Path.Combine(rootFolder, ".git")); + CheckDirectory(srcFolder); + CheckDirectory(docsFolder); + CheckFile(pathToSolution); + Template templateModule = await LoadTemplateAsync("Templates/Module.scriban-cs"); + Template templateDocs = await LoadTemplateAsync("Templates/Docs.scriban-txt"); + CSharpCompilationCaptureResult compilationCaptureResult = CSharpCompilationCapture.Build(pathToSolution); Solution solution = compilationCaptureResult.Workspace.CurrentSolution; Project[] solutionProjects = solution.Projects.ToArray(); @@ -68,23 +51,9 @@ static async Task Main(string[] args) // Compile the project Compilation compilation = await project.GetCompilationAsync() ?? throw new Exception("Compilation failed"); + ValidateCompilation(compilation); - ImmutableArray diagnostics = compilation.GetDiagnostics(); - Diagnostic[] errors = diagnostics.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error).ToArray(); - if (errors.Length > 0) - { - Console.WriteLine("Compilation errors:"); - foreach (var error in errors) - { - Console.WriteLine(error); - } - - Console.WriteLine("Error, Exiting."); - Environment.Exit(1); - return; - } - - //var kalkEngine = compilation.GetTypeByMetadataName("Kalk.Core.KalkEngine"); + // _ = compilation.GetTypeByMetadataName("Kalk.Core.KalkEngine"); var mapNameToModule = new Dictionary(); void GetOrCreateModule(ITypeSymbol typeSymbol, string className, AttributeData moduleAttribute, out KalkModuleToGenerate moduleToGenerate) @@ -157,7 +126,7 @@ void GetOrCreateModule(ITypeSymbol typeSymbol, string className, AttributeData m Name = name, XmlId = member.GetDocumentationCommentId(), Category = string.Empty, - IsCommand = method != null && method.ReturnsVoid, + IsCommand = method?.ReturnsVoid ?? false, Module = moduleToGenerate, }; desc.Names.Add(name); @@ -179,7 +148,11 @@ void GetOrCreateModule(ITypeSymbol typeSymbol, string className, AttributeData m for (var i = 0; i < method.Parameters.Length; i++) { var parameter = method.Parameters[i]; - if (i > 0) builder.Append(", "); + if (i > 0) + { + builder.Append(", "); + } + builder.Append(GetTypeName(parameter.Type)); } @@ -211,13 +184,11 @@ void GetOrCreateModule(ITypeSymbol typeSymbol, string className, AttributeData m } var modules = mapNameToModule.Values.OrderBy(x => x.ClassName).ToList(); - var templateStr = await File.ReadAllTextAsync("Templates/Module.scriban-cs"); - var template = Template.Parse(templateStr); var context = new TemplateContext { LoopLimit = 0, - MemberRenamer = x => x.Name + MemberRenamer = x => x.Name, }; var scriptObject = new ScriptObject() { @@ -225,15 +196,14 @@ void GetOrCreateModule(ITypeSymbol typeSymbol, string className, AttributeData m }; context.PushGlobal(scriptObject); - var result = await template.RenderAsync(context); + var result = await templateModule.RenderAsync(context); await File.WriteAllTextAsync(pathToGeneratedCode, result); - // await File.WriteAllTextAsync(Path.Combine(srcFolder, "ScribanRepoM.Tests", "RepoM.ActionMenu", "Generated", "Coen.generated.cs"), result); // Generate module site documentation foreach(KalkModuleToGenerate module in modules) { - await GenerateModuleSiteDocumentation(module, docsFolder); + await GenerateModuleSiteDocumentation(module, docsFolder, templateDocs); } return; @@ -247,7 +217,7 @@ void GetOrCreateModule(ITypeSymbol typeSymbol, string className, AttributeData m { var hasNoDesc = string.IsNullOrEmpty(member.Description); var hasNoTests = member.Tests.Count == 0; - if ((!hasNoDesc && !hasNoTests) || module.ClassName.Contains("Intrinsics")) + if ((!hasNoDesc && !hasNoTests)) { continue; } @@ -278,8 +248,29 @@ void GetOrCreateModule(ITypeSymbol typeSymbol, string className, AttributeData m Console.WriteLine($"{functionWithMissingDoc} functions with missing doc."); Console.WriteLine($"{functionWithMissingTests} functions with missing tests."); } - - private static async Task GenerateModuleSiteDocumentation(KalkModuleToGenerate module, string siteFolder) + + private static void ValidateCompilation(Compilation compilation) + { + ImmutableArray diagnostics = compilation.GetDiagnostics(); + Diagnostic[] errors = diagnostics.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error).ToArray(); + + if (errors.Length <= 0) + { + return; + } + + Console.WriteLine("Compilation errors:"); + foreach (Diagnostic error in errors) + { + Console.WriteLine(error); + } + + Console.WriteLine("Error, Exiting."); + Environment.Exit(1); + throw new Exception("Compilation error"); + } + + private static async Task GenerateModuleSiteDocumentation(KalkModuleToGenerate module, string siteFolder, Template templateDocs) { if (module.Name == "KalkEngine") { @@ -293,92 +284,6 @@ private static async Task GenerateModuleSiteDocumentation(KalkModuleToGenerate m var name = module.Name.ToLowerInvariant(); module.Url = $"/doc/api/{name}/"; - const string templateText = @"--- -title: {{module.Title}} -url: {{module.Url}} ---- -{{~ if !module.IsBuiltin ~}} - -{{ module.Description }} - -In order to use the functions provided by this module, you need to import this module: - -```kalk ->>> import {{module.Name}} -``` -{{~ end ~}} -{{~ if (module.Title | string.contains 'Intrinsics') ~}} - -In order to use the functions provided by this module, you need to import this module: - -```kalk ->>> import HardwareIntrinsics -``` -{%{~ -{{NOTE do}} -~}%} -These intrinsic functions are only available if your CPU supports `{{module.Name}}` features. -{%{~ -{{end}} -~}%} - -{{~ end ~}} -{{~ for member in module.Members ~}} - -## {{member.Name}} - -`{{member.Name}}{{~ if member.Params.size > 0 ~}}({{~ for param in member.Params ~}}{{ param.Name }}{{ param.IsOptional?'?':''}}{{ for.last?'':',' }}{{~ end ~}}){{~ end ~}}` - -{{~ if member.Description ~}} -{{ member.Description | regex.replace `^\s{4}` '' 'm' | string.rstrip }} -{{~ end ~}} -{{~ if member.Params.size > 0 ~}} - - {{~ for param in member.Params ~}} -- `{{ param.Name }}`: {{ param.Description}} - {{~end ~}} -{{~ end ~}} -{{~ if member.Returns ~}} - -### Returns - -{{ member.Returns | regex.replace `^\s{4}` '' 'm' | string.rstrip }} -{{~ end ~}} -{{~ if member.Remarks ~}} - -### Remarks - -{{ member.Remarks | regex.replace `^\s{4}` '' 'm' | string.rstrip }} -{{~ end ~}} -{{~ if member.Examples.size > 0 ~}} - -{{~ for example in member.Examples ~}} -### Example - -{{ example.Description | regex.replace `^\s{4}` '' 'm' | string.rstrip }} - -{{~ if example.Input ~}} -{{~ if example.Output ~}} -#### Input -{{~ end ~}} -```scriban -{{ example.Input | regex.replace `^\s{4}` '' 'm' | string.rstrip }} -``` - -{{~ end ~}} -{{~ if example.Output ~}} -#### Result - -``` -{{ example.Output | regex.replace `^\s{4}` '' 'm' | string.rstrip }} -``` -{{~ end ~}} -{{~ end ~}} -{{~ end ~}} -{{~ end ~}} -"; - var template = Template.Parse(templateText); - var apiFolder = siteFolder; var context = new TemplateContext @@ -391,7 +296,7 @@ private static async Task GenerateModuleSiteDocumentation(KalkModuleToGenerate m }; context.PushGlobal(scriptObject); context.MemberRenamer = x => x.Name; - var result = await template.RenderAsync(context); + var result = await templateDocs.RenderAsync(context); await File.WriteAllTextAsync(Path.Combine(apiFolder, $"{name}.generated.md"), result); } @@ -589,9 +494,34 @@ private static ExamplesDescriptor GetExampleData(XNode node) return result; } + private static void CheckDirectory(string path) + { + if (!Directory.Exists(path)) + { + throw new Exception($"Folder '{path}' does not exist"); + } + } + + private static void CheckFile(string path) + { + if (!File.Exists(path)) + { + throw new Exception($"File '{path}' does not exist"); + } + } + + private static async Task