15 | {
16 |
17 | }
18 |
19 | internal static class KeywordListSerializer
20 | {
21 | public static KeywordList Deserialize(byte[] buffer)
22 | {
23 | if (buffer == null)
24 | throw new ArgumentNullException(nameof(buffer));
25 |
26 | // Serialized sequentially as length-prefixed string.
27 | KeywordList keywords = new KeywordList();
28 | using (BinaryReader reader = new BinaryReader(new MemoryStream(buffer)))
29 | {
30 | while (reader.BaseStream.Position < reader.BaseStream.Length)
31 | {
32 | byte length = reader.ReadByte();
33 | byte[] keyword = reader.ReadBytes(length);
34 | keywords.Add(keyword);
35 | }
36 | }
37 | return keywords;
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/QuickHelp/Compression/StreamExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 |
4 | namespace QuickHelp.Compression
5 | {
6 | static class StreamExtensions
7 | {
8 | public static int ReadBytes(
9 | Stream stream, byte[] buffer, int offset, int count)
10 | {
11 | if (stream == null)
12 | throw new ArgumentNullException(nameof(stream));
13 | if (buffer == null)
14 | throw new ArgumentNullException(nameof(buffer));
15 | if (offset < 0 || offset > buffer.Length)
16 | throw new ArgumentOutOfRangeException(nameof(offset));
17 | if (count < 0 || count > buffer.Length - offset)
18 | throw new ArgumentOutOfRangeException(nameof(count));
19 |
20 | for (int i = 0; i < count; i++)
21 | {
22 | int value = stream.ReadByte();
23 | if (value < 0)
24 | return i;
25 | buffer[offset + i] = (byte)value;
26 | }
27 | return count;
28 | }
29 |
30 | public static void WriteBytes(
31 | Stream stream, byte[] buffer, int offset, int count)
32 | {
33 | if (stream == null)
34 | throw new ArgumentNullException(nameof(stream));
35 | if (buffer == null)
36 | throw new ArgumentNullException(nameof(buffer));
37 | if (offset < 0 || offset > buffer.Length)
38 | throw new ArgumentOutOfRangeException(nameof(offset));
39 | if (count < 0 || count > buffer.Length - offset)
40 | throw new ArgumentOutOfRangeException(nameof(count));
41 |
42 | for (int i = 0; i < count; i++)
43 | {
44 | stream.WriteByte(buffer[offset + i]);
45 | }
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/QuickHelp/Formatters/Default.css:
--------------------------------------------------------------------------------
1 | .help-content {
2 | font-family: Consolas;
3 | font-size: 12pt;
4 | }
5 |
6 | .help-content a:link {
7 | color: blue;
8 | text-decoration: none;
9 | }
10 |
11 | .help-content a:visited {
12 | color: blue;
13 | }
14 |
15 | .help-content a:hover {
16 | color: blue;
17 | text-decoration: underline;
18 | }
19 |
20 | .help-content a:active {
21 | color: blue;
22 | }
--------------------------------------------------------------------------------
/QuickHelp/Formatters/EmbeddedHtmlFormatter.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Reflection;
3 |
4 | namespace QuickHelp.Formatters
5 | {
6 | ///
7 | /// Provides methods to format a help topic as HTML suitable for embedding
8 | /// in a WebBrowser control.
9 | ///
10 | public class EmbeddedHtmlFormatter : HtmlFormatter
11 | {
12 | private static string s_styleSheet = LoadStyleSheet();
13 |
14 | private static string LoadStyleSheet()
15 | {
16 | Assembly assembly = Assembly.GetExecutingAssembly();
17 | string resourceName = "QuickHelp.Formatters.Default.css";
18 | using (Stream stream = assembly.GetManifestResourceStream(resourceName))
19 | using (StreamReader reader = new StreamReader(stream))
20 | {
21 | return reader.ReadToEnd();
22 | }
23 | }
24 |
25 | protected override string FormatUri(HelpTopic topic, HelpUri uri)
26 | {
27 | return "?" + Escape(uri.ToString());
28 | }
29 |
30 | protected override string GetStyleSheet()
31 | {
32 | return string.Format(" \n", s_styleSheet);
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/QuickHelp/Formatters/HtmlFormatter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace QuickHelp.Formatters
6 | {
7 | ///
8 | /// Abstract class that provides methods to format a help topic as HTML.
9 | ///
10 | public abstract class HtmlFormatter
11 | {
12 | ///
13 | /// Gets or sets a flag that controls whether to fix links in the
14 | /// output.
15 | ///
16 | ///
17 | /// If this property is set to true, the renderer excludes
18 | /// the enclosing pair of ◄ and ► from the link.
19 | ///
20 | public bool FixLinks { get; set; }
21 |
22 | ///
23 | /// Formats the given help topic as HTML and returns the HTML source.
24 | ///
25 | public string FormatTopic(HelpTopic topic)
26 | {
27 | if (topic == null)
28 | throw new ArgumentNullException(nameof(topic));
29 |
30 | StringBuilder html = new StringBuilder();
31 | html.AppendLine("");
32 | html.AppendLine(" ");
33 | html.AppendLine(string.Format(" {0}", Escape(topic.Title)));
34 | html.AppendLine(" ");
35 | html.Append(GetStyleSheet());
36 | html.AppendLine(" ");
37 | html.AppendLine(" ");
38 |
39 | html.Append(" ");
40 | for (int i = 0; i < topic.Lines.Count; i++)
41 | {
42 | FormatLine(html, topic, topic.Lines[i]);
43 | if (i < topic.Lines.Count - 1)
44 | html.AppendLine();
45 | }
46 | html.AppendLine("
");
47 |
48 | html.AppendLine(" ");
49 | html.AppendLine("");
50 | return html.ToString();
51 | }
52 |
53 | ///
54 | /// Formats a help line as HTML and returns the HTML source.
55 | ///
56 | ///
57 | /// This method produces properly structured HTML. That is, it avoids
58 | /// markup such as
59 | ///
60 | /// ...............
61 | ///
62 | /// The generated HTML is not the most compact possible, but is quite
63 | /// compact in practice.
64 | ///
65 | /// For a formal discussion about unpaired tags, see
66 | /// http://www.w3.org/html/wg/drafts/html/master/syntax.html#an-introduction-to-error-handling-and-strange-cases-in-the-parser
67 | ///
68 | private void FormatLine(StringBuilder html, HelpTopic topic, HelpLine line)
69 | {
70 | if (this.FixLinks)
71 | {
72 | line = FixLine(line);
73 | }
74 | for (int index = 0; index < line.Length;)
75 | {
76 | index = FormatLineSegment(html, topic, line, index);
77 | }
78 | }
79 |
80 | private static HelpLine FixLine(HelpLine line)
81 | {
82 | TextAttribute[] attributes = new TextAttribute[line.Length];
83 | for (int i = 0; i < line.Length; i++)
84 | {
85 | if (line.Text[i] == '►' &&
86 | line.Attributes[i].Link != null &&
87 | (i == line.Length - 1 || line.Attributes[i + 1].Link == null))
88 | {
89 | attributes[i] = new TextAttribute(line.Attributes[i].Style, null);
90 | }
91 | else
92 | {
93 | attributes[i] = line.Attributes[i];
94 | }
95 | }
96 | return new HelpLine(line.Text, attributes);
97 | }
98 |
99 | private int FormatLineSegment(StringBuilder html, HelpTopic topic, HelpLine line, int startIndex)
100 | {
101 | HelpUri link = line.Attributes[startIndex].Link;
102 | if (link != null)
103 | {
104 | html.AppendFormat("", FormatUri(topic, link));
105 | }
106 |
107 | Stack openTags = new Stack();
108 | int index = startIndex;
109 | while (index < line.Length && line.Attributes[index].Link == link)
110 | {
111 | TextAttribute oldAttrs = (index == startIndex) ?
112 | TextAttribute.Default : line.Attributes[index - 1];
113 | TextAttribute newAttrs = line.Attributes[index];
114 | TextStyle stylesToAdd = newAttrs.Style & ~oldAttrs.Style;
115 | TextStyle stylesToRemove = oldAttrs.Style & ~newAttrs.Style;
116 |
117 | while (stylesToRemove != TextStyle.None)
118 | {
119 | TextStyle top = openTags.Pop();
120 | FormatRemovedStyles(html, top);
121 | if ((stylesToRemove & top) != 0)
122 | {
123 | stylesToRemove &= ~top;
124 | }
125 | else
126 | {
127 | stylesToAdd |= top;
128 | }
129 | }
130 |
131 | if ((stylesToAdd & TextStyle.Bold) != 0)
132 | {
133 | html.Append("");
134 | openTags.Push(TextStyle.Bold);
135 | }
136 | if ((stylesToAdd & TextStyle.Italic) != 0)
137 | {
138 | html.Append("");
139 | openTags.Push(TextStyle.Italic);
140 | }
141 | if ((stylesToAdd & TextStyle.Underline) != 0)
142 | {
143 | html.Append("");
144 | openTags.Push(TextStyle.Underline);
145 | }
146 |
147 | html.Append(Escape("" + line.Text[index]));
148 | index++;
149 | }
150 |
151 | while (openTags.Count > 0)
152 | {
153 | FormatRemovedStyles(html, openTags.Pop());
154 | }
155 | if (link != null)
156 | {
157 | html.Append("");
158 | }
159 |
160 | return index;
161 | }
162 |
163 | ///
164 | /// Formats the given help uri for the HTML href attribute.
165 | ///
166 | protected abstract string FormatUri(HelpTopic topic, HelpUri uri);
167 |
168 | private static void FormatRemovedStyles(
169 | StringBuilder html, TextStyle change)
170 | {
171 | if ((change & TextStyle.Bold) != 0)
172 | html.Append("");
173 | if ((change & TextStyle.Italic) != 0)
174 | html.Append("");
175 | if ((change & TextStyle.Underline) != 0)
176 | html.Append("");
177 | }
178 |
179 | ///
180 | /// Gets an HTML fragment to be inserted into the head section of the
181 | /// HTML output. The default implementation returns an empty string.
182 | ///
183 | protected virtual string GetStyleSheet()
184 | {
185 | return "";
186 | }
187 |
188 | public static string Escape(string s)
189 | {
190 | return System.Security.SecurityElement.Escape(s);
191 | }
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/QuickHelp/Formatters/TextFormatter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 |
4 | namespace QuickHelp.Formatters
5 | {
6 | ///
7 | /// Provides methods to format a help topic as plain text.
8 | ///
9 | public class TextFormatter
10 | {
11 | public static string FormatTopic(HelpTopic topic)
12 | {
13 | if (topic == null)
14 | throw new ArgumentNullException(nameof(topic));
15 |
16 | StringBuilder sb = new StringBuilder();
17 | foreach (HelpLine line in topic.Lines)
18 | {
19 | sb.AppendLine(line.Text);
20 | }
21 | return sb.ToString();
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/QuickHelp/HelpDatabase.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Collections.Generic;
4 |
5 | namespace QuickHelp
6 | {
7 | ///
8 | /// Represents a help database that contains a collection of help topics.
9 | ///
10 | ///
11 | /// This class is the logical representation of a help database. The
12 | /// BinaryHelpSerializer class handles the serialization of the
13 | /// help database on disk.
14 | ///
15 | public class HelpDatabase
16 | {
17 | private readonly HelpTopicCollection topics;
18 | private readonly SortedDictionary contextMap;
19 | private readonly bool isCaseSensitive;
20 | private readonly string name;
21 |
22 | ///
23 | /// Creates a help database with the given name. The name is used to
24 | /// resolve external context strings.
25 | ///
26 | public HelpDatabase(string name)
27 | : this(name, false)
28 | {
29 | }
30 |
31 | ///
32 | /// Creates a help database with the given name, and specifies whether
33 | /// context strings are case sensitive.
34 | ///
35 | /// Name of the database
36 | ///
37 | public HelpDatabase(string name, bool isCaseSensitive)
38 | {
39 | if (name == null)
40 | throw new ArgumentNullException(nameof(name));
41 |
42 | StringComparer stringComparer = isCaseSensitive ?
43 | StringComparer.InvariantCulture :
44 | StringComparer.InvariantCultureIgnoreCase;
45 | this.contextMap = new SortedDictionary(stringComparer);
46 | this.isCaseSensitive = isCaseSensitive;
47 | this.name = name;
48 | this.topics = new HelpTopicCollection(this);
49 | }
50 |
51 | ///
52 | /// Gets the name of the help database. This name is used to resolve
53 | /// external context strings. The name is case-insensitive.
54 | ///
55 | public string Name
56 | {
57 | get { return name; }
58 | }
59 |
60 | ///
61 | /// Gets or sets a flag that indicates whether context strings are
62 | /// case-sensitive when resolving links.
63 | ///
64 | public bool IsCaseSensitive
65 | {
66 | get { return isCaseSensitive; }
67 | }
68 |
69 | ///
70 | /// Gets the collection of topics in this database.
71 | ///
72 | public HelpTopicCollection Topics
73 | {
74 | get { return topics; }
75 | }
76 |
77 | ///
78 | /// Gets a list of the context strings defined in this database.
79 | ///
80 | public IEnumerable ContextStrings
81 | {
82 | get { return contextMap.Keys; }
83 | }
84 |
85 | ///
86 | /// Associates a context string with a topic in this database.
87 | /// Multiple context strings may be associated with a single topic.
88 | ///
89 | /// The context string.
90 | /// Zero-based topic index.
91 | /// Whether the context string is treated as case-sensitive
92 | /// is controlled by the IsCaseSensitive
property.
93 | public void AddContext(string contextString, int topicIndex)
94 | {
95 | if (contextString == null)
96 | throw new ArgumentNullException(nameof(contextString));
97 | if (topicIndex < 0)
98 | throw new IndexOutOfRangeException(nameof(topicIndex));
99 |
100 | contextMap[contextString] = topicIndex;
101 | }
102 |
103 | ///
104 | /// Finds the topic associated with the given context string.
105 | ///
106 | /// The context string to resolve.
107 | /// The help topic associated with the given context string,
108 | /// or null if the context string cannot be resolved.
109 | /// Whether the context string is treated as case-sensitive
110 | /// is controlled by the IsCaseSensitive
property.
111 | public HelpTopic ResolveContext(string contextString)
112 | {
113 | if (contextString == null)
114 | throw new ArgumentNullException("contextString");
115 |
116 | int topicIndex;
117 | if (contextMap.TryGetValue(contextString, out topicIndex))
118 | return topics[topicIndex];
119 | else
120 | return null;
121 | }
122 | }
123 |
124 | public class HelpTopicCollection : IList
125 | {
126 | private readonly HelpDatabase m_database;
127 | private readonly List m_topics = new List();
128 |
129 | internal HelpTopicCollection(HelpDatabase database)
130 | {
131 | if (database == null)
132 | throw new ArgumentNullException(nameof(database));
133 | m_database = database;
134 | }
135 |
136 | private void Attach(HelpTopic topic)
137 | {
138 | if (topic != null)
139 | {
140 | if (topic.Database != null)
141 | {
142 | throw new InvalidOperationException("Topic is already part of a database.");
143 | }
144 | topic.Database = m_database;
145 | }
146 | }
147 |
148 | private void Detach(HelpTopic topic)
149 | {
150 | if (topic != null)
151 | {
152 | if (topic.Database != m_database)
153 | {
154 | throw new InvalidOperationException("Topic to detach is not part of this database.");
155 | }
156 | topic.Database = null;
157 | }
158 | }
159 |
160 | public HelpTopic this[int index]
161 | {
162 | get { return m_topics[index]; }
163 | set
164 | {
165 | if (m_topics[index] != value)
166 | {
167 | Attach(value);
168 | Detach(m_topics[index]);
169 | m_topics[index] = value;
170 | }
171 | }
172 | }
173 |
174 | public int Count
175 | {
176 | get { return m_topics.Count; }
177 | }
178 |
179 | public bool IsReadOnly
180 | {
181 | get { return false; }
182 | }
183 |
184 | public void Add(HelpTopic topic)
185 | {
186 | Attach(topic);
187 | m_topics.Add(topic);
188 | }
189 |
190 | public void Clear()
191 | {
192 | foreach (HelpTopic topic in m_topics)
193 | {
194 | Detach(topic);
195 | }
196 | m_topics.Clear();
197 | }
198 |
199 | public bool Contains(HelpTopic topic)
200 | {
201 | return m_topics.Contains(topic);
202 | }
203 |
204 | public void CopyTo(HelpTopic[] array, int arrayIndex)
205 | {
206 | m_topics.CopyTo(array, arrayIndex);
207 | }
208 |
209 | public IEnumerator GetEnumerator()
210 | {
211 | return m_topics.GetEnumerator();
212 | }
213 |
214 | public int IndexOf(HelpTopic topic)
215 | {
216 | return m_topics.IndexOf(topic);
217 | }
218 |
219 | public void Insert(int index, HelpTopic topic)
220 | {
221 | Attach(topic);
222 | m_topics.Insert(index, topic);
223 | }
224 |
225 | public bool Remove(HelpTopic topic)
226 | {
227 | if (m_topics.Remove(topic))
228 | {
229 | Detach(topic);
230 | return true;
231 | }
232 | return false;
233 | }
234 |
235 | public void RemoveAt(int index)
236 | {
237 | HelpTopic topic = m_topics[index];
238 | m_topics.RemoveAt(index);
239 | Detach(topic);
240 | }
241 |
242 | IEnumerator IEnumerable.GetEnumerator()
243 | {
244 | return ((IEnumerable)m_topics).GetEnumerator();
245 | }
246 | }
247 | }
--------------------------------------------------------------------------------
/QuickHelp/HelpLine.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace QuickHelp
6 | {
7 | ///
8 | /// Represents a line of text in a help topic, along with formatting and
9 | /// hyperlink information.
10 | ///
11 | public class HelpLine
12 | {
13 | readonly string text;
14 | readonly TextAttribute[] attributes;
15 |
16 | public HelpLine(string text, TextAttribute[] attributes)
17 | {
18 | if (text == null)
19 | throw new ArgumentNullException(nameof(text));
20 | if (attributes == null)
21 | throw new ArgumentNullException(nameof(attributes));
22 | if (text.Length != attributes.Length)
23 | throw new ArgumentException("text and attributes must have the same length.");
24 |
25 | this.text = text;
26 | this.attributes = attributes;
27 | }
28 |
29 | public HelpLine(string text)
30 | {
31 | if (text == null)
32 | throw new ArgumentNullException(nameof(text));
33 |
34 | this.text = text;
35 | this.attributes = new TextAttribute[text.Length];
36 | }
37 |
38 | public int Length
39 | {
40 | get { return text.Length; }
41 | }
42 |
43 | ///
44 | /// Gets the text in this line without any formatting information.
45 | /// This is the text displayed on the screen.
46 | ///
47 | public string Text
48 | {
49 | get { return text; }
50 | }
51 |
52 | ///
53 | /// Gets the attribute of each character in the line.
54 | ///
55 | public TextAttribute[] Attributes
56 | {
57 | get { return attributes; }
58 | }
59 |
60 | public IEnumerable Links
61 | {
62 | get
63 | {
64 | HelpUri lastLink = null;
65 | foreach (TextAttribute a in attributes)
66 | {
67 | if (a.Link != lastLink)
68 | {
69 | if (a.Link != null)
70 | yield return a.Link;
71 | lastLink = a.Link;
72 | }
73 | }
74 | }
75 | }
76 |
77 | public override string ToString()
78 | {
79 | return this.text;
80 | }
81 | }
82 |
83 | public struct TextAttribute
84 | {
85 | readonly TextStyle style;
86 | readonly HelpUri link;
87 |
88 | public TextAttribute(TextStyle style, HelpUri link)
89 | {
90 | this.style = style;
91 | this.link = link;
92 | }
93 |
94 | public TextStyle Style
95 | {
96 | get { return style; }
97 | }
98 |
99 | public HelpUri Link
100 | {
101 | get { return link; }
102 | }
103 |
104 | public override string ToString()
105 | {
106 | if (link == null)
107 | return style.ToString();
108 | else
109 | return style.ToString() + "; " + link.ToString();
110 | }
111 |
112 | public static readonly TextAttribute Default = new TextAttribute();
113 | }
114 |
115 | [Flags]
116 | public enum TextStyle
117 | {
118 | None = 0,
119 | Bold = 1,
120 | Italic = 2,
121 | Underline = 4,
122 | }
123 |
124 | public class HelpLineBuilder
125 | {
126 | readonly StringBuilder textBuilder;
127 | readonly List attrBuilder;
128 |
129 | public HelpLineBuilder(int capacity)
130 | {
131 | this.textBuilder = new StringBuilder(capacity);
132 | this.attrBuilder = new List(capacity);
133 | }
134 |
135 | public int Length
136 | {
137 | get { return textBuilder.Length; }
138 | }
139 |
140 | public void Append(string s, int index, int count, TextStyle styles)
141 | {
142 | textBuilder.Append(s, index, count);
143 | for (int i = 0; i < count; i++)
144 | attrBuilder.Add(new TextAttribute(styles, null));
145 | }
146 |
147 | public void Append(char c, TextStyle styles)
148 | {
149 | textBuilder.Append(c);
150 | attrBuilder.Add(new TextAttribute(styles, null));
151 | }
152 |
153 | public void AddLink(int index, int count, HelpUri link)
154 | {
155 | for (int i = index; i < index + count; i++)
156 | {
157 | attrBuilder[i] = new TextAttribute(attrBuilder[i].Style, link);
158 | }
159 | }
160 |
161 | public HelpLine ToLine()
162 | {
163 | return new HelpLine(textBuilder.ToString(), attrBuilder.ToArray());
164 | }
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/QuickHelp/HelpSystem.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace QuickHelp
5 | {
6 | ///
7 | /// Manages multiple cross-referenced help databases.
8 | ///
9 | public class HelpSystem
10 | {
11 | readonly List databases = new List();
12 |
13 | ///
14 | /// Gets the collection of help databases in this library.
15 | ///
16 | public List Databases
17 | {
18 | get { return databases; }
19 | }
20 |
21 | ///
22 | /// Finds a database with the given name, ignoring case.
23 | ///
24 | /// Name of the database to find.
25 | /// The help database, or null if not found.
26 | public HelpDatabase FindDatabase(string name)
27 | {
28 | if (name == null)
29 | throw new ArgumentNullException(nameof(name));
30 |
31 | foreach (HelpDatabase database in databases)
32 | {
33 | if (database.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase))
34 | return database;
35 | }
36 | return null;
37 | }
38 |
39 | ///
40 | /// Resolves the given uri in the current help system.
41 | ///
42 | ///
43 | ///
44 | ///
45 | /// The topic pointed to by the uri, or null if one cannot be
46 | /// found.
47 | ///
48 | public HelpTopic ResolveUri(HelpDatabase referrer, HelpUri uri)
49 | {
50 | if (uri == null)
51 | throw new ArgumentNullException(nameof(uri));
52 |
53 | HelpUriType uriType = uri.Type;
54 | if (uriType == HelpUriType.LocalTopic)
55 | {
56 | if (referrer != null)
57 | {
58 | int topicIndex = uri.TopicIndex;
59 | if (topicIndex >= 0 && topicIndex < referrer.Topics.Count)
60 | return referrer.Topics[topicIndex];
61 | }
62 | }
63 | else if (uriType == HelpUriType.GlobalContext)
64 | {
65 | string dbName = uri.DatabaseName;
66 | HelpDatabase db = FindDatabase(dbName);
67 | if (db != null)
68 | return db.ResolveContext(uri.ContextString);
69 | }
70 | else if (uriType == HelpUriType.LocalContext)
71 | {
72 | if (referrer != null)
73 | return referrer.ResolveContext(uri.ContextString);
74 | }
75 | else if (uriType == HelpUriType.Context)
76 | {
77 | if (referrer != null)
78 | {
79 | HelpTopic topic = referrer.ResolveContext(uri.ContextString);
80 | if (topic != null)
81 | return topic;
82 | }
83 | foreach (HelpDatabase db in databases)
84 | {
85 | HelpTopic topic = db.ResolveContext(uri.ContextString);
86 | if (topic != null)
87 | return topic;
88 | }
89 | return null;
90 | }
91 | return null;
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/QuickHelp/HelpTopic.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace QuickHelp
5 | {
6 | ///
7 | /// Represents a help topic that contains formatted text and links.
8 | ///
9 | public class HelpTopic
10 | {
11 | private readonly List lines = new List();
12 | private readonly List m_snippets = new List();
13 | private readonly List m_references = new List();
14 |
15 | public HelpTopic()
16 | {
17 | }
18 |
19 | ///
20 | /// Gets the database that contains this help topic.
21 | ///
22 | public HelpDatabase Database { get; internal set; }
23 |
24 | ///
25 | /// Gets the zero-based index of this topic within its containing database.
26 | ///
27 | /// TODO: remove this field
28 | public int TopicIndex
29 | {
30 | get
31 | {
32 | if (this.Database != null)
33 | return this.Database.Topics.IndexOf(this);
34 | else
35 | return -1;
36 | }
37 | }
38 |
39 | ///
40 | /// Gets or sets the title of the topic.
41 | ///
42 | public string Title { get; set; }
43 |
44 | ///
45 | /// Gets or sets the default window height in number of lines.
46 | ///
47 | public int WindowHeight { get; set; }
48 |
49 | ///
50 | /// Gets or sets the number of rows to freeze at the top of the help
51 | /// screen.
52 | ///
53 | public int FreezeHeight { get; set; }
54 |
55 | #if true
56 | ///
57 | /// Gets or sets the source of this topic. If the topic is read from
58 | /// a text file, the object is a string; if the topic is read from a
59 | /// binary file, the object is a byte[].
60 | ///
61 | public object Source { get; set; }
62 | #endif
63 |
64 | ///
65 | /// Gets the collection of lines in this topic.
66 | ///
67 | public List Lines
68 | {
69 | get { return lines; }
70 | }
71 |
72 | public override string ToString()
73 | {
74 | if (this.Title == null)
75 | return "(Untitled Topic)";
76 | else
77 | return this.Title;
78 | }
79 |
80 | ///
81 | /// Gets or sets a flag that indicates whether the topic should be
82 | /// treated as a list of topics.
83 | ///
84 | ///
85 | /// If this value is true, each line in the topic is treated
86 | /// as a list item and is supposed to point to a topic. If the line
87 | /// contains a link, that link points to the target. Otherwise, the
88 | /// first string terminated by two spaces or a newline character is
89 | /// treated as a context string that points to the target. If no such
90 | /// string exists, the first word is treated as a context string.
91 | ///
92 | public bool IsList { get; set; }
93 |
94 | ///
95 | /// Gets or sets a flag that instructs the help viewer to turn off
96 | /// special processing of certain characters.
97 | ///
98 | public bool IsRaw { get; set; }
99 |
100 | ///
101 | /// Gets or sets a flag that indicates the topic should not be
102 | /// displayed in the help viewer because it contains commands or
103 | /// internal information.
104 | ///
105 | public bool IsHidden { get; set; }
106 |
107 | ///
108 | /// Gets or sets the command to execute.
109 | ///
110 | public string ExecuteCommand { get; set; }
111 |
112 | ///
113 | /// Gets or sets the category of the topic. May be null if the
114 | /// topic is not assigned to any category.
115 | ///
116 | public string Category { get; set; }
117 |
118 | ///
119 | /// Gets or sets a flag that instructs the help viewer to display the
120 | /// topic in a popup window instead of in a regular, scrollable
121 | /// window.
122 | ///
123 | public bool IsPopup { get; set; }
124 |
125 | ///
126 | /// Gets the collection of help snippets in this topic.
127 | ///
128 | public List Snippets
129 | {
130 | get { return m_snippets; }
131 | }
132 |
133 | ///
134 | /// Gets or sets the previous topic in navigation.
135 | ///
136 | ///
137 | /// This is used by QuickHelp to skip large number of hidden or popup
138 | /// topics. If not specified, the previous topic in the sequence is
139 | /// used.
140 | ///
141 | public HelpUri Predecessor { get; set; }
142 |
143 | ///
144 | /// Gets or sets the next topic in navigation.
145 | ///
146 | ///
147 | /// This is used by QuickHelp to skip large number of hidden or popup
148 | /// topics. If not specified, the next topic in the sequence is used.
149 | ///
150 | public HelpUri Successor { get; set; }
151 |
152 | ///
153 | /// Gets the collection of references for the topic. Each reference
154 | /// is represented by a context string.
155 | ///
156 | public List References
157 | {
158 | get { return m_references; }
159 | }
160 | }
161 |
162 | ///
163 | /// Represents a range of lines in a help topic with a name.
164 | ///
165 | ///
166 | /// A snippet is called a "paste" in QuickHelp terms. It is typically
167 | /// used to point to a section of code that can be pasted.
168 | ///
169 | public class HelpSnippet
170 | {
171 | ///
172 | /// Gets or sets the name of the snippet.
173 | ///
174 | public string Name { get; set; }
175 |
176 | ///
177 | /// Gets or sets the zero-based line number of the first line of the
178 | /// snippet.
179 | ///
180 | public int StartLine { get; set; }
181 |
182 | ///
183 | /// Gets or sets the zero-based line number of the line just past the
184 | /// end of the snippet.
185 | ///
186 | public int EndLine { get; set; }
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/QuickHelp/HelpUri.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 |
4 | namespace QuickHelp
5 | {
6 | ///
7 | /// Refers to a location within a help system.
8 | ///
9 | ///
10 | /// A help uri may take one of the following formats, and is resolved in
11 | /// that order:
12 | ///
13 | /// @LXXXX -- where XXXX is a hexidecimal number with the higest bit set
14 | /// Display the topic with index (XXXX & 0x7FFF) in the local
15 | /// database.
16 | ///
17 | /// @contextstring
18 | /// Display the topic associated with "@contextstring". Only the
19 | /// local database is searched for the context.
20 | ///
21 | /// !command
22 | /// Execute the command specified after the exclamation point (!).
23 | /// The command is case sensitive. Commands are application-specific.
24 | ///
25 | /// filename!
26 | /// Display filename as a single topic. The specified file must be a
27 | /// text file no larger than 64K.
28 | ///
29 | /// helpfile!contextstring
30 | /// Search helpfile for contextstring and display the associated
31 | /// topic. Only the specified Help database or physical Help file is
32 | /// searched for the context.
33 | ///
34 | /// contextstring
35 | /// Display the topic associated with contextstring. The context
36 | /// string is first searched for in the local database; if it is
37 | /// not found, it is searched for in other databases in the help
38 | /// system, and the first match is returned.
39 | ///
40 | public class HelpUri
41 | {
42 | readonly string target;
43 |
44 | ///
45 | /// Creates a uri that points to a topic in the local database. This
46 | /// is called "local context" in QuickHelp terms.
47 | ///
48 | /// Zero-based topic index.
49 | ///
50 | /// If topic index is less than 0 or greater than or equal to 0x8000.
51 | ///
52 | public HelpUri(int topicIndex)
53 | {
54 | if (topicIndex < 0 || topicIndex >= 0x8000)
55 | throw new ArgumentOutOfRangeException(nameof(topicIndex));
56 |
57 | this.target = string.Format("@L{0:X4}", topicIndex | 0x8000);
58 | }
59 |
60 | ///
61 | /// Creates a uri directly.
62 | ///
63 | public HelpUri(string target)
64 | {
65 | if (target == null)
66 | throw new ArgumentNullException(nameof(target));
67 |
68 | this.target = target;
69 | }
70 |
71 | ///
72 | /// Gets the type of location this uri refers to.
73 | ///
74 | public HelpUriType Type
75 | {
76 | get
77 | {
78 | if (target == "")
79 | return HelpUriType.None;
80 |
81 | if (target.StartsWith("@"))
82 | {
83 | if (TopicIndex >= 0)
84 | return HelpUriType.LocalTopic;
85 | else
86 | return HelpUriType.LocalContext;
87 | }
88 |
89 | if (target.StartsWith("!"))
90 | return HelpUriType.Command;
91 | else if (target.EndsWith("!"))
92 | return HelpUriType.File;
93 | else if (target.Contains("!"))
94 | return HelpUriType.GlobalContext;
95 | else
96 | return HelpUriType.Context;
97 | }
98 | }
99 |
100 | ///
101 | /// Gets the topic index specified by this uri, or -1 if this uri
102 | /// does not specify a topic index.
103 | ///
104 | public int TopicIndex
105 | {
106 | get
107 | {
108 | if (target.Length == 6 && target.StartsWith("@L"))
109 | {
110 | int topicIndexOr8000;
111 | if (Int32.TryParse(
112 | target.Substring(2),
113 | NumberStyles.AllowHexSpecifier,
114 | CultureInfo.InvariantCulture,
115 | out topicIndexOr8000) &&
116 | (topicIndexOr8000 & 0x8000) != 0)
117 | {
118 | return topicIndexOr8000 & 0x7FFF;
119 | }
120 | }
121 | return -1;
122 | }
123 | }
124 |
125 | ///
126 | /// Gets the database name component of the url.
127 | ///
128 | ///
129 | /// The text to the left of the first '!' in the uri, or null
130 | /// if the uri does not contain any '!'.
131 | ///
132 | public string DatabaseName
133 | {
134 | get
135 | {
136 | int k = target.IndexOf('!');
137 | if (k <= 0)
138 | return null;
139 | else
140 | return target.Substring(0, k);
141 | }
142 | }
143 |
144 | ///
145 | /// Gets the context string component of the url.
146 | ///
147 | ///
148 | /// The text to the right of the first '!', or the entire target if
149 | /// the uri does not contain any '!'.
150 | ///
151 | public string ContextString
152 | {
153 | get
154 | {
155 | int k = target.IndexOf('!');
156 | if (k < 0)
157 | return target;
158 | else
159 | return target.Substring(k + 1);
160 | }
161 | }
162 |
163 | public string Target
164 | {
165 | get { return target; }
166 | }
167 |
168 | public override string ToString()
169 | {
170 | return target;
171 | }
172 | }
173 |
174 | ///
175 | /// Specifies the type of a help uri.
176 | ///
177 | public enum HelpUriType
178 | {
179 | ///
180 | /// The uri is empty.
181 | ///
182 | None = 0,
183 |
184 | ///
185 | /// The uri specifies a command to be executed.
186 | ///
187 | Command = 1,
188 |
189 | ///
190 | /// The uri contains a topic index to be resolved in the local
191 | /// database.
192 | ///
193 | LocalTopic = 2,
194 |
195 | ///
196 | /// The uri contains a context string to be resolved in the local
197 | /// database.
198 | ///
199 | LocalContext = 3,
200 |
201 | ///
202 | /// The uri contains a database name and a context string; the
203 | /// context string must be resolved in that database.
204 | ///
205 | GlobalContext = 4,
206 |
207 | ///
208 | /// The uri contains a context string that can be resolved in any
209 | /// database in the help system, with the local database searched
210 | /// first.
211 | ///
212 | Context = 5,
213 |
214 | ///
215 | /// The uri contains a database name to be displayed as a single
216 | /// help topic.
217 | ///
218 | File = 6,
219 | }
220 |
221 | #if false
222 | public class HelpLocation
223 | {
224 | HelpTopic Topic;
225 | ushort LineNumber; // zero-based
226 | byte ColumnNumber; // zero-based
227 | }
228 | #endif
229 | }
230 |
--------------------------------------------------------------------------------
/QuickHelp/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("QuickHelp")]
9 | [assembly: AssemblyDescription("QuickHelp File Decoder")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("QuickHelp")]
13 | [assembly: AssemblyCopyright("Copyright © 2014")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Setting ComVisible to false makes the types in this assembly not visible
18 | // to COM components. If you need to access a type in this assembly from
19 | // COM, set the ComVisible attribute to true on that type.
20 | [assembly: ComVisible(false)]
21 |
22 | // The following GUID is for the ID of the typelib if this project is exposed to COM
23 | [assembly: Guid("f483fa86-81e4-42b0-955c-79fda02acb01")]
24 |
25 | // Version information for an assembly consists of the following four values:
26 | //
27 | // Major Version
28 | // Minor Version
29 | // Build Number
30 | // Revision
31 | //
32 | // You can specify all the values or you can default the Build and Revision Numbers
33 | // by using the '*' as shown below:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion("1.0.0.0")]
36 | [assembly: AssemblyFileVersion("1.0.0.0")]
37 |
--------------------------------------------------------------------------------
/QuickHelp/QuickHelp.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {C18C4ED1-E4B6-4DC3-B7CF-55053FEC165B}
8 | Library
9 | Properties
10 | QuickHelp
11 | QuickHelp
12 | v2.0
13 | 512
14 |
15 |
16 |
17 | true
18 | full
19 | false
20 | bin\Debug\
21 | DEBUG;TRACE
22 | prompt
23 | 4
24 |
25 |
26 | pdbonly
27 | true
28 | bin\Release\
29 | TRACE
30 | prompt
31 | 4
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
72 |
--------------------------------------------------------------------------------
/QuickHelp/Serialization/BufferReader.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using System.IO;
5 |
6 | namespace QuickHelp.Serialization
7 | {
8 | ///
9 | /// Provides methods to read typed data from a byte buffer. This class is
10 | /// similar in functionality to a BinaryReader backed by a MemoryStream,
11 | /// but provides additional methods with a lightweight implementation.
12 | ///
13 | /// TODO: a BufferReader should keep track of context information (like
14 | /// line and column) so that it can be used to indicate location of any
15 | /// error.
16 | public class BufferReader
17 | {
18 | readonly Encoding encoding;
19 | readonly byte[] buffer;
20 | readonly int endIndex;
21 | int index;
22 |
23 | public BufferReader(byte[] buffer)
24 | : this(buffer, 0, buffer.Length, Encoding.UTF8)
25 | {
26 | }
27 |
28 | public BufferReader(byte[] buffer, Encoding encoding)
29 | : this(buffer, 0, buffer.Length, encoding)
30 | {
31 | }
32 |
33 | public BufferReader(byte[] buffer, int index, int count)
34 | : this(buffer, index, count, Encoding.UTF8)
35 | {
36 | }
37 |
38 | public BufferReader(byte[] buffer, int index, int count, Encoding encoding)
39 | {
40 | if (buffer == null)
41 | throw new ArgumentNullException("buffer");
42 | if (index < 0 || index > buffer.Length)
43 | throw new ArgumentOutOfRangeException("index");
44 | if (count < 0 || count > buffer.Length - index)
45 | throw new ArgumentOutOfRangeException("count");
46 | if (encoding == null)
47 | throw new ArgumentNullException("encoding");
48 |
49 | this.buffer = buffer;
50 | this.index = index;
51 | this.endIndex = index + count;
52 | this.encoding = encoding;
53 | }
54 |
55 | public bool IsEOF
56 | {
57 | get { return index >= endIndex; }
58 | }
59 |
60 | public byte ReadByte()
61 | {
62 | if (index >= endIndex)
63 | throw new EndOfStreamException();
64 | return buffer[index++];
65 | }
66 |
67 | public UInt16 ReadUInt16()
68 | {
69 | if (index + 2 > endIndex)
70 | throw new EndOfStreamException();
71 | int value = buffer[index] | (buffer[index + 1] << 8);
72 | index += 2;
73 | return (UInt16)value;
74 | }
75 |
76 | public string ReadNullTerminatedString()
77 | {
78 | int k = Array.IndexOf(buffer, (byte)0, index, endIndex - index);
79 | if (k == -1)
80 | throw new EndOfStreamException();
81 |
82 | string s = encoding.GetString(buffer, index, k - index);
83 | index = k + 1;
84 | return s;
85 | }
86 |
87 | public string ReadFixedLengthString(int length)
88 | {
89 | if (length < 0)
90 | throw new ArgumentOutOfRangeException("length");
91 | if (length > endIndex - index)
92 | throw new EndOfStreamException();
93 |
94 | string s = encoding.GetString(buffer, index, length);
95 | index += length;
96 | return s;
97 | }
98 |
99 | public BufferReader ReadBuffer(int length)
100 | {
101 | if (length < 0)
102 | throw new ArgumentOutOfRangeException("length");
103 | if (length > endIndex - index)
104 | throw new EndOfStreamException();
105 |
106 | BufferReader subReader = new BufferReader(buffer, index, length, encoding);
107 | index += length;
108 | return subReader;
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/QuickHelp/Serialization/Format.txt:
--------------------------------------------------------------------------------
1 | QuickHelp Binary Format
2 | =======================
3 |
4 | This document describes the binary format of a QuickHelp .HLP file.
5 |
6 |
7 | Overview
8 | --------
9 |
10 | A QuickHelp .HLP file comprises the following sections:
11 |
12 | +------------------------+
13 | | Signature |
14 | +------------------------+
15 | | File Header |
16 | +------------------------+
17 | | Topic Index |
18 | +------------------------+
19 | | Context Strings |
20 | +------------------------+
21 | | Context Map |
22 | +------------------------+
23 | | Keywords |
24 | +------------------------+
25 | | Huffman Tree |
26 | +------------------------+
27 | | Topic[0] Text |
28 | +------------------------+
29 | | ... |
30 | +------------------------+
31 | | Topic[N-1] Text |
32 | +------------------------+
33 |
34 | Each section is described in detail below.
35 |
36 | Note 1: All numeric fields are stored in little-endian order unless specified
37 | otherwise.
38 |
39 | Note 2: Multiple help databases may be concatenated and stored in a single
40 | file. In this document, we assume a single database for convenience.
41 |
42 |
43 | Signature
44 | ---------
45 |
46 | An HLP file starts with the two-byte signature: 0x4C, 0x4E.
47 |
48 |
49 | File Header
50 | -----------
51 |
52 | Following the signature is the File Header section, which is a structure with
53 | the following fields:
54 |
55 | Range Type Field Meaning
56 | ----------------------------------------------------------------------------
57 | 02-04 WORD Version always 2
58 | 04-06 WORD Attributes bit 0: case-sensitivity of context
59 | strings
60 | bit 1: prevent the database from being
61 | decoded by the HELPMAKE utility
62 | bits 2-15: reserved; always 0
63 | 06-07 BYTE ControlCharacter usually ':'; 0FFh also seen
64 | 07-08 BYTE (padding 1) reserved; always 0
65 | 08-0A WORD TopicCount number of topics in the database
66 | 0A-0C WORD ContextCount number of context strings in the
67 | database
68 | 0C-0E WORD DisplayWidth width of the help viewer in characters
69 | 0E-10 WORD PredefinedCtxCount number of predefined context strings;
70 | usually 0
71 | 10-1E BYTE[] DatabaseName database name used to resolve external
72 | links; NULL-terminated and NULL-padded
73 | 1E-22 DWORD (reserved 1) always 0
74 | 22-26 DWORD TopicIndex offset of the Topic Index section
75 | 26-2A DWORD ContextStringsOffset offset of the Context Strings section
76 | 2A-2E DWORD ContextMapOffset offset of the Context Mapping section
77 | 2E-32 DWORD KeywordsOffset offset of the Keywords section;
78 | 0 if keyword compression is not used
79 | 32-36 DWORD HuffmanTreeOffset offset of the Huffman Tree section;
80 | 0 if Huffman compression is not used
81 | 36-3A DWORD TopicTextOffset offset of the start of topic texts
82 | 3A-3E DWORD (reserved 2) always 0
83 | 3E-42 DWORD (reserved 3) always 0
84 | 42-46 DWORD DatabaseSize size of the help database in bytes
85 |
86 |
87 | Topic Index
88 | -----------
89 |
90 | The Topic Index section comprises (TopicCount + 1) DWORD integers. The first
91 | TopicCount integers specify the offset of the corresponding topic texts
92 | relative to the beginning of the help database. The last integer specifies
93 | the offset just past the end of the last topic.
94 |
95 | (N = TopicCount)
96 | +--------------------+
97 | | TopicOffset[0] | (DWORD) offset of the first topic
98 | +--------------------+
99 | | ... | ...
100 | +--------------------+
101 | | TopicOffset[N-1] | (DWORD) offset of the last topic
102 | +--------------------+
103 | | TopicOffset[N] | (DWORD) indicates the end of the last topic;
104 | +--------------------+ should be equal to DatabaseSize.
105 |
106 | The size (in bytes) of topic k can be found by computing the difference
107 | between TopicOffset[k] and TopicOffset[k+1].
108 |
109 |
110 | Context Strings
111 | ---------------
112 |
113 | The Context Strings section comprises (ContextCount) NULL-terminated context
114 | strings. The context strings are not sorted.
115 |
116 | (N = ContextCount)
117 | +--------------------+
118 | | ContextString[0] | first context string (NULL-terminated)
119 | +--------------------+
120 | | ... | ...
121 | +--------------------+
122 | | ContextString[N-1] | last context string (NULL-terminated)
123 | +--------------------+
124 |
125 |
126 | Context Map
127 | -----------
128 |
129 | The Context Map section comprises (ContextCount) WORD integers. Each integer
130 | specifies the index of the topic to which the corresponding context string
131 | resolves, and must be within the range 0 to (TopicCount-1) inclusive.
132 |
133 | (N = ContextCount)
134 | +-------------------+
135 | | ContextMap[0] | (WORD) index of the topic that the first context
136 | +-------------------+ string points to
137 | | ... | ...
138 | +-------------------+
139 | | ContextMap[N-1] | (WORD) index of the topic that the last context
140 | +-------------------+ string points to
141 |
142 |
143 | Keywords
144 | --------
145 |
146 | The Keywords section contains a list of frequently used words in the topics.
147 | It is used by the keyword compression (dictionary substitution) pass. This
148 | section does not exist if keyword compression is not used for this database.
149 |
150 | Each word is prefixed by a byte that specifies the length of the word in
151 | bytes, not counting the prefix itself. The word is NOT NULL-terminated.
152 |
153 | (N = implied)
154 | +-------------+
155 | | Word[0] | first word in dictionary (length-prefixed string)
156 | +-------------+
157 | | ... | ...
158 | +-------------+
159 | | Word[N-1] | last word in dictionary (length-prefixed string)
160 | +-------------+
161 |
162 | The words are sorted in lexicographical order. The length prefix is not taken
163 | into account when sorting.
164 |
165 | There can be at most 1024 (10 bits) words in the dictionary; but there can be
166 | fewer. The number of words is not explicitly specified and must be implied by
167 | reading the entire section.
168 |
169 |
170 | Huffman Tree
171 | ------------
172 |
173 | The Huffman Tree section contains the huffman tree used by the Huffman
174 | compression pass. This section does not exist if Huffman compression is not
175 | used for this database.
176 |
177 | Each node in a huffman tree is either a leaf node or an internal node. A leaf
178 | node represents a symbol valued from 0 to 255. An internal node must have two
179 | children and encodes a bit in the huffman code: the left child encodes 0 and
180 | the right child encodes 1.
181 |
182 | 511 nodes are sufficient to encode all 256 symbols. If the tree contains less
183 | than 511 nodes, only a subset of the 256 symbols are encoded.
184 |
185 | The Huffman tree is compactly stored in an array of WORD integers, where each
186 | integer represents a node. The array is terminated by an extra WORD of value
187 | zero. The nodes plus the terminating 0 WORD should match the length of the
188 | section specified by [HuffmanTreeOffset, TopicTextOffset).
189 |
190 | The serialized format of the Huffman tree is as follows. Denote the nodes by
191 | Node[0] through Node[N] where N <= 511 and Node[N] == 0. Then:
192 |
193 | - A leaf node, Node[i], has its highest bit set to 1, and the symbol it
194 | represents is stored in the low byte of Node[i].
195 |
196 | - An internal node, Node[i], has its highest is set to 0, and
197 | o its right child (1 bit) is Node[i+1];
198 | o its left child (0 bit) is Node[Node[i]/2].
199 |
200 | - The root node is Node[0].
201 |
202 | Note that this format is not specific to a Huffman tree; it can be used to
203 | serialize any proper binary tree.
204 |
205 | The above node numbering scheme can be generated by performing a post-order
206 | traversal of the tree and number the node from N to 1 as they are visited.
207 |
208 |
209 | Topic Text
210 | ----------
211 |
212 | Following the meta data sections are N blocks of topic text. Each topic is
213 | separately compressed. The compression method is described below.
214 |
215 | Topic text is compiled, compressed, and encoded in three steps:
216 |
217 | Step 1. Topic text is compiled from QuickHelp markup format to binary
218 | format.
219 |
220 | Step 2. The binary text is compressed using keyword compression and
221 | run-length encoding.
222 |
223 | Step 3. The compressed text is encoded with Huffman coding and stored in
224 | the help database.
225 |
226 | The following diagram illustrates the encoding procedure.
227 |
228 | +=====================+
229 | | Markup Topic Text |
230 | +=====================+
231 | |
232 | | [1] compile to binary format
233 | v
234 | +=====================+
235 | | Binary Topic Text |
236 | +=====================+
237 | |
238 | | [2] keyword compression and
239 | | run-length encoding
240 | v
241 | +=====================+
242 | | Compact Topic Text |
243 | +=====================+
244 | |
245 | | [3] Huffman encoding
246 | v
247 | +=====================+
248 | | Encoded Topic Text |
249 | +=====================+
250 |
251 |
252 | Step 1: Compile QuickHelp markup format to binary format
253 | --------------------------------------------------------
254 |
255 | The QuickHelp markup format is described in detail in MASM documentation,
256 | Chapter 18 - Creating Help Files With HELPMAKE.
257 |
258 | In QuickHelp binary format, each line is represented by two parts:
259 | 1. Text
260 | 2. Styles and links
261 |
262 | "Text" is the characters displayed on the screen, stripped of any formatting
263 | information.
264 |
265 | "Styles" associate each character with one or more of the following styles:
266 | bold, italic, and underline. In QuickHelp viewer, the styles are rendered as
267 | follows:
268 |
269 | Value Style Foreground Color Background Color
270 | ---------------------------------------------------------------------
271 | 0 Normal (default) white black
272 | 1 Bold highlighted white black
273 | 2 Italic green black
274 | 3 Bold+Italic cyan black
275 | 4 Underline red black
276 | 5 Bold+Underline highlighted white cyan
277 | 6 Italic+Underline white black
278 | 7 Bold+Italic+Underline black black
279 |
280 | "Links" associate a range of characters in a line with a link target. The link
281 | target must take one of the following forms:
282 |
283 | 1. a context string that matches a context in the current help database or
284 | another help database, or
285 |
286 | 2. a 16-bit integer with the highest bit set, whose lowest 15 bits specifies
287 | a topic index in the current help database.
288 |
289 | Links are defined orthogonal to styles. Links must not overlap.
290 |
291 | With this model in mind, below we describe the QuickHelp binary format.
292 |
293 | Each topic is first split into lines. Colon commands (see MASM docs for a
294 | detailed description) are treated as plain text when stored. Each line
295 | consists of a text block and an attribute block, like below:
296 |
297 | +-----------------+
298 | | TextBlockLen(X) | 1 byte number of bytes in text block, including
299 | +-----------------+ the "TextBlockLen" byte
300 | . .
301 | . TextBlockData . X-1 bytes characters in the line, stripped of any
302 | . . formatting information
303 | +-----------------+
304 | | AttrBlockLen(Y) | 1 byte number of bytes in attribute block,
305 | +-----------------+ including the "AttrBlockLen" byte
306 | . .
307 | . AttrBlockData . Y-1 bytes character style and link information; see
308 | . . below.
309 | +-----------------+
310 |
311 | "TextBlockLen" and "AttrBlockLen" must be greater than zero.
312 |
313 | "TextBlockData" is the plain text to display. Each byte corresponds to an
314 | ASCII or Extended ASCII character; on Windows, this is code page 437. Note,
315 | however, that characters 0-31 are rendered as graphic characters instead of
316 | interpreted as control characters when displayed in QuickHelp; this means
317 | that a further mapping must be performed after transforming using CP-437.
318 |
319 | "AttrBlockData" comprises a mandatory "Styles" part followed by an optional
320 | "Links" part.
321 |
322 | If no link exists in a line, the format of "AttrBlockData" is
323 |
324 | (Y-1) bytes
325 | +==========+
326 | | Styles |
327 | +==========+
328 |
329 | If links are present in a line, the format of "AttrBlockData" is
330 |
331 | (? bytes) 1 byte (? bytes) --> total (Y-1) bytes
332 | +==========+------+=========+
333 | | Styles | 0xFF | Links |
334 | +==========+------+=========+
335 |
336 | "Styles" is an alternating list of "chunk length" and "style", as follows:
337 |
338 | +--------------+
339 | | ChunkLen 0 | 1 byte
340 | +--------------+
341 | | Style 1 | 1 byte
342 | +--------------+
343 | | ChunkLen 1 | 1 byte
344 | +--------------+
345 | | Style 2 | 1 byte
346 | +--------------+
347 | | ChunkLen 2 | 1 byte
348 | +--------------+
349 | ~ ... ~ ...
350 | +--------------+
351 |
352 | This list always starts with a ChunkLen[0] field. The default style applies
353 | to the first (ChunkLen[0]) characters in the line. The next ChunkLen[1]
354 | characters apply the style defined in Style[1]. Following that, the next
355 | ChunkLen[2] characters apply the style defined in Style[2]; and so on.
356 |
357 | Each Style byte comprises the following bits:
358 |
359 | 7 6 5 4 3 2 1 0
360 | +---+---+---+---+---+---+---+---+
361 | | 0 | 0 | 0 | 0 | 0 | U | I | B |
362 | +---+---+---+---+---+---+---+---+
363 | |_______________| | | |---- Bold
364 | | | |-------- Italic
365 | reserved; |------------ Underline
366 | must be 0.
367 |
368 | Each Style[i] field replaces the previous style; the style bits are not merged
369 | or toggled.
370 |
371 | "Links" is an array of variable-length records, where each record defines a
372 | link in the line. The format of a record is:
373 |
374 | +--------------+
375 | | StartIndex | 1 byte ONE-based index of the first character in the
376 | +--------------+ link, inclusive.
377 | | EndIndex | 1 byte ONE-based index of the last character in the
378 | +--------------+ link, inclusive.
379 | | Context | ? bytes NULL-terminated context string that specifies
380 | | String | the link target, or an empty string to indicate
381 | +--------------+ that TopicIndex should be used as the target.
382 | | TopicIndex | WORD Optional; present only if ContextString is empty
383 | +--------------+
384 |
385 |
386 | Step 2: Keyword compression and run-length encoding
387 | ---------------------------------------------------
388 |
389 | The compressed data is a byte stream that has the following format.
390 |
391 | Each byte in the compressed stream is either a control byte or a value byte.
392 | This is determined as follows:
393 |
394 | Byte 00 - 0F : value byte
395 | Byte 10 - 1A : control byte
396 | Byte 1B - FF : value byte
397 |
398 | During decoding, value bytes are copied as is to the output, unless they
399 | follow a control byte and is treated as an argument. See below.
400 |
401 | There are eleven control bytes, valued from 0x10 (16) to 0x1A (26). A control
402 | byte takes one or two bytes as its argument. The format of each control byte
403 | is summarized below.
404 |
405 | Hex Dec Control Byte Argument Byte 1 Argument Byte 2
406 |
407 | +-----------------+-----------------+
408 | 10-17 16-23 | 0 0 0 1 0 A D D | D D D D D D D D |
409 | | S 9 8 | 7 6 5 4 3 2 1 0 |
410 | +-----------------+-----------------+
411 |
412 | +-----------------+-----------------+
413 | 18 24 | 0 0 0 1 1 0 0 0 | SPACE-COUNT |
414 | +-----------------+-----------------+
415 |
416 | +-----------------+-----------------+-----------------+
417 | 19 25 | 0 0 0 1 1 0 0 1 | REPEAT-BYTE | REPEAT-LENGTH |
418 | +-----------------+-----------------+-----------------+
419 |
420 | +-----------------+-----------------+
421 | 1A 26 | 0 0 0 1 1 0 1 0 | ESCAPE-BYTE |
422 | +-----------------+-----------------+
423 |
424 | Control bytes 10h-17h encode a dictionary entry index. The index, D, is
425 | specified by the lowest 2 bits of the control byte, followed by the 8 bits
426 | of the argument byte that follows. (This gives 10 bits available; hence the
427 | dictionary can contain no more than 1024 entries.) The dictionary entry is
428 | copied to the output. If the AS (Append-Space) bit is 1, a space (ASCII 32)
429 | is appended to the output.
430 |
431 | Control byte 18h encodes a run of spaces (ASCII 32). The number of spaces is
432 | specified by the argument byte that follows. This many spaces are appended to
433 | the output.
434 |
435 | Control byte 19h encodes a run of bytes. The byte to repeat is given by the
436 | first argument, and the run-length is given by the second argument. That many
437 | bytes are repeated and appended to the output.
438 |
439 | Control byte 1Ah escapes the next byte (argument) in the compressed stream.
440 | The argument is written as is to the output. This is necessary to output a
441 | byte in the range 10h to 1Ah.
442 |
443 |
444 | Step 3: Huffman coding
445 | ----------------------
446 |
447 | The resulting binary data from Step 2 is encoded by a huffman coder. The
448 | huffman tree encodes 256 symbols (i.e. byte value 0 - 255). There is no limit
449 | on the number of bits used to encode each symbol.
450 |
451 | +----------+==============+
452 | | OUTLEN | BIT STREAM |
453 | +----------+==============+
454 |
455 | OUTLEN is the number of bytes in the binary topic data that is produced by
456 | Step 1. Note that it is NOT the compressed data produced by Step 2.
457 |
458 | BIT STREAM is a bit stream that contains the huffman-encoded data from Step 2.
459 | For each byte in the stream, the bits are written to (and read from) starting
460 | from the MOST significant bit of that byte; there may be extra, unused bits at
461 | the end of the last byte. This is why we need the OUTLEN field.
462 |
--------------------------------------------------------------------------------
/QuickHelp/Serialization/Graphic437Encoding.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 |
4 | namespace QuickHelp.Serialization
5 | {
6 | public class Graphic437Encoding : Encoding
7 | {
8 | private static readonly Encoding CP437 = Encoding.GetEncoding(437);
9 | private const string GraphicCharacters = "\0☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼";
10 |
11 | public static bool IsControlCharacter(char c)
12 | {
13 | return (c < 32) || (c == 127);
14 | }
15 |
16 | public static bool ContainsControlCharacter(string s)
17 | {
18 | if (s == null)
19 | throw new ArgumentNullException(nameof(s));
20 |
21 | for (int i = 0; i < s.Length; i++)
22 | {
23 | if (IsControlCharacter(s[i]))
24 | return true;
25 | }
26 | return false;
27 | }
28 |
29 | public static void SubstituteControlCharacters(char[] chars)
30 | {
31 | if (chars == null)
32 | throw new ArgumentNullException(nameof(chars));
33 |
34 | SubstituteControlCharacters(chars, 0, chars.Length);
35 | }
36 |
37 | public static void SubstituteControlCharacters(char[] chars, int index, int count)
38 | {
39 | if (chars == null)
40 | throw new ArgumentNullException(nameof(chars));
41 | if (index < 0 || index > chars.Length)
42 | throw new ArgumentOutOfRangeException(nameof(index));
43 | if (count < 0 || count > chars.Length - index)
44 | throw new ArgumentOutOfRangeException(nameof(count));
45 |
46 | for (int i = index; i < index + count; i++)
47 | {
48 | if (chars[i] < 32)
49 | chars[i] = GraphicCharacters[chars[i]];
50 | else if (chars[i] == 127)
51 | chars[i] = '⌂';
52 | }
53 | }
54 |
55 | public static string SubstituteControlCharacters(string s)
56 | {
57 | if (s == null)
58 | return null;
59 |
60 | if (!ContainsControlCharacter(s))
61 | return s;
62 |
63 | char[] chars = s.ToCharArray();
64 | SubstituteControlCharacters(chars);
65 | return new string(chars);
66 | }
67 |
68 | public override int GetByteCount(char[] chars, int index, int count)
69 | {
70 | return CP437.GetByteCount(chars, index, count);
71 | }
72 |
73 | public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex)
74 | {
75 | return CP437.GetBytes(chars, charIndex, charCount, bytes, byteIndex);
76 | }
77 |
78 | public override int GetCharCount(byte[] bytes, int index, int count)
79 | {
80 | return CP437.GetCharCount(bytes, index, count);
81 | }
82 |
83 | public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex)
84 | {
85 | int charCount = CP437.GetChars(bytes, byteIndex, byteCount, chars, charIndex);
86 | SubstituteControlCharacters(chars, charIndex, charCount);
87 | return charCount;
88 | }
89 |
90 | public override int GetMaxByteCount(int charCount)
91 | {
92 | return CP437.GetMaxByteCount(charCount);
93 | }
94 |
95 | public override int GetMaxCharCount(int byteCount)
96 | {
97 | return CP437.GetMaxCharCount(byteCount);
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/QuickHelp/Serialization/HelpCommand.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace QuickHelp.Serialization
5 | {
6 | ///
7 | /// Specifies a dot or colon command in serialized help content.
8 | ///
9 | ///
10 | /// Because dot or colon commands are only relevant in serialized format,
11 | /// this enum and related classes are internal to the assembly.
12 | ///
13 | enum HelpCommand
14 | {
15 | ///
16 | /// Special value that indicates the absence of a command.
17 | ///
18 | None = 0,
19 |
20 | ///
21 | /// Lists the category in which the current topic appears and its
22 | /// position in the list of topics. The category name is used by the
23 | /// QuickHelp Categories command, which displays the list of topics.
24 | /// Supported only by QuickHelp.
25 | ///
26 | [HelpCommandFormat(".category", ":c", "string")]
27 | Category,
28 |
29 | ///
30 | /// Indicates that the topic cannot be displayed. Use this command to
31 | /// hide command topics and other internal information.
32 | ///
33 | [HelpCommandFormat(".command", ":x", null)]
34 | Command,
35 |
36 | ///
37 | /// Takes a string as parameter, which is a comment that appears only
38 | /// in the source file. Comments are not inserted in the database and
39 | /// are not restored during decoding.
40 | ///
41 | [HelpCommandFormat(".comment", null, "string")]
42 | [HelpCommandFormat("..", null, "string")]
43 | Comment,
44 |
45 | ///
46 | /// Takes a string as parameter. The string defines a context.
47 | ///
48 | [HelpCommandFormat(".context", null, "string")]
49 | Context,
50 |
51 | ///
52 | /// Ends a paste section. See the .paste command. Supported only by
53 | /// QuickHelp.
54 | ///
55 | [HelpCommandFormat(".end", ":e", null)]
56 | End,
57 |
58 | ///
59 | /// Executes the specified command. For example,
60 | /// .execute Pmark context represents a jump to the specified context
61 | /// at the specified mark. See the .mark command.
62 | ///
63 | [HelpCommandFormat(".execute", ":y", "command")]
64 | Execute,
65 |
66 | ///
67 | /// Locks the first numlines lines at the top of the screen. These
68 | /// lines do not move when the text is scrolled.
69 | ///
70 | [HelpCommandFormat(".freeze", ":z", "numlines")]
71 | Freeze,
72 |
73 | ///
74 | /// Sets the default window size for the topic in topiclength lines.
75 | ///
76 | [HelpCommandFormat(".length", ":l", "topiclength")]
77 | Length,
78 |
79 | ///
80 | /// Tells HELPMAKE to reset the line number to begin at number for
81 | /// subsequent lines of the input file. Line numbers appear in
82 | /// HELPMAKE error messages. See .source. The .line command is not
83 | /// inserted in the Help database and is not restored during decoding.
84 | ///
85 | [HelpCommandFormat(".line", null, "number")]
86 | Line,
87 |
88 | ///
89 | /// Indicates that the current topic contains a list of topics. Help
90 | /// displays a highlighted line; you can choose a topic by moving the
91 | /// highlighted line over the desired topic and pressing ENTER. If the
92 | /// line contains a coded link, Help looks up that link. If it does
93 | /// not contain a link, Help looks within the line for a string
94 | /// terminated by two spaces or a newline character and looks up that
95 | /// string. Otherwise, Help looks up the first word.
96 | ///
97 | [HelpCommandFormat(".list", ":i", null)]
98 | List,
99 |
100 | ///
101 | /// Defines a mark immediately preceding the following line of text. The
102 | /// marked line shows a script command where the display of a topic
103 | /// begins. The name identifies the mark. The column is an integer value
104 | /// specifying a column location within the marked line. Supported only
105 | /// by QuickHelp.
106 | ///
107 | [HelpCommandFormat(".mark", ":m", "name [[column]]")]
108 | Mark,
109 |
110 | ///
111 | /// Tells the Help reader to look up the next topic using context
112 | /// instead of the topic that physically follows it in the file.
113 | /// You can use this command to skip large blocks of .command or
114 | /// .popup topics.
115 | ///
116 | [HelpCommandFormat(".next", ":>", "context")]
117 | Next,
118 |
119 | ///
120 | /// Begins a paste section. The pastename appears in the QuickHelp
121 | /// Paste menu. Supported only by QuickHelp.
122 | ///
123 | [HelpCommandFormat(".paste", ":p", "pastename")]
124 | Paste,
125 |
126 | ///
127 | /// Tells the Help reader to display the current topic as a popup
128 | /// window instead of as a normal, scrollable topic. Supported only
129 | /// by QuickHelp.
130 | ///
131 | [HelpCommandFormat(".popup", ":g", null)]
132 | Popup,
133 |
134 | ///
135 | /// Tells the Help reader to look up the previous topic using context
136 | /// instead of the topic that physically precedes it in the file. You
137 | /// can use this command to skip large blocks of .command or .popup
138 | /// topics.
139 | ///
140 | [HelpCommandFormat(".previous", ":<", "context")]
141 | Previous,
142 |
143 | ///
144 | /// Turns off special processing of certain characters by the Help
145 | /// reader.
146 | ///
147 | [HelpCommandFormat(".raw", ":u", null)]
148 | Raw,
149 |
150 | ///
151 | /// Tells the Help reader to display the topic in the Reference menu.
152 | /// You can list multiple topics; separate each additional topic with
153 | /// a comma. A .ref command is not affected by the /W option. If no
154 | /// topic is specified, QuickHelp searches the line immediately
155 | /// following for a See or See Also reference; if present, the
156 | /// reference must be the first word on the line. Supported only by
157 | /// QuickHelp.
158 | ///
159 | [HelpCommandFormat(".ref", ":r", "topic[[, topic]]")]
160 | Ref,
161 |
162 | ///
163 | /// Tells HELPMAKE that subsequent topics come from filename. HELPMAKE
164 | /// error messages contain the name and line number of the input file.
165 | /// The .source command tells HELPMAKE to use filename in the message
166 | /// instead of the name of the input file and to reset the line number
167 | /// to 1. This is useful when you concatenate several sources to form
168 | /// the input file. See .line. The .source command is not inserted in
169 | /// the Help database and is not restored during decoding.
170 | ///
171 | [HelpCommandFormat(".source", null, "filename")]
172 | Source,
173 |
174 | ///
175 | /// Defines text as the name or title to be displayed in place of the
176 | /// context string if the application Help displays a title. This
177 | /// command is always the first line in the context unless you also
178 | /// use the .length or .freeze commands.
179 | ///
180 | [HelpCommandFormat(".topic", ":n", "text")]
181 | Topic,
182 | }
183 |
184 | [AttributeUsage(AttributeTargets.All, AllowMultiple=true)]
185 | class HelpCommandFormatAttribute : Attribute
186 | {
187 | readonly string dotCommand;
188 | readonly string colonCommand;
189 | readonly string parameterFormat;
190 |
191 | public HelpCommandFormatAttribute(
192 | string dotCommand, string colonCommand, string parameterFormat)
193 | {
194 | if (dotCommand == null)
195 | throw new ArgumentNullException(nameof(dotCommand));
196 | if (dotCommand.Length == 0 || dotCommand[0] != '.')
197 | throw new ArgumentException("Dot command must start with a dot.", nameof(dotCommand));
198 |
199 | if (colonCommand == null)
200 | throw new ArgumentNullException(nameof(colonCommand));
201 | if (colonCommand.Length == 0 || colonCommand[0] != ':')
202 | throw new ArgumentException("Colon command must start with a colon.", nameof(colonCommand));
203 |
204 | if (parameterFormat == null)
205 | throw new ArgumentNullException(nameof(parameterFormat));
206 |
207 | this.dotCommand = dotCommand;
208 | this.colonCommand = colonCommand;
209 | this.parameterFormat = parameterFormat;
210 | }
211 | }
212 |
213 | // TODO: rename Converter to something else
214 | static class HelpCommandConverter
215 | {
216 | ///
217 | /// Parses a line for a command and executes the command if present.
218 | ///
219 | ///
220 | /// true if the line represents a command and is executed;
221 | /// false if the line does not represent a command. Throws an
222 | /// exception if the line represents an unknown or invalid command.
223 | ///
224 | public static bool ProcessCommand(
225 | string line, char controlCharacter, HelpTopic topic)
226 | {
227 | HelpCommand command;
228 | string parameter;
229 | // if (!ParseCommand(line, controlCharacter, out command, out parameter))
230 | if (!ParseColonCommand(line, controlCharacter, out command, out parameter))
231 | return false;
232 |
233 | ExecuteCommand(command, parameter, topic);
234 | return true;
235 | }
236 |
237 | ///
238 | /// Processes a command within the given topic.
239 | ///
240 | ///
241 | /// true if the command is successfully processed; false
242 | /// if the command is not supported or if the syntax is invalid.
243 | ///
244 | private static void ExecuteCommand(
245 | HelpCommand command, string parameter, HelpTopic topic)
246 | {
247 | switch (command)
248 | {
249 | case HelpCommand.Category:
250 | topic.Category = parameter;
251 | break;
252 |
253 | case HelpCommand.Command:
254 | topic.IsHidden = true;
255 | break;
256 |
257 | case HelpCommand.End:
258 | if (topic.Snippets.Count > 0)
259 | {
260 | topic.Snippets[topic.Snippets.Count - 1].EndLine
261 | = topic.Lines.Count;
262 | }
263 | break;
264 |
265 | case HelpCommand.Execute:
266 | // TODO: there could be multiple execute commands.
267 | topic.ExecuteCommand = parameter;
268 | break;
269 |
270 | case HelpCommand.Freeze:
271 | topic.FreezeHeight = Int32.Parse(parameter);
272 | break;
273 |
274 | case HelpCommand.Length:
275 | topic.WindowHeight = Int32.Parse(parameter);
276 | break;
277 |
278 | case HelpCommand.List:
279 | topic.IsList = true;
280 | break;
281 |
282 | case HelpCommand.Mark:
283 | // TODO: to be implemented
284 | System.Diagnostics.Debug.WriteLine(string.Format(
285 | "**** NOT IMPLEMENTED **** HelpCommand.Mark @ Line {0} of Topic {1} ({2}): {3}",
286 | topic.Lines.Count, topic.TopicIndex, topic.Title, parameter));
287 | break;
288 |
289 | case HelpCommand.Next:
290 | topic.Successor = new HelpUri(parameter);
291 | break;
292 |
293 | case HelpCommand.Paste:
294 | {
295 | HelpSnippet snippet = new HelpSnippet();
296 | snippet.Name = parameter;
297 | snippet.StartLine = topic.Lines.Count;
298 | snippet.EndLine = topic.Lines.Count;
299 | topic.Snippets.Add(snippet);
300 | }
301 | break;
302 |
303 | case HelpCommand.Popup:
304 | topic.IsPopup = true;
305 | break;
306 |
307 | case HelpCommand.Previous:
308 | topic.Predecessor = new HelpUri(parameter);
309 | break;
310 |
311 | case HelpCommand.Raw:
312 | topic.IsRaw = true;
313 | break;
314 |
315 | case HelpCommand.Ref:
316 | if (string.IsNullOrEmpty(parameter))
317 | {
318 | // TODO: The references are in the following
319 | // lines until the next blank line. We don't
320 | // handle this for the moment.
321 | }
322 | else
323 | {
324 | string[] references = parameter.Split(',');
325 | foreach (string reference in references)
326 | {
327 | string contextString = reference.Trim();
328 | if (!string.IsNullOrEmpty(contextString))
329 | topic.References.Add(contextString);
330 | }
331 | }
332 | break;
333 |
334 | case HelpCommand.Topic:
335 | topic.Title = parameter;
336 | break;
337 |
338 | default:
339 | throw new NotImplementedException();
340 | }
341 | }
342 |
343 | private static bool ParseCommand(
344 | string line, char controlCharacter,
345 | out HelpCommand command, out string parameters)
346 | {
347 | command = HelpCommand.None;
348 | parameters = "";
349 |
350 | if (string.IsNullOrEmpty(line))
351 | return false;
352 | else if (line[0] == '.')
353 | return ParseDotCommand(line, out command, out parameters);
354 | else if (line[0] == controlCharacter)
355 | return ParseColonCommand(line, controlCharacter, out command, out parameters);
356 | else
357 | return false;
358 | }
359 |
360 | private static bool ParseColonCommand(
361 | string line, char controlCharacter,
362 | out HelpCommand command, out string parameters)
363 | {
364 | command = HelpCommand.None;
365 | parameters = "";
366 |
367 | if (string.IsNullOrEmpty(line) || line[0] != controlCharacter)
368 | return false;
369 |
370 | if (line.Length < 2)
371 | throw new ArgumentException("Colon command must not be blank.");
372 |
373 | if (!ColonCommandToHelpCommandMapping.TryGetValue(line[1], out command))
374 | throw new ArgumentException("Colon command is not recognized.");
375 |
376 | parameters = line.Substring(2);
377 | return true;
378 | }
379 |
380 | private static bool ParseDotCommand(
381 | string line, out HelpCommand command, out string parameters)
382 | {
383 | command = HelpCommand.None;
384 | parameters = "";
385 |
386 | if (string.IsNullOrEmpty(line) || line[0] != '.')
387 | return false;
388 |
389 | string dotCommand;
390 | int k = line.IndexOf(' ');
391 | if (k < 0)
392 | {
393 | dotCommand = line.Substring(1);
394 | parameters = "";
395 | }
396 | else
397 | {
398 | dotCommand = line.Substring(1, k - 1);
399 | parameters = line.Substring(k + 1);
400 | }
401 |
402 | char colonCommand;
403 | if (DotCommandToColonCommandMapping.TryGetValue(dotCommand, out colonCommand))
404 | {
405 | command = ColonCommandToHelpCommandMapping[colonCommand];
406 | return true;
407 | }
408 |
409 | // Process source-only dot commands that do not have an equivalent
410 | // colon command.
411 | switch (dotCommand)
412 | {
413 | case "comment":
414 | case ".":
415 | command = HelpCommand.Comment;
416 | break;
417 | case "context":
418 | command = HelpCommand.Context;
419 | break;
420 | case "source":
421 | command = HelpCommand.Source;
422 | break;
423 | default:
424 | throw new ArgumentException("Dot command is not recognized.");
425 | }
426 | return true;
427 | }
428 |
429 | private static readonly Dictionary
430 | DotCommandToColonCommandMapping = new Dictionary
431 | {
432 | { "category", 'c' },
433 | { "command", 'x' },
434 | { "end", 'e' },
435 | { "execute", 'y' },
436 | { "freeze", 'z' },
437 | { "length", 'l' },
438 | { "list", 'i' },
439 | { "mark", 'm' },
440 | { "next", '>' },
441 | { "paste", 'p' },
442 | { "popup", 'g' },
443 | { "previous", '<' },
444 | { "raw", 'u' },
445 | { "ref", 'r' },
446 | { "topic", 'n' },
447 | };
448 |
449 | private static readonly Dictionary
450 | ColonCommandToHelpCommandMapping = new Dictionary
451 | {
452 | { '<', HelpCommand.Previous },
453 | { '>', HelpCommand.Next },
454 | { 'c', HelpCommand.Category },
455 | { 'e', HelpCommand.End },
456 | { 'g', HelpCommand.Popup },
457 | { 'i', HelpCommand.List },
458 | { 'l', HelpCommand.Length },
459 | { 'm', HelpCommand.Mark },
460 | { 'n', HelpCommand.Topic },
461 | { 'p', HelpCommand.Paste },
462 | { 'r', HelpCommand.Ref },
463 | { 'u', HelpCommand.Raw },
464 | { 'x', HelpCommand.Command },
465 | { 'y', HelpCommand.Execute },
466 | { 'z', HelpCommand.Freeze },
467 | };
468 | }
469 | }
470 |
--------------------------------------------------------------------------------
/QuickHelp/Serialization/HelpFile.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace QuickHelp.Serialization
4 | {
5 | class BinaryHelpFileHeader
6 | {
7 | public UInt16 Version;
8 | public HelpFileAttributes Attributes;
9 | public byte ControlCharacter;
10 | public byte Padding1;
11 | public UInt16 TopicCount;
12 | public UInt16 ContextCount;
13 | public byte DisplayWidth;
14 | public byte Padding2;
15 | public UInt16 Padding3;
16 | public string DatabaseName;
17 | public int Reserved1;
18 | public int TopicIndexOffset;
19 | public int ContextStringsOffset;
20 | public int ContextMapOffset;
21 | public int KeywordsOffset;
22 | public int HuffmanTreeOffset;
23 | public int TopicTextOffset;
24 | public int Reserved2;
25 | public int Reserved3;
26 | public int DatabaseSize;
27 | }
28 |
29 | [Flags]
30 | enum HelpFileAttributes : ushort
31 | {
32 | None = 0,
33 |
34 | ///
35 | /// Indicates that the context strings in the archive are
36 | /// case-sensitive.
37 | ///
38 | CaseSensitive = 1,
39 |
40 | ///
41 | /// Indicates that the help archive may not be decoded by the
42 | /// HELPMAKE utility.
43 | ///
44 | Locked = 2,
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/QuickHelp/Serialization/SerializationOptions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using QuickHelp.Compression;
4 |
5 | namespace QuickHelp.Serialization
6 | {
7 | ///
8 | /// Contains options to control the serialization process.
9 | ///
10 | public class SerializationOptions
11 | {
12 | private readonly List m_keywords = new List();
13 |
14 | ///
15 | /// Gets or sets the serialized format.
16 | ///
17 | public SerializationFormat Format { get; set; }
18 |
19 | public char ControlCharacter { get; set; }
20 |
21 | ///
22 | /// Gets or sets the compression level.
23 | ///
24 | ///
25 | /// On serialization, if Format is Automatic or Binary, this value
26 | /// controls the compression level in the serialized .HLP file. If
27 | /// Format is Markup, this value is ignored.
28 | ///
29 | /// On deserialization, this property is set to the actual compression
30 | /// level used in the input.
31 | ///
32 | public CompressionFlags Compression { get; set; }
33 |
34 | ///
35 | /// Gets or sets a list of keywords used for keyword compression.
36 | ///
37 | ///
38 | /// On serialization, if keyword compression is enabled, the
39 | /// serializer uses the dictionary specified by this property if it
40 | /// is not null, or computes the dictionary on the fly and
41 | /// updates this property if it is null.
42 | ///
43 | /// On deserialization, the serializer sets this property to the
44 | /// actual dictionary used in the input or null if the source
45 | /// does not use keyword compression.
46 | ///
47 | public byte[][] Keywords { get; set; }
48 |
49 | ///
50 | /// Gets or sets the Huffman tree used for Huffman compression.
51 | ///
52 | ///
53 | /// On serialization, if Huffman compression is enabled, the
54 | /// serializer uses the Huffman tree specified by this property if it
55 | /// is not null, or computes a Huffman tree on-the-fly and
56 | /// updates this property if it is null.
57 | ///
58 | /// On deserialization, the serializer sets this property to the
59 | /// actual Huffman tree used in the input, or null if the
60 | /// source does not use Huffman compression.
61 | ///
62 | public HuffmanTree HuffmanTree { get; set; }
63 | }
64 |
65 | ///
66 | /// Specifies the serialized format of a help database.
67 | ///
68 | public enum SerializationFormat
69 | {
70 | ///
71 | /// On deserialization, automatically detect the input format. On
72 | /// serialization, use Binary format.
73 | ///
74 | Automatic = 0,
75 |
76 | ///
77 | /// Specifies the binary format (with .HLP extension).
78 | ///
79 | Binary = 1,
80 |
81 | ///
82 | /// Specifies the markup format (with .SRC extension).
83 | ///
84 | Markup = 2,
85 | }
86 |
87 | [Flags]
88 | public enum CompressionFlags
89 | {
90 | None = 0,
91 | RunLength = 1,
92 | Keyword = 2,
93 | ExtendedKeyword = 4,
94 | Huffman = 8,
95 | All = RunLength | Keyword | ExtendedKeyword | Huffman
96 | }
97 |
98 | }
99 |
--------------------------------------------------------------------------------
/QuickHelp/Serialization/StreamView.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 |
4 | namespace QuickHelp.Serialization
5 | {
6 | ///
7 | /// Represents a view on an underlying base stream with different position
8 | /// and length.
9 | ///
10 | class StreamView : Stream
11 | {
12 | private readonly Stream m_baseStream;
13 | private readonly long m_length;
14 | private long m_position;
15 |
16 | public StreamView(Stream baseStream, long length)
17 | : this(baseStream, length, 0)
18 | {
19 | }
20 |
21 | ///
22 | /// Creates a view on a stream.
23 | ///
24 | public StreamView(Stream baseStream, long length, long position)
25 | {
26 | if (baseStream == null)
27 | throw new ArgumentNullException(nameof(baseStream));
28 | if (length < 0)
29 | throw new ArgumentOutOfRangeException(nameof(length));
30 | if (!(position >= 0 && position <= length))
31 | throw new ArgumentOutOfRangeException(nameof(position));
32 |
33 | m_baseStream = baseStream;
34 | m_length = length;
35 | m_position = position;
36 | }
37 |
38 | public override long Length
39 | {
40 | get { return m_length; }
41 | }
42 |
43 | public override int Read(byte[] buffer, int offset, int count)
44 | {
45 | if (buffer == null)
46 | throw new ArgumentNullException(nameof(buffer));
47 | if (!(offset >= 0 && offset <= buffer.Length))
48 | throw new ArgumentOutOfRangeException(nameof(offset));
49 | if (!(count >= 0 && offset + count <= buffer.Length))
50 | throw new ArgumentOutOfRangeException(nameof(count));
51 |
52 | if (count > m_length - m_position)
53 | count = (int)(m_length - m_position);
54 | if (count == 0)
55 | return 0;
56 |
57 | // TODO: read full or throw exception
58 | int actual = m_baseStream.Read(buffer, offset, count);
59 | m_position += actual;
60 | return actual;
61 | }
62 |
63 | public override bool CanRead
64 | {
65 | get { return m_baseStream.CanRead; }
66 | }
67 |
68 | public override bool CanWrite
69 | {
70 | get { return false; }
71 | }
72 |
73 | public override bool CanSeek
74 | {
75 | get { return false; }
76 | }
77 |
78 | public override void Flush()
79 | {
80 | throw new NotSupportedException();
81 | }
82 |
83 | public override long Position
84 | {
85 | get { return m_position; }
86 | set { throw new NotSupportedException(); }
87 | }
88 |
89 | public override long Seek(long offset, SeekOrigin origin)
90 | {
91 | throw new NotSupportedException();
92 | }
93 |
94 | public override void Write(byte[] buffer, int offset, int count)
95 | {
96 | throw new NotImplementedException();
97 | }
98 |
99 | public override void SetLength(long value)
100 | {
101 | throw new NotSupportedException();
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This project provides a .Net library and several utility programs to view MS-DOS (.HLP) help files,
2 | making them accessible in a modern environment.
3 |
4 | **Highlights**
5 |
6 | [QuickHelp Format Description](https://raw.githubusercontent.com/fancidev/DosHelp/master/QuickHelp/Serialization/Format.txt)
7 | describes the format of a QuickHelp .HLP file.
8 |
9 | [QuickHelp Library](https://github.com/fancidev/DosHelp/tree/master/QuickHelp) is a .NET 2.0 library
10 | that enables you to read QuickHelp files.
11 |
12 | [HelpBrowser](https://github.com/fancidev/DosHelp/tree/master/HelpBrowser) is a program (requires
13 | .NET 2.0) that allows the user to browse the contents in a QuickHelp file.
14 |
15 | [HelpConvert](https://github.com/fancidev/DosHelp/tree/master/HelpConvert) is a command line utility
16 | (requires .NET 2.0) that converts a QuickHelp file to a set of cross-referenced HTML pages suitable
17 | for viewing in a browser.
18 |
19 | **Reference**
20 |
21 | *Creating Help Files With HELPMAKE* (Chapter 18 of MASM Documentation, Environment and Tools)
22 |
23 | *Microsoft Professional Advisor, Library Reference*
--------------------------------------------------------------------------------