diff --git a/DotNetTry.sln.DotSettings b/DotNetTry.sln.DotSettings index 7fa67efcf..c30c28c31 100644 --- a/DotNetTry.sln.DotSettings +++ b/DotNetTry.sln.DotSettings @@ -1,2 +1,3 @@  - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/MLS.Agent.Tests/CommandLine/VerifyCommandTests.cs b/MLS.Agent.Tests/CommandLine/VerifyCommandTests.cs index 2be4a5b2b..b41e5860e 100644 --- a/MLS.Agent.Tests/CommandLine/VerifyCommandTests.cs +++ b/MLS.Agent.Tests/CommandLine/VerifyCommandTests.cs @@ -135,6 +135,41 @@ await VerifyCommand.Do( $"{root}{Path.DirectorySeparatorChar}doc.md*Line 2:*{root}{Path.DirectorySeparatorChar}Program.cs (in project {root}{Path.DirectorySeparatorChar}some.csproj)*".EnforceLF()); } + [Fact] + public async Task Fails_if_language_is_not_compatible_with_backing_project() + { + var root = Create.EmptyWorkspace(isRebuildablePackage: true).Directory; + + var directoryAccessor = new InMemoryDirectoryAccessor(root, root) + { + ("some.csproj", CsprojContents), + ("Program.cs", CompilingProgramCs), + ("support.fs", "let a = 0"), + ("doc.md", @" +```fs --source-file support.fs --project some.csproj +``` +") + }.CreateFiles(); + + var console = new TestConsole(); + + await VerifyCommand.Do( + new VerifyOptions(root), + console, + () => directoryAccessor, + PackageRegistry.CreateForTryMode(root)); + + _output.WriteLine(console.Out.ToString()); + + console.Out + .ToString() + .EnforceLF() + .Trim() + .Should() + .Match( + $"*Build failed as project {root}{Path.DirectorySeparatorChar}some.csproj is not compatible with language fsharp*".EnforceLF()); + } + [Fact] public async Task When_non_editable_code_blocks_do_not_contain_errors_then_validation_succeeds() { diff --git a/MLS.Agent.Tests/Markdown/CodeBlockAnnotationExtensionTests.cs b/MLS.Agent.Tests/Markdown/CodeBlockAnnotationExtensionTests.cs index 879dd7bca..4361fc778 100644 --- a/MLS.Agent.Tests/Markdown/CodeBlockAnnotationExtensionTests.cs +++ b/MLS.Agent.Tests/Markdown/CodeBlockAnnotationExtensionTests.cs @@ -67,14 +67,14 @@ static void MyProgram(string[] args) var document = $@"```{language} --source-file Program.cs ```"; - string html = (await pipeline.RenderHtmlAsync(document)).EnforceLF(); + var html = (await pipeline.RenderHtmlAsync(document)).EnforceLF(); html.Should().Contain(fileContent.HtmlEncode().ToString()); } [Fact] - public async Task Does_not_insert_code_when_specified_language_is_not_csharp() + public async Task Does_not_insert_code_when_specified_language_is_not_supported() { - string expectedValue = + var expectedValue = @"
console.log("Hello World");
 
".EnforceLF(); @@ -90,27 +90,34 @@ public async Task Does_not_insert_code_when_specified_language_is_not_csharp() html.Should().Contain(expectedValue); } - [Fact] - public async Task Does_not_insert_code_when_csharp_is_specified_but_no_additional_options() + [Theory] + [InlineData("cs", "language-cs")] + [InlineData("csharp", "language-csharp")] + [InlineData("c#", "language-c#")] + [InlineData("fs", "language-fs")] + [InlineData("fsharp", "language-fsharp")] + [InlineData("f#", "language-f#")] + public async Task Does_not_insert_code_when_supported_language_is_specified_but_no_additional_options(string fenceLanguage, string expectedClass) { - string expectedValue = -@"
Console.WriteLine("Hello World");
+            var expectedValue =
+$@"
Console.WriteLine("Hello World");
 
".EnforceLF(); var testDir = TestAssets.SampleConsole; var directoryAccessor = new InMemoryDirectoryAccessor(testDir); var pipeline = new MarkdownPipelineBuilder().UseCodeBlockAnnotations(directoryAccessor, Default.PackageFinder).Build(); - var document = @" -```cs + var document = $@" +```{fenceLanguage} Console.WriteLine(""Hello World""); ```"; var html = (await pipeline.RenderHtmlAsync(document)).EnforceLF(); html.Should().Contain(expectedValue); } + [Fact] - public async Task Error_messsage_is_displayed_when_the_linked_file_does_not_exist() + public async Task Error_message_is_displayed_when_the_linked_file_does_not_exist() { var testDir = TestAssets.SampleConsole; var directoryAccessor = new InMemoryDirectoryAccessor(testDir) @@ -190,6 +197,38 @@ public async Task Sets_the_trydotnet_package_attribute_using_the_passed_project_ output.Value.Should().Be(fullProjectPath.FullName); } + [Theory] + [InlineData("cs", "Program.cs", "sample.csproj", "csharp")] + [InlineData("c#", "Program.cs", "sample.csproj", "csharp")] + [InlineData("fs", "Program.fs", "sample.fsproj", "fsharp")] + [InlineData("f#", "Program.fs", "sample.fsproj", "fsharp")] + public async Task Sets_the_trydotnet_language_attribute_using_the_fence_command(string fenceLanguage, string fileName, string projectName, string expectedLanguage) + { + var rootDirectory = TestAssets.SampleConsole; + var currentDir = new DirectoryInfo(Path.Combine(rootDirectory.FullName, "docs")); + var directoryAccessor = new InMemoryDirectoryAccessor(currentDir, rootDirectory) + { + ($"src/sample/{fileName}", ""), + ($"src/sample/{projectName}", "") + }; + + var pipeline = new MarkdownPipelineBuilder().UseCodeBlockAnnotations(directoryAccessor, Default.PackageFinder).Build(); + + var package = $"../src/sample/{projectName}"; + var document = + $@"```{fenceLanguage} --project {package} --source-file ../src/sample/{fileName} +```"; + + var html = (await pipeline.RenderHtmlAsync(document)).EnforceLF(); + + var htmlDocument = new HtmlDocument(); + htmlDocument.LoadHtml(html); + var trydotnetLanguage = htmlDocument.DocumentNode + .SelectSingleNode("//pre/code").Attributes["data-trydotnet-language"]; + + trydotnetLanguage.Value.Should().Be(expectedLanguage); + } + [Fact] public async Task Sets_the_trydotnet_package_attribute_using_the_passed_package_option() { diff --git a/MLS.Agent/CommandLine/VerifyCommand.cs b/MLS.Agent/CommandLine/VerifyCommand.cs index fc67c19b3..a421a63f4 100644 --- a/MLS.Agent/CommandLine/VerifyCommand.cs +++ b/MLS.Agent/CommandLine/VerifyCommand.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.CommandLine; +using System.IO; using System.Linq; using System.Threading.Tasks; using Microsoft.DotNet.Try.Markdown; @@ -13,6 +14,7 @@ using WorkspaceServer; using WorkspaceServer.Servers.Roslyn; using Buffer = Microsoft.DotNet.Try.Protocol.Buffer; +using File = Microsoft.DotNet.Try.Protocol.File; namespace MLS.Agent.CommandLine { @@ -135,6 +137,17 @@ async Task ReportCompileResults( .Select(b => b.ProjectOrPackageName()) .FirstOrDefault(name => !string.IsNullOrWhiteSpace(name)); + var language = session + .Select(b => b.Language()) + .FirstOrDefault(name => !string.IsNullOrWhiteSpace(name)); + + if (!ProjectIsCompatibleWithLanguage( new UriOrFileInfo(projectOrPackageName), language)) + { + SetError(); + + console.Out.WriteLine($" Build failed as project {projectOrPackageName} is not compatible with language {language}"); + } + var editableCodeBlocks = session.Where(b => b.Annotations.Editable).ToList(); var buffers = editableCodeBlocks @@ -165,6 +178,7 @@ async Task ReportCompileResults( var workspace = new Workspace( workspaceType: projectOrPackageName, + language: language, files: files.ToArray(), buffers: buffers.ToArray()); @@ -173,7 +187,7 @@ async Task ReportCompileResults( var processed = await mergeTransformer.TransformAsync(workspace); processed = await inliningTransformer.TransformAsync(processed); - processed = new Workspace(usings: processed.Usings, workspaceType: processed.WorkspaceType, files: processed.Files); + processed = new Workspace(usings: processed.Usings, workspaceType: processed.WorkspaceType, language:processed.Language, files: processed.Files); var result = await workspaceServer.Value.Compile(new WorkspaceRequest(processed)); @@ -257,5 +271,28 @@ void ReportCodeLinkageResults( } } } + + private static bool ProjectIsCompatibleWithLanguage(UriOrFileInfo projectOrPackage, string language) + { + var supported = true; + if (projectOrPackage.IsFile) + { + var extension = projectOrPackage.FileExtension.ToLowerInvariant(); + switch (extension) + { + case ".csproj": + supported = StringComparer.OrdinalIgnoreCase.Compare(language, "csharp") == 0; + break; + + case ".fsproj": + supported = StringComparer.OrdinalIgnoreCase.Compare(language, "fsharp") == 0; + break; + default: + supported = false; + break; + } + } + return supported; + } } } \ No newline at end of file diff --git a/MLS.Agent/MLS.Agent.csproj b/MLS.Agent/MLS.Agent.csproj index 3911a7402..452b99c54 100644 --- a/MLS.Agent/MLS.Agent.csproj +++ b/MLS.Agent/MLS.Agent.csproj @@ -129,7 +129,8 @@ + BeforeTargets="BeforeBuild" + Condition="'$(NCrunch)' != '1'"> <_TryDotNetCssExists Condition="Exists('$(MSBuildThisFileDirectory)wwwroot\css\trydotnet.css')">true @@ -147,7 +148,7 @@ - + $(MSBuildThisFileDirectory)..\Microsoft.DotNet.Try.Client $(MSBuildThisFileDirectory)wwwroot/client @@ -167,7 +168,8 @@ Inputs="@(TryDotNetJsInput)" Outputs="$(TryDotNetJsFile);$(TryDotNetJsMap)" DependsOnTargets="GatherInputs" - BeforeTargets="BeforeBuild"> + BeforeTargets="BeforeBuild" + Condition="'$(NCrunch)' != '1'"> <_TryDotNetMinJsExists Condition="Exists('$(TryDotNetJsFile)')">true @@ -189,7 +191,8 @@ Inputs="@(ClientInputFiles)" Outputs="$(ClientOutputFile)" DependsOnTargets="GatherInputs" - BeforeTargets="BeforeBuild"> + BeforeTargets="BeforeBuild" + Condition="'$(NCrunch)' != '1'"> <_TryDotNetClientExists Condition="Exists('$(ClientOutputFile)')">true diff --git a/MLS.Agent/MLS.Agent.v3.ncrunchproject b/MLS.Agent/MLS.Agent.v3.ncrunchproject index eb119af1c..069f496c5 100644 --- a/MLS.Agent/MLS.Agent.v3.ncrunchproject +++ b/MLS.Agent/MLS.Agent.v3.ncrunchproject @@ -5,6 +5,7 @@ ..\Microsoft.DotNet.Try.Client\**.* ..\Microsoft.DotNet.Try.js\**.* ..\Microsoft.DotNet.Try.Styles\**.* + wwwroot\**.* \ No newline at end of file diff --git a/MLS.Agent/Markdown/AnnotatedCodeBlockExtensions.cs b/MLS.Agent/Markdown/AnnotatedCodeBlockExtensions.cs index 7531c6268..46b47f40c 100644 --- a/MLS.Agent/Markdown/AnnotatedCodeBlockExtensions.cs +++ b/MLS.Agent/Markdown/AnnotatedCodeBlockExtensions.cs @@ -30,5 +30,10 @@ public static string ProjectOrPackageName(this AnnotatedCodeBlock block) (block.Annotations as LocalCodeBlockAnnotations)?.Project?.FullName ?? block.Annotations?.Package; } + public static string Language(this AnnotatedCodeBlock block) + { + return + block.Annotations?.NormalizedLanguage; + } } } \ No newline at end of file diff --git a/MLS.Agent/Markdown/LocalCodeFenceAnnotationsParser.cs b/MLS.Agent/Markdown/LocalCodeFenceAnnotationsParser.cs index 16c5f074b..f9a92de56 100644 --- a/MLS.Agent/Markdown/LocalCodeFenceAnnotationsParser.cs +++ b/MLS.Agent/Markdown/LocalCodeFenceAnnotationsParser.cs @@ -20,18 +20,24 @@ public class LocalCodeFenceAnnotationsParser : CodeFenceAnnotationsParser public LocalCodeFenceAnnotationsParser( IDirectoryAccessor directoryAccessor, PackageRegistry packageRegistry, - IDefaultCodeBlockAnnotations defaultAnnotations = null) : base(defaultAnnotations, csharp => - { - AddProjectOption(csharp, directoryAccessor); - AddSourceFileOption(csharp); - }) + IDefaultCodeBlockAnnotations defaultAnnotations = null) : base(defaultAnnotations, + csharp => + { + AddCsharpProjectOption(csharp, directoryAccessor); + AddSourceFileOption(csharp); + }, + fsharp => + { + AddFsharpProjectOption(fsharp, directoryAccessor); + AddSourceFileOption(fsharp); + }) { _directoryAccessor = directoryAccessor; _packageRegistry = packageRegistry ?? throw new ArgumentNullException(nameof(packageRegistry)); } public override CodeFenceOptionsParseResult TryParseCodeFenceOptions( - string line, + string line, MarkdownParserContext context = null) { var result = base.TryParseCodeFenceOptions(line, context); @@ -57,7 +63,7 @@ protected override ModelBinder CreateModelBinder() return new ModelBinder(typeof(LocalCodeBlockAnnotations)); } - private static void AddSourceFileOption(Command csharp) + private static void AddSourceFileOption(Command command) { var sourceFileArg = new Argument( result => @@ -76,46 +82,58 @@ private static void AddSourceFileOption(Command csharp) return ArgumentResult.Failure($"Error parsing the filename: {filename}"); }) - { - Name = "SourceFile", - Arity = ArgumentArity.ZeroOrOne - }; + { + Name = "SourceFile", + Arity = ArgumentArity.ZeroOrOne + }; var sourceFileOption = new Option("--source-file", argument: sourceFileArg); - csharp.AddOption(sourceFileOption); + command.AddOption(sourceFileOption); } - private static void AddProjectOption( - Command csharp, + private static void AddCsharpProjectOption( + Command command, IDirectoryAccessor directoryAccessor) + { + AddProjectOption(command, directoryAccessor, ".csproj"); + } + + private static void AddFsharpProjectOption( + Command command, + IDirectoryAccessor directoryAccessor) + { + AddProjectOption(command,directoryAccessor, ".fsproj"); + } + + private static void AddProjectOption( + Command command, + IDirectoryAccessor directoryAccessor, + string projectFileExtension) { var projectOptionArgument = new Argument(result => - { - var projectPath = new RelativeFilePath(result.Tokens.Select(t => t.Value).Single()); + { + var projectPath = new RelativeFilePath(result.Tokens.Select(t => t.Value).Single()); - if (directoryAccessor.FileExists(projectPath)) - { - return ArgumentResult.Success(directoryAccessor.GetFullyQualifiedPath(projectPath)); - } + if (directoryAccessor.FileExists(projectPath)) + { + return ArgumentResult.Success(directoryAccessor.GetFullyQualifiedPath(projectPath)); + } - return ArgumentResult.Failure($"Project not found: {projectPath.Value}"); - }) - { - Name = "project", - Arity = ArgumentArity.ExactlyOne - }; + return ArgumentResult.Failure($"Project not found: {projectPath.Value}"); + }) + { + Name = "project", + Arity = ArgumentArity.ExactlyOne + }; projectOptionArgument.SetDefaultValue(() => { var rootDirectory = directoryAccessor.GetFullyQualifiedPath(new RelativeDirectoryPath(".")); var projectFiles = directoryAccessor.GetAllFilesRecursively() - .Where(file => - { - return directoryAccessor.GetFullyQualifiedPath(file.Directory).FullName == rootDirectory.FullName && file.Extension == ".csproj"; - }) - .ToArray(); + .Where(file => directoryAccessor.GetFullyQualifiedPath(file.Directory).FullName == rootDirectory.FullName && file.Extension == projectFileExtension) + .ToArray(); if (projectFiles.Length == 1) { @@ -126,9 +144,9 @@ private static void AddProjectOption( }); var projectOption = new Option("--project", - argument: projectOptionArgument); + argument: projectOptionArgument); - csharp.Add(projectOption); + command.Add(projectOption); } } } \ No newline at end of file diff --git a/Microsoft.DotNet.Try.Client/src/IState.ts b/Microsoft.DotNet.Try.Client/src/IState.ts index 4339a799d..9c9423e8f 100644 --- a/Microsoft.DotNet.Try.Client/src/IState.ts +++ b/Microsoft.DotNet.Try.Client/src/IState.ts @@ -140,6 +140,7 @@ export interface IWorkspaceState export interface IWorkspace { workspaceType: string; + language?: string; files?: IWorkspaceFile[]; buffers: IWorkspaceBuffer[]; usings?: string[]; diff --git a/Microsoft.DotNet.Try.Markdown/AnnotatedCodeBlock.cs b/Microsoft.DotNet.Try.Markdown/AnnotatedCodeBlock.cs index f4cf3196c..233d8e975 100644 --- a/Microsoft.DotNet.Try.Markdown/AnnotatedCodeBlock.cs +++ b/Microsoft.DotNet.Try.Markdown/AnnotatedCodeBlock.cs @@ -15,7 +15,7 @@ namespace Microsoft.DotNet.Try.Markdown { public class AnnotatedCodeBlock : FencedCodeBlock { - protected readonly List _diagnostics = new List(); + private readonly List _diagnostics = new List(); private string _sourceCode; private bool _initialized; @@ -99,7 +99,7 @@ protected virtual async Task AddAttributes(CodeBlockAnnotations annotations) AddAttributeIfNotNull("data-trydotnet-region", annotations.Region); AddAttributeIfNotNull("data-trydotnet-session-id", annotations.Session); - AddAttribute("class", $"language-{annotations.Language}"); + AddAttribute("class", $"language-{annotations.NormalizedLanguage}"); } public void RenderTo( diff --git a/Microsoft.DotNet.Try.Markdown/AnnotatedCodeBlockParser.cs b/Microsoft.DotNet.Try.Markdown/AnnotatedCodeBlockParser.cs index 6acbf3b98..6d13fa73b 100644 --- a/Microsoft.DotNet.Try.Markdown/AnnotatedCodeBlockParser.cs +++ b/Microsoft.DotNet.Try.Markdown/AnnotatedCodeBlockParser.cs @@ -10,12 +10,12 @@ namespace Microsoft.DotNet.Try.Markdown { public class AnnotatedCodeBlockParser : FencedBlockParserBase { - private readonly CodeFenceAnnotationsParser codeFenceAnnotationsParser; + private readonly CodeFenceAnnotationsParser _codeFenceAnnotationsParser; private int _order; public AnnotatedCodeBlockParser(CodeFenceAnnotationsParser codeFenceAnnotationsParser) { - this.codeFenceAnnotationsParser = codeFenceAnnotationsParser ?? throw new ArgumentNullException(nameof(codeFenceAnnotationsParser)); + _codeFenceAnnotationsParser = codeFenceAnnotationsParser ?? throw new ArgumentNullException(nameof(codeFenceAnnotationsParser)); OpeningCharacters = new[] { '`' }; InfoParser = ParseCodeOptions; } @@ -30,7 +30,7 @@ protected bool ParseCodeOptions(BlockProcessor state, ref StringSlice line, IFen return false; } - var result = codeFenceAnnotationsParser.TryParseCodeFenceOptions(line.ToString(), + var result = _codeFenceAnnotationsParser.TryParseCodeFenceOptions(line.ToString(), state.Context); switch (result) diff --git a/Microsoft.DotNet.Try.Markdown/CodeBlockAnnotations.cs b/Microsoft.DotNet.Try.Markdown/CodeBlockAnnotations.cs index 3b03f7175..8163fd4c4 100644 --- a/Microsoft.DotNet.Try.Markdown/CodeBlockAnnotations.cs +++ b/Microsoft.DotNet.Try.Markdown/CodeBlockAnnotations.cs @@ -4,7 +4,9 @@ using System; using System.CommandLine; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; +using System.Xml; namespace Microsoft.DotNet.Try.Markdown { @@ -37,18 +39,23 @@ public CodeBlockAnnotations( { Session = $"Run{++_sessionIndex}"; } + + NormalizedLanguage = parseResult?.CommandResult.Name; + Language = parseResult?.Tokens.First().Value; + RunArgs = runArgs ?? Untokenize(parseResult); } public virtual string Package { get; } public RelativeFilePath DestinationFile { get; } public string Region { get; } - public string RunArgs { get; set; } + public string RunArgs { get; } public ParseResult ParseResult { get; } public string PackageVersion { get; } public string Session { get; } public bool Editable { get; } public bool Hidden { get; } - public string Language { get; set; } + public string Language { get; } + public string NormalizedLanguage { get; } public virtual Task TryGetExternalContent() => Task.FromResult(CodeBlockContentFetchResult.None); @@ -65,7 +72,22 @@ public virtual Task AddAttributes(AnnotatedCodeBlock block) block.AddAttribute("data-trydotnet-package-version", PackageVersion); } + if (!string.IsNullOrWhiteSpace(NormalizedLanguage)) + { + block.AddAttribute("data-trydotnet-language", NormalizedLanguage); + } + return Task.CompletedTask; } + + private static string Untokenize(ParseResult result) => + result == null + ? null + : string.Join(" ", result.Tokens + .Select(t => t.Value) + .Skip(1) + .Select(t => Regex.IsMatch(t, @".*\s.*") + ? $"\"{t}\"" + : t)); } } \ No newline at end of file diff --git a/Microsoft.DotNet.Try.Markdown/CodeFenceAnnotationsParser.cs b/Microsoft.DotNet.Try.Markdown/CodeFenceAnnotationsParser.cs index 049126ddc..81dd7cd8a 100644 --- a/Microsoft.DotNet.Try.Markdown/CodeFenceAnnotationsParser.cs +++ b/Microsoft.DotNet.Try.Markdown/CodeFenceAnnotationsParser.cs @@ -13,18 +13,20 @@ namespace Microsoft.DotNet.Try.Markdown { public class CodeFenceAnnotationsParser { - private readonly IDefaultCodeBlockAnnotations defaultAnnotations; + private readonly IDefaultCodeBlockAnnotations _defaultAnnotations; private readonly Parser _parser; private readonly Lazy _modelBinder; - private string packageOptionName = "--package"; - private string packageVersionOptionName = "--package-version"; + private HashSet _supportedLanguages; + private const string PackageOptionName = "--package"; + private const string PackageVersionOptionName = "--package-version"; public CodeFenceAnnotationsParser( IDefaultCodeBlockAnnotations defaultAnnotations = null, - Action configureCsharpCommand = null) + Action configureCsharpCommand = null, + Action configureFsharpCommand = null) { - this.defaultAnnotations = defaultAnnotations; - _parser = CreateOptionsParser(configureCsharpCommand); + _defaultAnnotations = defaultAnnotations; + _parser = CreateOptionsParser(configureCsharpCommand, configureFsharpCommand); _modelBinder = new Lazy(CreateModelBinder); } @@ -37,21 +39,21 @@ public virtual CodeFenceOptionsParseResult TryParseCodeFenceOptions( if (parserContext.TryGetDefaultCodeBlockAnnotations(out var defaults)) { if (defaults.Package != null && - !line.Contains(packageOptionName)) + !line.Contains(PackageOptionName)) { - line += $" {packageOptionName} {defaults.Package}"; + line += $" {PackageOptionName} {defaults.Package}"; } if (defaults.PackageVersion != null && - !line.Contains(packageVersionOptionName)) + !line.Contains(PackageVersionOptionName)) { - line += $" {packageVersionOptionName} {defaults.PackageVersion}"; + line += $" {PackageVersionOptionName} {defaults.PackageVersion}"; } } var result = _parser.Parse(line); - if (result.CommandResult.Name != "csharp" || + if (!_supportedLanguages.Contains(result.CommandResult.Name) || result.Tokens.Count == 1) { return CodeFenceOptionsParseResult.None; @@ -61,58 +63,61 @@ public virtual CodeFenceOptionsParseResult TryParseCodeFenceOptions( { return CodeFenceOptionsParseResult.Failed(new List(result.Errors.Select(e => e.Message))); } - else - { - var annotations = (CodeBlockAnnotations) _modelBinder.Value.CreateInstance(new BindingContext(result)); - annotations.Language = result.Tokens.First().Value; - annotations.RunArgs = Untokenize(result); + var annotations = (CodeBlockAnnotations)_modelBinder.Value.CreateInstance(new BindingContext(result)); - return CodeFenceOptionsParseResult.Succeeded(annotations); - } + return CodeFenceOptionsParseResult.Succeeded(annotations); } - private static string Untokenize(ParseResult result) => - string.Join(" ", result.Tokens - .Select(t => t.Value) - .Skip(1) - .Select(t => Regex.IsMatch(t, @".*\s.*") - ? $"\"{t}\"" - : t)); + private Parser CreateOptionsParser( + Action configureCsharpCommand = null, + Action configureFsharpCommand = null) + { + + var languageCommands = new[] + { + CreateCsharpCommand(configureCsharpCommand), + CreateFsharpCommand(configureFsharpCommand) + }; + _supportedLanguages = new HashSet(languageCommands.Select(c => c.Name)); + return new Parser(new RootCommand(symbols: languageCommands)); + } - private Parser CreateOptionsParser(Action configureCsharpCommand = null) + private IEnumerable