diff options
Diffstat (limited to 'tools/Sandcastle/Source/ChmBuilder/ChmBuilder.cs')
-rw-r--r-- | tools/Sandcastle/Source/ChmBuilder/ChmBuilder.cs | 739 |
1 files changed, 739 insertions, 0 deletions
diff --git a/tools/Sandcastle/Source/ChmBuilder/ChmBuilder.cs b/tools/Sandcastle/Source/ChmBuilder/ChmBuilder.cs new file mode 100644 index 0000000..1e2aeb2 --- /dev/null +++ b/tools/Sandcastle/Source/ChmBuilder/ChmBuilder.cs @@ -0,0 +1,739 @@ +// Copyright © Microsoft Corporation. +// This source file is subject to the Microsoft Permissive License. +// See http://www.microsoft.com/resources/sharedsource/licensingbasics/sharedsourcelicenses.mspx. +// All other rights reserved. + +using System; +using System.Collections; +using System.Collections.Specialized; +using System.Collections.Generic; +using System.Xml; +using System.Xml.XPath; +using System.Text; +using System.Text.RegularExpressions; +using System.IO; +using System.Reflection; +using Microsoft.Ddue.Tools.CommandLine; + + +namespace Microsoft.Ddue.Tools +{ + + /// <summary> + /// <language id="1033" codepage="65001" name="0x409 English (United States)" /> + /// <language id="2052" codepage="936" name="0x804 Chinese (PRC)" /> + /// </summary> + internal struct LangInfo + { + public int CodePage; + public int ID; + public string Name; + } + + internal struct KKeywordInfo + { + public string File; + public string MainEntry; + public string SubEntry; + } + + public class ChmBuilder + { + + private ChmBuilderArgs _args; + private XPathDocument _config; + //private bool _metadata; + + //defalut topic of chm: get this value when gerenating hhc, save to hhp + private string _defaultTopic = string.Empty; + private bool _hasToc; + + private int _indentCount = 0; + //private string _htmlDirectory; + //private string _tocFile; + //private string _projectName; + //private string _outputDirectory; + private LangInfo _lang; + + //store all "K" type Keywords + List < KKeywordInfo > kkwdTable = new List < KKeywordInfo >(); + + //store all titles from html files + Hashtable titleTable = new Hashtable(); + + + public ChmBuilder(ChmBuilderArgs args) + { + this._args = args; + _args.htmlDirectory = StripEndBackSlash(Path.GetFullPath(_args.htmlDirectory)); + if (String.IsNullOrEmpty(_args.tocFile)) + _hasToc = false; + else + _hasToc = true; + _args.outputDirectory = StripEndBackSlash(Path.GetFullPath(_args.outputDirectory)); + _config = new XPathDocument(args.configFile); + LoadLanginfo(_args.langid); + } + public static int Main(string[] args) + { + ConsoleApplication.WriteBanner(); + + OptionCollection options = new OptionCollection(); + options.Add(new SwitchOption("?", "Show this help page.")); + options.Add(new StringOption("html", "Specify a html directory.", "htmlDirectory")); + options.Add(new StringOption("project", "Specify a project name.", "projectName")); + options.Add(new StringOption("toc", "Specify a toc file.", "tocFile")); + options.Add(new StringOption("lcid", "Specify a language id.If unspecified, 1033 is used.", "languageId")); + options.Add(new StringOption("out", "Specify an output directory. If unspecified, Chm is used.", "outputDirectory")); + options.Add(new BooleanOption("metadata", "Specify whether output metadata or not. Default value is false.")); + options.Add(new StringOption("config", "Specify a configuration file. If unspecified, ChmBuilder.config is used", "configFilePath")); + + ParseArgumentsResult results = options.ParseArguments(args); + if (results.Options["?"].IsPresent) + { + Console.WriteLine("ChmBuilder /html: /project: /toc: /out: /metadata:"); + options.WriteOptionSummary(Console.Out); + return (0); + } + + ChmBuilderArgs cbArgs = new ChmBuilderArgs(); + + // check for invalid options + if (!results.Success) + { + results.WriteParseErrors(Console.Out); + return (1); + } + + // check for missing or extra assembly directories + if (results.UnusedArguments.Count != 0) + { + Console.WriteLine("No non-option arguments are supported."); + return (1); + } + + if (!results.Options["html"].IsPresent) + { + ConsoleApplication.WriteMessage(LogLevel.Error, "You must specify a html directory."); + return (1); + } + cbArgs.htmlDirectory = (string)results.Options["html"].Value; + if (!Directory.Exists(cbArgs.htmlDirectory)) + { + ConsoleApplication.WriteMessage(LogLevel.Error, String.Format("Direcotry: {0} not found.", cbArgs.htmlDirectory)); + return (1); + } + + if (!results.Options["project"].IsPresent) + { + ConsoleApplication.WriteMessage(LogLevel.Error, "You must specify a project name."); + return (1); + } + cbArgs.projectName = (string)results.Options["project"].Value; + + if (results.Options["lcid"].IsPresent) + { + try + { + cbArgs.langid = Convert.ToInt32(results.Options["lcid"].Value); + } + catch + { + ConsoleApplication.WriteMessage(LogLevel.Error, String.Format("{0} is not a valid integer.", results.Options["lcid"].Value)); + return (1); + } + } + + + if (results.Options["toc"].IsPresent) + { + cbArgs.tocFile = (string)results.Options["toc"].Value; + if (!File.Exists(cbArgs.tocFile)) + { + ConsoleApplication.WriteMessage(LogLevel.Error, String.Format("File: {0} not found.", cbArgs.tocFile)); + return (1); + } + } + + if (!results.Options["out"].IsPresent) + cbArgs.outputDirectory = "Chm"; + else + cbArgs.outputDirectory = (string)results.Options["out"].Value; + if (!Directory.Exists(cbArgs.outputDirectory)) + { + Directory.CreateDirectory(cbArgs.outputDirectory); + //ConsoleApplication.WriteMessage(LogLevel.Error, String.Format("Direcotry: {0} not found.", cbArgs.outputDirectory)); + //return (1); + } + + if (results.Options["metadata"].IsPresent && (bool)results.Options["metadata"].Value) + { + cbArgs.metadata = true; + } + + if (results.Options["config"].IsPresent) + { + cbArgs.configFile = (string)results.Options["config"].Value; + } + if (!File.Exists(cbArgs.configFile)) + { + ConsoleApplication.WriteMessage(LogLevel.Error, String.Format("Config file: {0} not found.", cbArgs.configFile)); + return (1); + } + + try + { + ChmBuilder chmBuilder = new ChmBuilder(cbArgs); + chmBuilder.Run(); + } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + return (1); + } + return 0; + } + + //there are some special characters in hxs html, just convert them to what we want + public static string ReplaceMarks(string input) + { + string ret = input.Replace("%3C", "<"); + ret = ret.Replace("%3E", ">"); + ret = ret.Replace("%2C", ","); + return ret; + } + + /// <summary> + /// eg: "c:\tmp\" to "c:\tmp" + /// </summary> + /// <param name="dir"></param> + /// <returns></returns> + public static string StripEndBackSlash(string dir) + { + if (dir.EndsWith("\\")) + return dir.Substring(0, dir.Length - 1); + else + return dir; + } + + public void Run() + { + WriteHtmls(); + WriteHhk(); + if (_hasToc) WriteHhc(); + WriteHhp(); + } + + private static int CompareKeyword(KKeywordInfo x, KKeywordInfo y) + { + if (x.MainEntry != y.MainEntry) + return (x.MainEntry.CompareTo(y.MainEntry)); + else + { + string s1 = x.SubEntry; + string s2 = y.SubEntry; + if (s1 == null) + s1 = string.Empty; + if (s2 == null) + s2 = string.Empty; + return (s1.CompareTo(s2)); + } + } + + /// <summary> + /// read chmTitle from chmBuilder.config + /// </summary> + /// <returns></returns> + private string GetChmTitle() + { + + XPathNodeIterator iter = _config.CreateNavigator().Select("/configuration/chmTitles/title"); + while (iter.MoveNext()) + { + if (iter.Current.GetAttribute("projectName", string.Empty).ToLower() == _args.projectName.ToLower()) + return iter.Current.Value; + } + + //if no title found, set title to projectname + return _args.projectName; + } + + /// <summary> + /// + /// </summary> + private void InsertSeealsoIndice() + { + kkwdTable.Sort(CompareKeyword); + string lastMainEntry = string.Empty; + for (int i = 0; i < kkwdTable.Count; i++) + { + if (!string.IsNullOrEmpty(kkwdTable[i].SubEntry)) + { + if (i > 0) + lastMainEntry = kkwdTable[i - 1].MainEntry; + if (lastMainEntry != kkwdTable[i].MainEntry) + { + KKeywordInfo seealso = new KKeywordInfo(); + seealso.MainEntry = kkwdTable[i].MainEntry; + kkwdTable.Insert(i, seealso); + } + } + } + } + + /// <summary> + /// load language info from config file + /// </summary> + /// <param name="lcid"></param> + private void LoadLanginfo(int lcid) + { + XPathNavigator node = _config.CreateNavigator().SelectSingleNode(String.Format("/configuration/languages/language[@id='{0}']", lcid.ToString())); + if (node != null) + { + _lang = new LangInfo(); + _lang.ID = lcid; + _lang.CodePage = Convert.ToInt32(node.GetAttribute("codepage", string.Empty)); + _lang.Name = node.GetAttribute("name", string.Empty); + } + else + { + throw new ArgumentException(String.Format("language {0} is not found in config file.", lcid)); + } + } + + private void WriteHhc() + { + XmlReaderSettings settings = new XmlReaderSettings(); + settings.ConformanceLevel = ConformanceLevel.Fragment; + settings.IgnoreWhitespace = true; + settings.IgnoreComments = true; + XmlReader reader = XmlReader.Create(_args.tocFile, settings); + + //<param name="Local" value="Html\15ed547b-455d-808c-259e-1eaa3c86dccc.htm"> + //"html" before GUID + string _localFilePrefix = _args.htmlDirectory.Substring(_args.htmlDirectory.LastIndexOf('\\') + 1); + + string fileAttr; + string titleValue; + using (StreamWriter sw = new StreamWriter(String.Format("{0}\\{1}.hhc", _args.outputDirectory, _args.projectName), false, Encoding.GetEncoding(_lang.CodePage))) + { + sw.WriteLine("<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML/EN\">"); + sw.WriteLine("<HTML>"); + sw.WriteLine(" <BODY>"); + + bool bDefaultTopic = true; + while (reader.Read()) + { + switch (reader.NodeType) + { + case XmlNodeType.Element: + if (reader.Name == "topic") + { + _indentCount = reader.Depth; + fileAttr = reader.GetAttribute("file") + ".htm"; + if (titleTable.Contains(fileAttr)) + titleValue = (string)titleTable[fileAttr]; + else + titleValue = String.Empty; + + WriteHhcLine(sw, "<UL>"); + WriteHhcLine(sw, " <LI><OBJECT type=\"text/sitemap\">"); + WriteHhcLine(sw, String.Format(" <param name=\"Name\" value=\"{0}\">", titleValue)); + WriteHhcLine(sw, String.Format(" <param name=\"Local\" value=\"{0}\\{1}\">", _localFilePrefix, fileAttr)); + if (bDefaultTopic) + { + _defaultTopic = _localFilePrefix + "\\" + reader.GetAttribute("file") + ".htm"; + bDefaultTopic = false; + } + WriteHhcLine(sw, " </OBJECT></LI>"); + if (reader.IsEmptyElement) + { + WriteHhcLine(sw, "</UL>"); + } + } + break; + + case XmlNodeType.EndElement: + if (reader.Name == "topic") + { + _indentCount = reader.Depth; + WriteHhcLine(sw, "</UL>"); + } + break; + + default: + //Console.WriteLine(reader.Name); + break; + } + } + sw.WriteLine(" </BODY>"); + sw.WriteLine("</HTML>"); + } + } + + private void WriteHhcLine(TextWriter writer, string value) + { + //write correct indent space + writer.WriteLine(); + for (int i = 0; i < _indentCount - 1; i++) + writer.Write(" "); + writer.Write(value); + } + + private void WriteHhk() + { + int iPrefix = _args.outputDirectory.Length + 1; + bool isIndent = false; + + + InsertSeealsoIndice(); + using (StreamWriter sw = new StreamWriter(String.Format("{0}\\{1}.hhk", _args.outputDirectory, _args.projectName), false, Encoding.GetEncoding(_lang.CodePage))) + { + sw.WriteLine("<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML/EN\">"); + sw.WriteLine("<HTML>"); + sw.WriteLine(" <BODY>"); + sw.WriteLine(" <UL>"); + + foreach (KKeywordInfo ki in kkwdTable) + { + if (!string.IsNullOrEmpty(ki.MainEntry)) + { + string kwdValue = ki.MainEntry; + if (!string.IsNullOrEmpty(ki.SubEntry)) + { + if (!isIndent) + { + isIndent = true; + sw.WriteLine(" <UL>"); + } + kwdValue = ki.SubEntry; + } + else + { + if (isIndent) + { + isIndent = false; + sw.WriteLine(" </UL>"); + } + } + + sw.WriteLine(" <LI><OBJECT type=\"text/sitemap\">"); + sw.WriteLine(String.Format(" <param name=\"Name\" value=\"{0}\">", kwdValue)); + if (String.IsNullOrEmpty(ki.File)) + sw.WriteLine(String.Format(" <param name=\"See Also\" value=\"{0}\">", kwdValue)); + else + sw.WriteLine(String.Format(" <param name=\"Local\" value=\"{0}\">", ki.File.Substring(iPrefix))); + sw.WriteLine(" </OBJECT><LI>"); + } + } + + sw.WriteLine(" </UL>"); + sw.WriteLine(" </BODY>"); + sw.WriteLine("</HTML>"); + } + } + + + /// <summary> + /// In hhp.template, {0} is projectName, {1} is defalutTopic, {2}:Language, {3}:Title + /// </summary> + private void WriteHhp() + { + string hhpFile = String.Format("{0}\\{1}.hhp", _args.outputDirectory, _args.projectName); + Encoding ei = Encoding.GetEncoding(_lang.CodePage); + + using (FileStream writer = File.OpenWrite(hhpFile)) + { + string var0 = _args.projectName; + string var1 = _defaultTopic; + string var2 = _lang.Name; + string var3 = GetChmTitle(); + + XPathNodeIterator iter = _config.CreateNavigator().Select("/configuration/hhpTemplate/line"); + + while (iter.MoveNext()) + { + String line = iter.Current.Value; + AddText(writer, String.Format(line, var0, var1, var2, var3), ei); + AddText(writer, "\r\n", ei); + } + } + } + + private void AddText(FileStream fs, string value, Encoding ei) + { + byte[] info = ei.GetBytes(value); + fs.Write(info, 0, info.Length); + } + + private void WriteHtmls() + { + string _outhtmldir = _args.outputDirectory + _args.htmlDirectory.Substring(_args.htmlDirectory.LastIndexOf('\\')); + HxsChmConverter converter = new HxsChmConverter(_args.htmlDirectory, _outhtmldir, titleTable, kkwdTable, _args.metadata); + converter.Process(); + } + } + + /// <summary> + /// Convert hxs-ready html page to chm-ready page + /// 1. strip of xmlisland; + /// 2. <mshelp:link> link tiltle </link> ==> <span class="nolink">link title</span> + /// </summary> + internal class HxsChmConverter + { + private string _currentFile; + private string _currentTitle; + private string _htmlDir; + List < KKeywordInfo > _kkeywords; + private bool _metadata; + private string _outputDir; + + Hashtable _titles; + + private int _topicCount = 0; + + public HxsChmConverter(string htmlDir, string outputDir, Hashtable titles, List < KKeywordInfo > kkeywords, bool metadata) + { + _htmlDir = htmlDir; + _outputDir = outputDir; + _titles = titles; + _kkeywords = kkeywords; + _metadata = metadata; + } + + public void Process() + { + _topicCount = 0; + ProcessDirectory(_htmlDir, _outputDir); + Console.WriteLine("Processed {0} files.", _topicCount); + } + + private void ProcessDirectory(string srcDir, string destDir) + { + if (!Directory.Exists(destDir)) + { + Directory.CreateDirectory(destDir); + } + + string[] fileEntries = Directory.GetFiles(srcDir); + foreach (string fileName in fileEntries) + { + string destFile = destDir + fileName.Substring(fileName.LastIndexOf('\\')); + + FileInfo fi = new FileInfo(fileName); + string extion = fi.Extension.ToLower(); + //process .htm and .html files, just copy other files, like css, gif. TFS DCR 318537 + if (extion == ".htm" || extion == ".html") + { + try + { + ProcessFile(fileName, destFile); + } + /* + catch (XmlException ex) + { + ConsoleApplication.WriteMessage(LogLevel.Error, String.Format("Invalid XML file {0}", fileName)); + ConsoleApplication.WriteMessage(LogLevel.Error, ex.Message); + _stop = true; + return; + } + */ + catch (Exception) + { + ConsoleApplication.WriteMessage(LogLevel.Error, String.Format("failed to process file {0}", fileName)); + throw; + } + } + else + File.Copy(fileName, destFile, true); + } + + // Recurse into subdirectories of this directory. + string[] subdirectoryEntries = Directory.GetDirectories(srcDir, "*", SearchOption.TopDirectoryOnly); + foreach (string subdirectory in subdirectoryEntries) + { + DirectoryInfo di = new DirectoryInfo(subdirectory); + string newSubdir = destDir + "\\" + di.Name; + ProcessDirectory(subdirectory, newSubdir); + } + } + + private void ProcessFile(string srcFile, string destFile) + { + //Console.WriteLine("Processing:{0}",srcFile); + + XmlReaderSettings settings = new XmlReaderSettings(); + settings.ConformanceLevel = ConformanceLevel.Fragment; + settings.IgnoreWhitespace = false; + settings.IgnoreComments = true; + XmlReader reader = XmlReader.Create(srcFile, settings); + + XmlWriterSettings settings2 = new XmlWriterSettings(); + settings2.Indent = false; + settings2.IndentChars = "\t"; + settings2.OmitXmlDeclaration = true; + XmlWriter writer = XmlWriter.Create(destFile, settings2); + + _currentTitle = String.Empty; + _currentFile = destFile; + + _topicCount++; + + while (reader.Read()) + { + if (_metadata == false && reader.Name.ToLower() == "xml" && reader.NodeType == XmlNodeType.Element) + { + //skip xml data island + reader.ReadOuterXml(); + } + + switch (reader.NodeType) + { + + case XmlNodeType.Element: + string elementName = reader.Name.ToLower(); + + //skip <mshelp:link> node, + if (elementName == "mshelp:link") + { + writer.WriteStartElement("span"); + writer.WriteAttributeString("class", "nolink"); + reader.MoveToContent(); + } + + else + { + if (!String.IsNullOrEmpty(reader.Prefix)) + writer.WriteStartElement(reader.Prefix, reader.LocalName, null); + else + writer.WriteStartElement(reader.Name); + + if (reader.HasAttributes) + { + while (reader.MoveToNextAttribute()) + { + if (!String.IsNullOrEmpty(reader.Prefix)) + writer.WriteAttributeString(reader.Prefix, reader.LocalName, null, reader.Value); + else + //If we write the following content to output file, we will get xmlexception saying the 2003/5 namespace is redefined. So hard code to skip "xmlns". + //<pre>My.Computer.FileSystem.RenameFile(<span class="literal" xmlns="http://ddue.schemas.microsoft.com/authoring/2003/5"> + if (!(reader.Depth > 2 && reader.Name.StartsWith("xmlns"))) + writer.WriteAttributeString(reader.Name, reader.Value); + } + // Move the reader back to the element node. + reader.MoveToElement(); + } + + //read html/head/title, save it to _currentTitle + if (reader.Depth == 2 && elementName == "title") + { + if (!reader.IsEmptyElement) //skip <Title/> node, fix bug 425406 + { + reader.Read(); + if (reader.NodeType == XmlNodeType.Text) + { + _currentTitle = reader.Value; + writer.WriteRaw(reader.Value); + } + } + } + + if (reader.IsEmptyElement) + writer.WriteEndElement(); + } + break; + + case XmlNodeType.Text: + writer.WriteValue(reader.Value); + break; + + case XmlNodeType.EndElement: + writer.WriteFullEndElement(); + break; + + case XmlNodeType.Whitespace: + case XmlNodeType.SignificantWhitespace: + writer.WriteWhitespace(reader.Value); + break; + + + default: + //Console.WriteLine(reader.Name); + break; + } + } + + writer.Close(); + + ReadXmlIsland(srcFile); + + _titles.Add(destFile.Substring(destFile.LastIndexOf("\\") + 1), _currentTitle); + } + + + /// <summary> + /// As XmlReader is forward only and we added support for leaving xmlisland data. + /// We have to use another xmlreader to find TocTile, keywords etc. + /// </summary> + /// <param name="filename"></param> + private void ReadXmlIsland(string filename) + { + XmlReaderSettings settings = new XmlReaderSettings(); + settings.ConformanceLevel = ConformanceLevel.Fragment; + settings.IgnoreWhitespace = false; + settings.IgnoreComments = true; + XmlReader reader = XmlReader.Create(filename, settings); + + //Fix TFS bug 289403: search if there is comma in k keyword except those in () or <>. + //sample1: "StoredNumber (T1,T2) class, about StoredNumber (T1,T2) class"; + //sample2: "StoredNumber <T1,T2> class, about StoredNumber <T1,T2> class"; + Regex r = new Regex(@",([^\)\>]+|([^\<\>]*\<[^\<\>]*\>[^\<\>]*)?|([^\(\)]*\([^\(\)]*\)[^\(\)]*)?)$"); + + while (reader.Read()) + { + if (reader.IsStartElement()) + { + if (reader.Name.ToLower() == "mshelp:toctitle") + { + string titleAttr = reader.GetAttribute("Title"); + if (!String.IsNullOrEmpty(titleAttr)) + _currentTitle = titleAttr; + } + + if (reader.Name.ToLower() == "mshelp:keyword") + { + string indexType = reader.GetAttribute("Index"); + if (indexType == "K") + { + KKeywordInfo kkwdinfo = new KKeywordInfo(); + string kkeyword = reader.GetAttribute("Term"); + if (!string.IsNullOrEmpty(kkeyword)) + { + kkeyword = ChmBuilder.ReplaceMarks(kkeyword); + Match match = r.Match(kkeyword); + if (match.Success) + { + kkwdinfo.MainEntry = kkeyword.Substring(0, match.Index); + kkwdinfo.SubEntry = kkeyword.Substring(match.Index + 1).TrimStart(new char[] { ' ' }); + } + else + { + kkwdinfo.MainEntry = kkeyword; + } + + kkwdinfo.File = _currentFile; + _kkeywords.Add(kkwdinfo); + } + } + } + } + + if (reader.NodeType == XmlNodeType.EndElement) + { + if (reader.Name == "xml") + return; + } + } + } + } +} |