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
- In scope:
- Our input will be a set of rtf files with
.rtf
as the file extension name. - The rtf files will be built as html document.
- Our input will be a set of rtf files with
- Out of scope:
- Picture or other object in rtf files.
- Hyperlink in rtf files. (in the advanced tutorial, we will describe how to support hyperlinks in a custom plugin.)
- Metadata and title.
Preparation
Create a new C# class library targeting
net8.0
or later.Add NuGet package reference to
System.Composition
,Docfx.Plugins
andDocfx.Common
.Add a project for converting rtf to html: Clone project MarkupConverter, and reference it.
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
Create a new class (RtfDocumentProcessor.cs) with the following code:
[Export(typeof(IDocumentProcessor))] public class RtfDocumentProcessor : IDocumentProcessor { // todo : implements IDocumentProcessor. }
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.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.Implement
Save
method as follows:public SaveResult Save(FileModel model) { return new SaveResult { DocumentType = "Conceptual", FileWithoutExtension = Path.ChangeExtension(model.File, null), }; }
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; }
Name
property is used to display in the log, so give any constant string you like.
e.g.:public string Name => nameof(RtfDocumentProcessor);
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:
- For all documents in one processor always
Prebuild
->Build
->Postbuild
. - For all documents in one processor always invoke
Prebuild
byBuildOrder
. - For each document in one processor always invoke
Build
byBuildOrder
. - For all documents in one processor always invoke
Postbuild
byBuildOrder
.
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])
- For all documents in one processor always
Create our RtfBuildStep:
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. }
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; }
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
- Build our project.
- 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 runDocFX build
command with parameter-t {template}
.Hint: DocFX can merge templates so create a template that only contains the
plugins
folder, then run the commandDocFX build
with parameter-t {templateForRender},{templateForPlugins}
.
Build document
- Run command
DocFX init
and set the source article with**.rtf
. - Run command
DocFX build
.