Table of Contents

Create a custom plugin

In this topic we will create a plug-in to convert some simple rich text format files to html documents.

Goal and limitation

  1. In scope:
    1. Our input will be a set of rtf files with .rtf as the file extension name.
    2. The rtf files will be built as html document.
  2. Out of scope:
    1. Picture or other object in rtf files.
    2. Hyperlink in rtf files. (in the advanced tutorial, we will describe how to support hyperlinks in a custom plugin.)
    3. Metadata and title.

Preparation

  1. Create a new C# class library targeting net8.0 or later.

  2. Add NuGet package reference to System.Composition, Docfx.Plugins and Docfx.Common.

  3. Add a project for converting rtf to html: Clone project MarkupConverter, and reference it.

  4. Copy the code file CSharp/parallel/ParallelExtensionsExtras/TaskSchedulers/StaTaskScheduler.cs from DotNet Samples

Create a document processor

Responsibility of the document processor

  • Declare which file can be handled.
  • Load from the file to the object model.
  • Provide build steps.
  • Report document type, file links and xref links in document.
  • Update references.

Create our RtfDocumentProcessor

  1. Create a new class (RtfDocumentProcessor.cs) with the following code:

    [Export(typeof(IDocumentProcessor))]
    public class RtfDocumentProcessor : IDocumentProcessor
    {
        // todo : implements IDocumentProcessor.
    }
    
  2. Declare that we can handle the .rtf file:

    public ProcessingPriority GetProcessingPriority(FileAndType file)
    {
        if (file.Type == DocumentType.Article &&
            ".rtf".Equals(Path.GetExtension(file.File), StringComparison.OrdinalIgnoreCase))
        {
            return ProcessingPriority.Normal;
        }
        return ProcessingPriority.NotSupported;
    }
    

    Here we declare this processor can handle any .rtf file in the article category with normal priority. When two or more processors compete for the same file, DocFX will give it to the higher priority one. Unexpected: two or more processor declare for the same file with same priority.

  3. Load our rtf file by reading all text:

    public FileModel Load(FileAndType file, ImmutableDictionary<string, object> metadata)
    {
        var content = new Dictionary<string, object>
        {
            ["conceptual"] = File.ReadAllText(Path.Combine(file.BaseDir, file.File)),
            ["type"] = "Conceptual",
            ["path"] = file.File,
        };
        var localPathFromRoot = PathUtility.MakeRelativePath(EnvironmentContext.BaseDirectory, EnvironmentContext.FileAbstractLayer.GetPhysicalPath(file.File));
    
        return new FileModel(file, content)
        {
            LocalPathFromRoot = localPathFromRoot,
        };
    }
    

    We use Dictionary<string, object> as the data model, similar to how ConceptualDocumentProcessor stores the content of markdown files.

  4. Implement Save method as follows:

    public SaveResult Save(FileModel model)
    {
        return new SaveResult
        {
            DocumentType = "Conceptual",
            FileWithoutExtension = Path.ChangeExtension(model.File, null),
        };
    }
    
  5. BuildSteps property can provide several build steps for the model. We suggest implementing this in the following manner:

    [ImportMany(nameof(RtfDocumentProcessor))]
    public IEnumerable<IDocumentBuildStep> BuildSteps { get; set; }
    
  6. Name property is used to display in the log, so give any constant string you like.
    e.g.:

    public string Name => nameof(RtfDocumentProcessor);
    
  7. Since we don't support hyperlink, keep the UpdateHref method empty.

    public void UpdateHref(FileModel model, IDocumentBuildContext context)
    {
    }
    

View the final RtfDocumentProcessor.cs

Create a document build step

Responsibility of the build step

  • Reconstruct documents via the Prebuild method, e.g.: remove some document according to a certain rule.

  • Transform document content via Build method, e.g.: transform rtf content to html content.

  • Transform more content required by all document processed via the PostBuild method, e.g.: extract the link text from the title of another document.

  • About build order:

    1. For all documents in one processor always Prebuild -> Build -> Postbuild.
    2. For all documents in one processor always invoke Prebuild by BuildOrder.
    3. For each document in one processor always invoke Build by BuildOrder.
    4. For all documents in one processor always invoke Postbuild by BuildOrder.

    e.g.: Document processor X has two steps: A (with BuildOrder=1), B (with BuildOrder=2). When X is handling documents [D1, D2, D3], the invoke order is as follows:

    A.Prebuild([D1, D2, D3]) returns [D1, D2, D3]
    B.Prebuild([D1, D2, D3]) returns [D1, D2, D3]
    Parallel(
      A.Build(D1) -> B.Build(D1),
      A.Build(D2) -> B.Build(D2),
      A.Build(D3) -> B.Build(D3)
    )
    A.Postbuild([D1, D2, D3])
    B.Postbuild([D1, D2, D3])
    

Create our RtfBuildStep:

  1. Create a new class (RtfBuildStep.cs), and declare it is a build step for RtfDocumentProcessor:

    [Export(nameof(RtfDocumentProcessor), typeof(IDocumentBuildStep))]
    public class RtfBuildStep : IDocumentBuildStep
    {
        // todo : implements IDocumentBuildStep.
    }
    
  2. In the Build method, convert rtf to html:

    private readonly TaskFactory _taskFactory = new TaskFactory(new StaTaskScheduler(1));
    
    public void Build(FileModel model, IHostService host)
    {
        string content = (string)((Dictionary<string, object>)model.Content)["conceptual"];
        content = _taskFactory.StartNew(() => RtfToHtmlConverter.ConvertRtfToHtml(content)).Result;
        ((Dictionary<string, object>)model.Content)["conceptual"] = content;
    }
    
  3. Implement other methods:

    public int BuildOrder => 0;
    
    public string Name => nameof(RtfBuildStep);
    
    public void Postbuild(ImmutableList<FileModel> models, IHostService host)
    {
    }
    
    public IEnumerable<FileModel> Prebuild(ImmutableList<FileModel> models, IHostService host)
    {
        return models;
    }
    

View the final RtfBuildStep.cs

Enable plug-in

  1. Build our project.
  2. Copy the output dll files to:
    • Global: the Docfx executable directory.

    • Non-global: a folder you create with the name plugins under a template folder. Then run DocFX build command with parameter -t {template}.

      Hint: DocFX can merge templates so create a template that only contains the plugins folder, then run the command DocFX build with parameter -t {templateForRender},{templateForPlugins}.

Build document

  1. Run command DocFX init and set the source article with **.rtf.
  2. Run command DocFX build.