dotnet / try

Try .NET provides developers and content authors with tools to create interactive experiences.
MIT License
2.9k stars 526 forks source link

Extend dotnet try global tool to copy code blocks from sample solution to markdown files #729

Open ax0l0tl opened 4 years ago

ax0l0tl commented 4 years ago

Problem I want to use .net interactive documentation to write my documentation but markdown doc files should still display my code samples when viewed in an environment without try .net hosting (git hub README or simple markdown viewer). Right now I have to copy the sample regions manually from the C# projects to markdcown code blocks to make sure the samples are visible without try .net, which is error-prone and inconvenient.

Suggestion I would like to use the global dotnet try tool to keep my markdown in sync with the sample code. It would be great to have a sync command that just copies the referenced regions into the markdown code blocks. Usage would be similar to the existing verify command:

dotnet try sync

Even if the docs are never used as interactive docs one gets the great advantage of writing sample code with compiler support. So no more broken code samples in the documentation.

Basic implementation I created a basic implementation with the current try dotnet version. It is working for me and perhaps it is useful to start with (unfortunately pull requests are not possible at the moment):


using System.CommandLine;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Markdig;
using Markdig.Renderers.Normalize;
using Markdig.Syntax;
using Microsoft.DotNet.Try.Markdown;
using MLS.Agent.Markdown;
using MLS.Agent.Tools;
using WorkspaceServer;

namespace MLS.Agent.CommandLine
{
    public static class SyncCommand
    {
        public static async Task<int> Do(
            SyncOptions syncOptions,
            IConsole console,
            StartupOptions startupOptions = null
        )
        {
            var directoryAccessor = syncOptions.RootDirectory;
            var packageRegistry = PackageRegistry.CreateForTryMode(directoryAccessor);
            var markdownProject = new MarkdownProject(
                directoryAccessor,
                packageRegistry,
                startupOptions);

            var markdownFiles = markdownProject.GetAllMarkdownFiles().ToArray();
            if (markdownFiles.Length == 0)
            {
                console.Error.WriteLine($"No markdown files found under {directoryAccessor.GetFullyQualifiedRoot()}");
                return -1;
            }

            foreach (var markdownFile in markdownFiles)
            {
                var document = ParseMarkdownDocument(markdownFile);

                var pipeline = new MarkdownPipelineBuilder().UseNormalizeCodeBlockAnnotations().Build();
                var writer = new StringWriter();
                var renderer = new NormalizeRenderer(writer);
                renderer.Options.ExpandAutoLinks = true;
                pipeline.Setup(renderer);

                var blocks = document
                    .OfType<AnnotatedCodeBlock>()
                    .OrderBy(c => c.Order)
                    .ToList();

                if (!blocks.Any())
                    continue;

                await Task.WhenAll(blocks.Select(b => b.InitializeAsync()));

                renderer.Render(document);
                writer.Flush();

                var updated = writer.ToString();

                var fullName = directoryAccessor.GetFullyQualifiedPath(markdownFile.Path).FullName;
                File.WriteAllText(fullName, updated);

                console.Out.WriteLine($"Updated code sections in file {fullName}");
            }

            return 0;
        }

        private static MarkdownDocument ParseMarkdownDocument(MarkdownFile markdownFile)
        {
            var pipeline = markdownFile.Project.GetMarkdownPipelineFor(markdownFile.Path);

            var document = Markdig.Markdown.Parse(
                markdownFile.ReadAllText(),
                pipeline);
            return document;
        }
    }
}

using Markdig;
using Markdig.Renderers;
using Markdig.Renderers.Normalize;
using Markdig.Syntax;

namespace Microsoft.DotNet.Try.Markdown
{
    public static class MarkdownNormalizePipelineBuilderExtensions
    {
        public static MarkdownPipelineBuilder UseNormalizeCodeBlockAnnotations(
            this MarkdownPipelineBuilder pipeline)
        {
            var extensions = pipeline.Extensions;

            if (!extensions.Contains<NormalizeBlockAnnotationExtension>())
            {
                extensions.Add(new NormalizeBlockAnnotationExtension());
            }

            if (!extensions.Contains<SkipEmptyLinkReferencesExtension>())
            {
                extensions.Add(new SkipEmptyLinkReferencesExtension());
            }

            return pipeline;
        }
    }

    public class NormalizeBlockAnnotationExtension : IMarkdownExtension        
    {
        public void Setup(MarkdownPipelineBuilder pipeline)
        {
        }

        public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
        {
            var normalizeRenderer = renderer as NormalizeRenderer;
            var renderers = normalizeRenderer?.ObjectRenderers;
            if (renderers != null && !renderers.Contains<NormalizeAnnotatedCodeBlockRenderer>())
            {
                var codeLinkBlockRenderer = new NormalizeAnnotatedCodeBlockRenderer();
                renderers.Insert(0, codeLinkBlockRenderer);
            }   
        }

        public class NormalizeAnnotatedCodeBlockRenderer : CodeBlockRenderer
        {
            protected override void Write(
                NormalizeRenderer renderer,
                CodeBlock codeBlock)
            {
                if (codeBlock is AnnotatedCodeBlock codeLinkBlock)
                {
                    codeLinkBlock.Arguments = $"{codeLinkBlock.Annotations.Language} {codeLinkBlock.Annotations.RunArgs}";
                    base.Write(renderer, codeBlock);
                }
                else
                {
                    base.Write(renderer, codeBlock);
                }
            }
        }
    }

    public class SkipEmptyLinkReferencesExtension : IMarkdownExtension        
    {
        public void Setup(MarkdownPipelineBuilder pipeline)
        {
        }

        public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
        {
            var normalizeRenderer = renderer as NormalizeRenderer;
            var renderers = normalizeRenderer?.ObjectRenderers;
            if (renderers != null)
            {
                renderers.RemoveAll(r => r is LinkReferenceDefinitionRenderer);
                var linkRefRenderer = new SkipEmptyLinkReferencesRender();
                renderers.Insert(0, linkRefRenderer);
            }   
        }

        public class SkipEmptyLinkReferencesRender : LinkReferenceDefinitionRenderer
        {
            protected override void Write(NormalizeRenderer renderer, LinkReferenceDefinition linkDef)
            {
                if (linkDef.Label == null && linkDef.Url == null)
                    return;

                base.Write(renderer, linkDef);
            }
        }
    }
}
jonsequitur commented 4 years ago

Very nice!

This sounds like it relates to the publish feature we outlined last year (#214) but haven't gotten to.

If you're interested in making a PR, it would be appreciated and we'll be happy to help.

ax0l0tl commented 4 years ago

Hi @jonsequitur, thanks for linking #214, I missed that one. I would make a pull request perhaps with a reduced feature set of publish leaving out the --interactive switch. How can I do that? As far as I can see I need to be able to push to a feature branch or similar.

amis92 commented 4 years ago

@ax0l0tl I guess you might be new to working with GitHub. You'll need to fork this repo, push to a branch on a fork, and then make a PR here. Here is a guide on forking: https://guides.github.com/activities/forking/