/// <summary> /// Extracts source directories from the csproj file. /// The expected directory info list is the csproj's directory and all project references. /// </summary> /// <param name="csprojFile"></param> /// <returns></returns> private DirectoryInfoBase[] ExtractSourceDirectories(FileInfoBase csprojFile) { // Data validation Ensure.That(() => csprojFile).IsNotNull(); Ensure.That(csprojFile.Exists, string.Format("Could not find '{0}' file", csprojFile)).IsTrue(); // Initialize the extracted directories List<DirectoryInfoBase> extractedSourceDirectories = new List<DirectoryInfoBase>(); // Add the csproj's directory DirectoryInfoBase projectDirectory = this.fileSystem.DirectoryInfo.FromDirectoryName(this.fileSystem.Path.GetDirectoryName(csprojFile.FullName)); extractedSourceDirectories.Add(projectDirectory); // Load xml document XmlDocument xmlDocument = new XmlDocument(); using (Stream stream = csprojFile.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { xmlDocument.Load(stream); } // Extract XmlNamespaceManager xmlNamespaceManager = new XmlNamespaceManager(xmlDocument.NameTable); xmlNamespaceManager.AddNamespace("msbuild", "http://schemas.microsoft.com/developer/msbuild/2003"); XmlNodeList xmlNodes = xmlDocument.SelectNodes("//msbuild:ProjectReference", xmlNamespaceManager); for (int i = 0; i < xmlNodes.Count; ++i) { XmlNode xmlNode = xmlNodes.Item(i); string includeValue = xmlNode.Attributes["Include"].Value; string combinedPath = this.fileSystem.Path.Combine(projectDirectory.FullName, includeValue); // The combinedPath can contains both forward and backslash path chunk. // In linux environment we can end up having "/..\" in the path which make the GetDirectoryName method bugging (returns empty). // For this reason we need to make sure that the combined path uses forward slashes combinedPath = combinedPath.Replace(@"\", "/"); // Add the combined path extractedSourceDirectories.Add(this.fileSystem.DirectoryInfo.FromDirectoryName(this.fileSystem.Path.GetDirectoryName(this.fileSystem.Path.GetFullPath(combinedPath)))); } // Return the extracted directories return extractedSourceDirectories.ToArray(); }
/// <summary> /// Generates the index page for the documentation. /// </summary> /// <param name="indexConfiguration">The index configuration.</param> /// <returns>The generation errors.</returns> public GenerationError[] GenerateIndex(IndexConfiguration indexConfiguration) { // Data validation Ensure.That(() => indexConfiguration).IsNotNull(); // Try to generate the index html file List<GenerationError> generationError = new List<GenerationError>(); try { string outputFileHtml = this.fileSystem.Path.Combine(this.OutputDirectory.FullName, indexConfiguration.Output); this.GenerateIndexFile(indexConfiguration.Template, outputFileHtml, indexConfiguration); } catch (TemplateParsingException templateParsingException) { generationError.Add(new Model.GenerationError(indexConfiguration.Template, string.Format("Error during HTML generation: {0}", templateParsingException.Message), templateParsingException.Line, templateParsingException.Column)); } catch (System.Exception exception) { generationError.Add(new Model.GenerationError(indexConfiguration.Template, string.Format("Error during HTML generation: {0}", exception.Message), 0, 0)); } // Return generation errors return generationError.ToArray(); }
/// <summary> /// Generates all documents. /// </summary> /// <returns>The generation errors.</returns> public GenerationError[] GenerateAll() { // Register bundles if pdf generation is not skipped if (!this.SkipPdf) this.RegisterHtmlToXLibraries(); // Run generation for each configuration List<GenerationError> errors = new List<GenerationError>(); foreach (Configuration configuration in this.IndexConfiguration.Configurations) { // Generate the documentation errors.AddRange(this.Generate(configuration)); } // Generate the index html file if any index template is set if (!string.IsNullOrWhiteSpace(this.IndexConfiguration.Template)) { errors.AddRange(this.GenerateIndex(this.IndexConfiguration)); } // Report processing successful return errors.ToArray(); }
/// <summary> /// Generates documentation. /// </summary> /// <returns>The generation errors.</returns> public Model.GenerationError[] Generate(Configuration configuration) { // Initialize the list containing all generation errors List<Model.GenerationError> generationError = new List<Model.GenerationError>(); // Ensure output directory exists if (!this.OutputDirectory.Exists) { this.OutputDirectory.Create(); } // Process all pages List<Model.Page> pages = new List<Model.Page>(); foreach (Page page in configuration.Pages) { // Compute the page id used as a tab id and page prefix for bookmarking string pageId = page.Path.Replace(".", string.Empty).Replace("/", string.Empty).Replace("\\", string.Empty); // Load the document Block document; // Process the page string pageFilePath = this.fileSystem.FileInfo.FromFileName(page.FileSystemPath).FullName; using (StreamReader reader = new StreamReader(this.fileSystem.File.Open(pageFilePath, FileMode.Open, FileAccess.Read, FileShare.Read))) { document = CommonMarkConverter.ProcessStage1(reader); } // Process snippet CommonMarkConverter.ProcessStage2(document); Dictionary<Guid, Extension.Model.Snippet> snippetDictionary = new Dictionary<Guid, Extension.Model.Snippet>(); foreach (var node in document.AsEnumerable()) { // Filter fenced code if (node.Block != null && node.Block.Tag == BlockTag.FencedCode) { // Build extraction rule string fencedCode = node.Block.FencedCodeData.Info; // Do not trigger extraction if the fenced code is empty if (string.IsNullOrWhiteSpace(fencedCode)) { continue; } SnippetExtractionRule snippetExtractionRule = SnippetExtractionRule.Parse(fencedCode); // Extract and inject snippet and the factory were able to create an extractor if (null != snippetExtractionRule) { // Cleanup Projbook specific syntax node.Block.FencedCodeData.Info = snippetExtractionRule.Language; // Inject snippet try { // Retrieve the extractor instance ISnippetExtractor snippetExtractor; if (!this.extractorCache.TryGetValue(snippetExtractionRule.TargetPath, out snippetExtractor)) { snippetExtractor = this.snippetExtractorFactory.CreateExtractor(snippetExtractionRule); this.extractorCache[snippetExtractionRule.TargetPath] = snippetExtractor; } // Look for the file in available source directories FileSystemInfoBase fileSystemInfo = null; DirectoryInfoBase[] directoryInfos = null; if (TargetType.FreeText != snippetExtractor.TargetType) { directoryInfos = this.ExtractSourceDirectories(this.CsprojFile); foreach (DirectoryInfoBase directoryInfo in directoryInfos) { // Get directory name string directoryName = directoryInfo.FullName; if (1 == directoryName.Length && Path.DirectorySeparatorChar == directoryName[0]) directoryName = string.Empty; // Compute file full path string fullFilePath = this.fileSystem.Path.GetFullPath(this.fileSystem.Path.Combine(directoryName, snippetExtractionRule.TargetPath ?? string.Empty)); switch (snippetExtractor.TargetType) { case TargetType.File: if (this.fileSystem.File.Exists(fullFilePath)) { fileSystemInfo = this.fileSystem.FileInfo.FromFileName(fullFilePath); } break; case TargetType.Folder: if (this.fileSystem.Directory.Exists(fullFilePath)) { fileSystemInfo = this.fileSystem.DirectoryInfo.FromDirectoryName(fullFilePath); } break; } // Stop lookup if the file system info is found if (null != fileSystemInfo) { break; } } } // Raise an error if cannot find the file if (null == fileSystemInfo && TargetType.FreeText != snippetExtractor.TargetType) { // Locate block line int line = this.LocateBlockLine(node.Block, page); // Compute error column: Index of the path in the fenced code + 3 (for the ``` prefix) + 1 (to be 1 based) int column = fencedCode.IndexOf(snippetExtractionRule.TargetPath) + 4; // Report error generationError.Add(new Model.GenerationError( sourceFile: page.Path, message: string.Format("Cannot find target '{0}' in any referenced project ({0})", snippetExtractionRule.TargetPath, string.Join(";", directoryInfos.Select(x => x.FullName))), line: line, column: column)); continue; } // Extract the snippet Extension.Model.Snippet snippet = TargetType.FreeText == snippetExtractor.TargetType ? snippetExtractor.Extract(null, snippetExtractionRule.TargetPath) : snippetExtractor.Extract(fileSystemInfo, snippetExtractionRule.Pattern); // Reference snippet Guid guid = Guid.NewGuid(); snippetDictionary[guid] = snippet; // Inject reference as content StringContent code = new StringContent(); string content = SNIPPET_REFERENCE_PREFIX + guid; code.Append(content, 0, content.Length); node.Block.StringContent = code; // Change tag to html for node snippets NodeSnippet nodeSnippet = snippet as NodeSnippet; if (null != nodeSnippet) { node.Block.Tag = BlockTag.HtmlBlock; } } catch (SnippetExtractionException snippetExtraction) { // Locate block line int line = this.LocateBlockLine(node.Block, page); // Compute error column: Fenced code length - pattern length + 3 (for the ``` prefix) + 1 (to be 1 based) int column = fencedCode.Length - snippetExtractionRule.Pattern.Length + 4; // Report error generationError.Add(new Model.GenerationError( sourceFile: page.Path, message: string.Format("{0}: {1}", snippetExtraction.Message, snippetExtraction.Pattern), line: line, column: column)); } catch (System.Exception exception) { generationError.Add(new Model.GenerationError( sourceFile: page.Path, message: exception.Message, line: 0, column: 0)); } } } } // Write to output ProjbookHtmlFormatter projbookHtmlFormatter = null; MemoryStream documentStream = new MemoryStream(); using (StreamWriter writer = new StreamWriter(documentStream)) { // Setup custom formatter CommonMarkSettings.Default.OutputDelegate = (d, o, s) => (projbookHtmlFormatter = new ProjbookHtmlFormatter(pageId, o, s, configuration.SectionTitleBase, snippetDictionary, SNIPPET_REFERENCE_PREFIX)).WriteDocument(d); // Render CommonMarkConverter.ProcessStage3(document, writer); } // Initialize the pre section content string preSectionContent = string.Empty; // Retrieve page content byte[] pageContent = documentStream.ToArray(); // Set the whole page content if no page break is detected if (projbookHtmlFormatter.PageBreak.Length == 0) { preSectionContent = System.Text.Encoding.UTF8.GetString(pageContent); } // Compute pre section content from the position 0 to the first page break position if (projbookHtmlFormatter.PageBreak.Length > 0 && projbookHtmlFormatter.PageBreak.First().Position > 0) { preSectionContent = this.StringFromByteArray(pageContent, 0, projbookHtmlFormatter.PageBreak.First().Position); } // Build section list List<Model.Section> sections = new List<Model.Section>(); for (int i = 0; i < projbookHtmlFormatter.PageBreak.Length; ++i) { // Retrieve the current page break PageBreakInfo pageBreak = projbookHtmlFormatter.PageBreak[i]; // Extract the content from the current page break to the next one if any string content = null; if (i < projbookHtmlFormatter.PageBreak.Length - 1) { PageBreakInfo nextBreak = projbookHtmlFormatter.PageBreak[1 + i]; content = this.StringFromByteArray(pageContent, pageBreak.Position, nextBreak.Position - pageBreak.Position); } // Otherwise extract the content from the current page break to the end of the content else { content = this.StringFromByteArray(pageContent, pageBreak.Position, pageContent.Length - pageBreak.Position); } // Create a new section and add to the known list sections.Add(new Model.Section( id: pageBreak.Id, level: pageBreak.Level, title: pageBreak.Title, content: content)); } // Add new page pages.Add(new Model.Page( id: pageId, title: page.Title, preSectionContent: preSectionContent, sections: sections.ToArray())); } // Html generation if (configuration.GenerateHtml) { try { string outputFileHtml = this.fileSystem.Path.Combine(this.OutputDirectory.FullName, configuration.OutputHtml); this.GenerateFile(configuration.TemplateHtml, outputFileHtml, configuration, pages); } catch (TemplateParsingException templateParsingException) { generationError.Add(new Model.GenerationError(configuration.TemplateHtml, string.Format("Error during HTML generation: {0}", templateParsingException.Message), templateParsingException.Line, templateParsingException.Column)); } catch (System.Exception exception) { generationError.Add(new Model.GenerationError(configuration.TemplateHtml, string.Format("Error during HTML generation: {0}", exception.Message), 0, 0)); } } // Pdf generation if (configuration.GeneratePdf && !this.SkipPdf) { try { // Generate the pdf template string outputFileHtml = this.fileSystem.Path.Combine(this.OutputDirectory.FullName, configuration.OutputPdf); this.GenerateFile(configuration.TemplatePdf, outputFileHtml, configuration, pages); #if !NOPDF // Compute file names string outputPdf = this.fileSystem.Path.ChangeExtension(configuration.OutputPdf, ".pdf"); string outputFilePdf = this.fileSystem.Path.Combine(this.OutputDirectory.FullName, outputPdf); // Prepare the converter MultiplexingConverter pdfConverter = new MultiplexingConverter(); pdfConverter.ObjectSettings.Page = outputFileHtml; pdfConverter.Error += (s, e) => { generationError.Add(new Model.GenerationError(configuration.TemplatePdf, string.Format("Error during PDF generation: {0}", e.Value), 0, 0)); }; // Prepare file system if abstracted bool requireCopyToFileSystem = !File.Exists(outputFileHtml); try { // File system may be abstracted, this requires to copy the pdf generation file to the actual file system // in order to allow wkhtmltopdf to process the generated html as input file if (requireCopyToFileSystem) { File.WriteAllBytes(outputFileHtml, this.fileSystem.File.ReadAllBytes(outputFileHtml)); } // Run pdf converter using (pdfConverter) using (Stream outputFileStream = this.fileSystem.File.Open(outputFilePdf, FileMode.Create, FileAccess.Write, FileShare.None)) { try { byte[] buffer = pdfConverter.Convert(); outputFileStream.Write(buffer, 0, buffer.Length); } catch { // Ignore generation errors at that level // Errors are handled by the error handling having the best description } } } finally { if (requireCopyToFileSystem && File.Exists(outputFileHtml)) { File.Delete(outputFileHtml); } } #endif } catch (TemplateParsingException templateParsingException) { generationError.Add(new Model.GenerationError(configuration.TemplatePdf, string.Format("Error during PDF generation: {0}", templateParsingException.Message), templateParsingException.Line, templateParsingException.Column)); } catch (System.Exception exception) { if (null != exception.InnerException && INCORRECT_FORMAT_HRESULT == exception.InnerException.HResult) { // Report detailed error message for wrong architecture loading string runningArchitectureProccess = IntPtr.Size == 8 ? "x64" : "x86"; string otherRunningArchitectureProccess = IntPtr.Size != 8 ? "x64" : "x86"; generationError.Add(new Model.GenerationError(configuration.TemplatePdf, string.Format("Error during PDF generation: Could not load wkhtmltopdf for {0}. Try again running as a {1} process.", runningArchitectureProccess, otherRunningArchitectureProccess), 0, 0)); } else { // Report unknown error generationError.Add(new Model.GenerationError(configuration.TemplatePdf, string.Format("Error during PDF generation: {0}", exception.Message), 0, 0)); } } } // Return the generation errors return generationError.ToArray(); }
/// <summary> /// Generates documentation. /// </summary> public Model.GenerationError[] Generate() { // Initialize the list containing all generation errors List<Model.GenerationError> generationError = new List<Model.GenerationError>(); // Ensute output directory exists if (!this.OutputDirectory.Exists) { this.OutputDirectory.Create(); } // Read configuration ConfigurationLoader configurationLoader = new ConfigurationLoader(); Configuration configuration; try { configuration = configurationLoader.Load(this.ConfigFile); } catch (System.Exception exception) { generationError.Add(new Model.GenerationError(this.ConfigFile.FullName, string.Format("Error during loading configuration: {0}", exception.Message))); return generationError.ToArray(); } // Process all pages List<Model.Page> pages = new List<Model.Page>(); bool first = true; foreach (Page page in configuration.Pages) { // Declare formatter InjectAnchorHtmlFormatter formatter = null; // Load the document Block document; FileInfo fileInfo = new FileInfo(page.Path); // Skip the page if doesn't exist if (!fileInfo.Exists) { generationError.Add(new Model.GenerationError(this.ConfigFile.FullName, string.Format("Error during loading configuration: Could not find file '{0}'", fileInfo.FullName))); continue; } // Process the page using (StreamReader reader = new StreamReader(new FileStream(new FileInfo(page.Path).FullName, FileMode.Open))) { document = CommonMarkConverter.ProcessStage1(reader); } // Process snippet CommonMarkConverter.ProcessStage2(document); foreach (var node in document.AsEnumerable()) { // Filter fenced code if (node.Block != null && node.Block.Tag == BlockTag.FencedCode) { // Extract snippet string fencedCode = node.Block.FencedCodeData.Info; ISnippetExtractor snippetExtractor = this.snippetExtractorFactory.CreateExtractor(fencedCode); // Extract and inject snippet and the factory were able to create an extractor if (null != snippetExtractor) { // Cleanup Projbook specific syntax node.Block.FencedCodeData.Info = snippetExtractor.Language; // Inject snippet try { Model.Snippet snippet = snippetExtractor.Extract(); StringContent code = new StringContent(); code.Append(snippet.Content, 0, snippet.Content.Length); node.Block.StringContent = code; } catch (SnippetExtractionException snippetExtraction) { generationError.Add(new Model.GenerationError( sourceFile: page.Path, message: string.Format("{0}: {1}", snippetExtraction.Message, snippetExtraction.Pattern))); } catch (System.Exception exception) { generationError.Add(new Model.GenerationError( sourceFile: page.Path, message: exception.Message)); } } } } // Setup custom formatter CommonMarkSettings.Default.OutputDelegate = (d, output, settings) => { formatter = new InjectAnchorHtmlFormatter(page.Path, output, settings); formatter.WriteDocument(d); }; // Write to output MemoryStream documentStream = new MemoryStream(); using (StreamWriter writer = new StreamWriter(documentStream)) { CommonMarkConverter.ProcessStage3(document, writer); } // Add new page pages.Add(new Model.Page( id: page.Path.Replace(".", string.Empty).Replace("/", string.Empty), title: page.Title, isHome: first, anchor: formatter.Anchors, content: System.Text.Encoding.UTF8.GetString(documentStream.ToArray()))); first = false; } // Standard generation try { this.GenerateFile(this.TemplateFile.FullName, this.TemplateFile.FullName, configuration, pages); } catch (System.Exception exception) { generationError.Add(new Model.GenerationError(this.TemplateFile.FullName, string.Format("Error during HTML generation: {0}", exception.Message))); } // Pdf specific generation string pdfTemplate = this.TemplateFilePdf.Exists ? this.TemplateFilePdf.FullName : this.TemplateFile.FullName; try { this.GenerateFile(pdfTemplate, this.TemplateFilePdf.FullName, configuration, pages); } catch (System.Exception exception) { generationError.Add(new Model.GenerationError(pdfTemplate, string.Format("Error during PDF generation: {0}", exception.Message))); } // Return the generation errors return generationError.ToArray(); }
/// <summary> /// Generates documentation. /// </summary> public Model.GenerationError[] Generate() { // Initialize the list containing all generation errors List<Model.GenerationError> generationError = new List<Model.GenerationError>(); // Ensute output directory exists if (!this.OutputDirectory.Exists) { this.OutputDirectory.Create(); } // Read configuration ConfigurationLoader configurationLoader = new ConfigurationLoader(); Configuration configuration; try { configuration = configurationLoader.Load(this.ConfigFile); } catch (System.Exception exception) { generationError.Add(new Model.GenerationError(this.ConfigFile.FullName, string.Format("Error during loading configuration: {0}", exception.Message))); return generationError.ToArray(); } // Process all pages List<Model.Page> pages = new List<Model.Page>(); foreach (Page page in configuration.Pages) { // Compute the page id used as a tab id and page prefix for bookmarking string pageId = page.Path.Replace(".", string.Empty).Replace("/", string.Empty); // Load the document Block document; FileInfo fileInfo = new FileInfo(page.Path); // Skip the page if doesn't exist if (!fileInfo.Exists) { generationError.Add(new Model.GenerationError(this.ConfigFile.FullName, string.Format("Error during loading configuration: Could not find file '{0}'", fileInfo.FullName))); continue; } // Process the page using (StreamReader reader = new StreamReader(new FileStream(new FileInfo(page.Path).FullName, FileMode.Open))) { document = CommonMarkConverter.ProcessStage1(reader); } // Process snippet CommonMarkConverter.ProcessStage2(document); foreach (var node in document.AsEnumerable()) { // Filter fenced code if (node.Block != null && node.Block.Tag == BlockTag.FencedCode) { // Buil extraction rule string fencedCode = node.Block.FencedCodeData.Info; SnippetExtractionRule snippetExtractionRule = SnippetExtractionRule.Parse(fencedCode); // Extract and inject snippet and the factory were able to create an extractor if (null != snippetExtractionRule) { // Cleanup Projbook specific syntax node.Block.FencedCodeData.Info = snippetExtractionRule.Language; // Inject snippet try { // Retrieve the extractor instance ISnippetExtractor snippetExtractor; if (!this.extractorCache.TryGetValue(snippetExtractionRule.FileName, out snippetExtractor)) { snippetExtractor = this.snippetExtractorFactory.CreateExtractor(snippetExtractionRule); this.extractorCache[snippetExtractionRule.FileName] = snippetExtractor; } // Extract the snippet Model.Snippet snippet = snippetExtractor.Extract(snippetExtractionRule.FileName, snippetExtractionRule.Pattern); StringContent code = new StringContent(); code.Append(snippet.Content, 0, snippet.Content.Length); node.Block.StringContent = code; } catch (SnippetExtractionException snippetExtraction) { generationError.Add(new Model.GenerationError( sourceFile: page.Path, message: string.Format("{0}: {1}", snippetExtraction.Message, snippetExtraction.Pattern))); } catch (System.Exception exception) { generationError.Add(new Model.GenerationError( sourceFile: page.Path, message: exception.Message)); } } } } // Setup custom formatter CommonMarkSettings.Default.OutputDelegate = (d, o, s) => new InjectAnchorHtmlFormatter(pageId, this.sectionSplittingIdentifier, o, s).WriteDocument(d); // Write to output MemoryStream documentStream = new MemoryStream(); using (StreamWriter writer = new StreamWriter(documentStream)) { CommonMarkConverter.ProcessStage3(document, writer); } // Initialize the pre section content string preSectionContent = string.Empty; // Retrieve page content string pageContent = System.Text.Encoding.UTF8.GetString(documentStream.ToArray()); // Build section list List<Model.Section> sections = new List<Model.Section>(); Match match = regex.Match(pageContent); bool matched = false; while(match.Success) { // Initialize the pre section part from 0 to the first matching index for the first matching if (!matched) { preSectionContent = pageContent.Substring(0, match.Groups[0].Index); } // Create a new section and add to the known list sections.Add(new Model.Section( id: match.Groups[2].Value, title: match.Groups[1].Value, content: match.Groups[3].Value)); // Mode to the next match match = match.NextMatch(); matched = true; } // If nothing has been matching simple consider the whole input as pre section content if (!matched) { preSectionContent = pageContent; } // Add new page pages.Add(new Model.Page( id: pageId, title: page.Title, preSectionContent: preSectionContent, sections: sections.ToArray())); } // Standard generation try { this.GenerateFile(this.TemplateFile.FullName, this.TemplateFile.FullName, configuration, pages); } catch (System.Exception exception) { generationError.Add(new Model.GenerationError(this.TemplateFile.FullName, string.Format("Error during HTML generation: {0}", exception.Message))); } // Pdf specific generation string pdfTemplate = this.TemplateFilePdf.Exists ? this.TemplateFilePdf.FullName : this.TemplateFile.FullName; try { this.GenerateFile(pdfTemplate, this.TemplateFilePdf.FullName, configuration, pages); } catch (System.Exception exception) { generationError.Add(new Model.GenerationError(pdfTemplate, string.Format("Error during PDF generation: {0}", exception.Message))); } // Return the generation errors return generationError.ToArray(); }