markus / MarkusAutoUpdate / src / NetSparkle / Libraries / MarkdownSharp.cs @ f2b4c204
이력 | 보기 | 이력해설 | 다운로드 (69.7 KB)
1 | d8f5045e | taeseongkim | /* |
---|---|---|---|
2 | * MarkdownSharp |
||
3 | * ------------- |
||
4 | * a C# Markdown processor |
||
5 | * |
||
6 | * Markdown is a text-to-HTML conversion tool for web writers |
||
7 | * Copyright (c) 2004 John Gruber |
||
8 | * http://daringfireball.net/projects/markdown/ |
||
9 | * |
||
10 | * Markdown.NET |
||
11 | * Copyright (c) 2004-2009 Milan Negovan |
||
12 | * http://www.aspnetresources.com |
||
13 | * http://aspnetresources.com/blog/markdown_announced.aspx |
||
14 | * |
||
15 | * MarkdownSharp |
||
16 | * Copyright (c) 2009-2011 Jeff Atwood |
||
17 | * http://stackoverflow.com |
||
18 | * http://www.codinghorror.com/blog/ |
||
19 | * http://code.google.com/p/markdownsharp/ |
||
20 | * |
||
21 | * History: Milan ported the Markdown processor to C#. He granted license to me so I can open source it |
||
22 | * and let the community contribute to and improve MarkdownSharp. |
||
23 | * |
||
24 | */ |
||
25 | |||
26 | #region Copyright and license |
||
27 | |||
28 | /* |
||
29 | |||
30 | Copyright (c) 2009 - 2010 Jeff Atwood |
||
31 | |||
32 | http://www.opensource.org/licenses/mit-license.php |
||
33 | |||
34 | Permission is hereby granted, free of charge, to any person obtaining a copy |
||
35 | of this software and associated documentation files (the "Software"), to deal |
||
36 | in the Software without restriction, including without limitation the rights |
||
37 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||
38 | copies of the Software, and to permit persons to whom the Software is |
||
39 | furnished to do so, subject to the following conditions: |
||
40 | |||
41 | The above copyright notice and this permission notice shall be included in |
||
42 | all copies or substantial portions of the Software. |
||
43 | |||
44 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||
45 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||
46 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||
47 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||
48 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||
49 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
||
50 | THE SOFTWARE. |
||
51 | |||
52 | Copyright (c) 2003-2004 John Gruber |
||
53 | <http://daringfireball.net/> |
||
54 | All rights reserved. |
||
55 | |||
56 | Redistribution and use in source and binary forms, with or without |
||
57 | modification, are permitted provided that the following conditions are |
||
58 | met: |
||
59 | |||
60 | * Redistributions of source code must retain the above copyright notice, |
||
61 | this list of conditions and the following disclaimer. |
||
62 | |||
63 | * Redistributions in binary form must reproduce the above copyright |
||
64 | notice, this list of conditions and the following disclaimer in the |
||
65 | documentation and/or other materials provided with the distribution. |
||
66 | |||
67 | * Neither the name "Markdown" nor the names of its contributors may |
||
68 | be used to endorse or promote products derived from this software |
||
69 | without specific prior written permission. |
||
70 | |||
71 | This software is provided by the copyright holders and contributors "as |
||
72 | is" and any express or implied warranties, including, but not limited |
||
73 | to, the implied warranties of merchantability and fitness for a |
||
74 | particular purpose are disclaimed. In no event shall the copyright owner |
||
75 | or contributors be liable for any direct, indirect, incidental, special, |
||
76 | exemplary, or consequential damages (including, but not limited to, |
||
77 | procurement of substitute goods or services; loss of use, data, or |
||
78 | profits; or business interruption) however caused and on any theory of |
||
79 | liability, whether in contract, strict liability, or tort (including |
||
80 | negligence or otherwise) arising in any way out of the use of this |
||
81 | software, even if advised of the possibility of such damage. |
||
82 | */ |
||
83 | |||
84 | #endregion |
||
85 | |||
86 | using System; |
||
87 | using System.Collections.Generic; |
||
88 | using System.Configuration; |
||
89 | using System.Text; |
||
90 | using System.Text.RegularExpressions; |
||
91 | |||
92 | namespace MarkdownSharp |
||
93 | { |
||
94 | |||
95 | /// <summary> |
||
96 | /// |
||
97 | /// </summary> |
||
98 | public class MarkdownOptions |
||
99 | { |
||
100 | /// <summary> |
||
101 | /// when true, (most) bare plain URLs are auto-hyperlinked |
||
102 | /// WARNING: this is a significant deviation from the markdown spec |
||
103 | /// </summary> |
||
104 | public bool AutoHyperlink { get; set; } |
||
105 | /// <summary> |
||
106 | /// when true, RETURN becomes a literal newline |
||
107 | /// WARNING: this is a significant deviation from the markdown spec |
||
108 | /// </summary> |
||
109 | public bool AutoNewlines { get; set; } |
||
110 | /// <summary> |
||
111 | /// use ">" for HTML output, or " />" for XHTML output |
||
112 | /// </summary> |
||
113 | public string EmptyElementSuffix { get; set; } |
||
114 | /// <summary> |
||
115 | /// when true, problematic URL characters like [, ], (, and so forth will be encoded |
||
116 | /// WARNING: this is a significant deviation from the markdown spec |
||
117 | /// </summary> |
||
118 | public bool EncodeProblemUrlCharacters { get; set; } |
||
119 | /// <summary> |
||
120 | /// when false, email addresses will never be auto-linked |
||
121 | /// WARNING: this is a significant deviation from the markdown spec |
||
122 | /// </summary> |
||
123 | public bool LinkEmails { get; set; } |
||
124 | /// <summary> |
||
125 | /// when true, bold and italic require non-word characters on either side |
||
126 | /// WARNING: this is a significant deviation from the markdown spec |
||
127 | /// </summary> |
||
128 | public bool StrictBoldItalic { get; set; } |
||
129 | } |
||
130 | |||
131 | |||
132 | /// <summary> |
||
133 | /// Markdown is a text-to-HTML conversion tool for web writers. |
||
134 | /// Markdown allows you to write using an easy-to-read, easy-to-write plain text format, |
||
135 | /// then convert it to structurally valid XHTML (or HTML). |
||
136 | /// </summary> |
||
137 | public class Markdown |
||
138 | { |
||
139 | private const string _version = "1.13"; |
||
140 | |||
141 | #region Constructors and Options |
||
142 | |||
143 | /// <summary> |
||
144 | /// Create a new Markdown instance using default options |
||
145 | /// </summary> |
||
146 | public Markdown() |
||
147 | : this(false) |
||
148 | { |
||
149 | } |
||
150 | |||
151 | /// <summary> |
||
152 | /// Create a new Markdown instance and optionally load options from a configuration |
||
153 | /// file. There they should be stored in the appSettings section, available options are: |
||
154 | /// |
||
155 | /// Markdown.StrictBoldItalic (true/false) |
||
156 | /// Markdown.EmptyElementSuffix (">" or " />" without the quotes) |
||
157 | /// Markdown.LinkEmails (true/false) |
||
158 | /// Markdown.AutoNewLines (true/false) |
||
159 | /// Markdown.AutoHyperlink (true/false) |
||
160 | /// Markdown.EncodeProblemUrlCharacters (true/false) |
||
161 | /// |
||
162 | /// </summary> |
||
163 | public Markdown(bool loadOptionsFromConfigFile) |
||
164 | { |
||
165 | if (!loadOptionsFromConfigFile) return; |
||
166 | // |
||
167 | // var settings = ConfigurationManager.AppSettings; |
||
168 | // foreach (string key in settings.Keys) |
||
169 | // { |
||
170 | // switch (key) |
||
171 | // { |
||
172 | // case "Markdown.AutoHyperlink": |
||
173 | // _autoHyperlink = Convert.ToBoolean(settings[key]); |
||
174 | // break; |
||
175 | // case "Markdown.AutoNewlines": |
||
176 | // _autoNewlines = Convert.ToBoolean(settings[key]); |
||
177 | // break; |
||
178 | // case "Markdown.EmptyElementSuffix": |
||
179 | // _emptyElementSuffix = settings[key]; |
||
180 | // break; |
||
181 | // case "Markdown.EncodeProblemUrlCharacters": |
||
182 | // _encodeProblemUrlCharacters = Convert.ToBoolean(settings[key]); |
||
183 | // break; |
||
184 | // case "Markdown.LinkEmails": |
||
185 | // _linkEmails = Convert.ToBoolean(settings[key]); |
||
186 | // break; |
||
187 | // case "Markdown.StrictBoldItalic": |
||
188 | // _strictBoldItalic = Convert.ToBoolean(settings[key]); |
||
189 | // break; |
||
190 | // } |
||
191 | // } |
||
192 | } |
||
193 | |||
194 | /// <summary> |
||
195 | /// Create a new Markdown instance and set the options from the MarkdownOptions object. |
||
196 | /// </summary> |
||
197 | public Markdown(MarkdownOptions options) |
||
198 | { |
||
199 | _autoHyperlink = options.AutoHyperlink; |
||
200 | _autoNewlines = options.AutoNewlines; |
||
201 | _emptyElementSuffix = options.EmptyElementSuffix; |
||
202 | _encodeProblemUrlCharacters = options.EncodeProblemUrlCharacters; |
||
203 | _linkEmails = options.LinkEmails; |
||
204 | _strictBoldItalic = options.StrictBoldItalic; |
||
205 | } |
||
206 | |||
207 | |||
208 | /// <summary> |
||
209 | /// use ">" for HTML output, or " />" for XHTML output |
||
210 | /// </summary> |
||
211 | public string EmptyElementSuffix |
||
212 | { |
||
213 | get { return _emptyElementSuffix; } |
||
214 | set { _emptyElementSuffix = value; } |
||
215 | } |
||
216 | private string _emptyElementSuffix = " />"; |
||
217 | |||
218 | /// <summary> |
||
219 | /// when false, email addresses will never be auto-linked |
||
220 | /// WARNING: this is a significant deviation from the markdown spec |
||
221 | /// </summary> |
||
222 | public bool LinkEmails |
||
223 | { |
||
224 | get { return _linkEmails; } |
||
225 | set { _linkEmails = value; } |
||
226 | } |
||
227 | private bool _linkEmails = true; |
||
228 | |||
229 | /// <summary> |
||
230 | /// when true, bold and italic require non-word characters on either side |
||
231 | /// WARNING: this is a significant deviation from the markdown spec |
||
232 | /// </summary> |
||
233 | public bool StrictBoldItalic |
||
234 | { |
||
235 | get { return _strictBoldItalic; } |
||
236 | set { _strictBoldItalic = value; } |
||
237 | } |
||
238 | private bool _strictBoldItalic = false; |
||
239 | |||
240 | /// <summary> |
||
241 | /// when true, RETURN becomes a literal newline |
||
242 | /// WARNING: this is a significant deviation from the markdown spec |
||
243 | /// </summary> |
||
244 | public bool AutoNewLines |
||
245 | { |
||
246 | get { return _autoNewlines; } |
||
247 | set { _autoNewlines = value; } |
||
248 | } |
||
249 | private bool _autoNewlines = false; |
||
250 | |||
251 | /// <summary> |
||
252 | /// when true, (most) bare plain URLs are auto-hyperlinked |
||
253 | /// WARNING: this is a significant deviation from the markdown spec |
||
254 | /// </summary> |
||
255 | public bool AutoHyperlink |
||
256 | { |
||
257 | get { return _autoHyperlink; } |
||
258 | set { _autoHyperlink = value; } |
||
259 | } |
||
260 | private bool _autoHyperlink = false; |
||
261 | |||
262 | /// <summary> |
||
263 | /// when true, problematic URL characters like [, ], (, and so forth will be encoded |
||
264 | /// WARNING: this is a significant deviation from the markdown spec |
||
265 | /// </summary> |
||
266 | public bool EncodeProblemUrlCharacters |
||
267 | { |
||
268 | get { return _encodeProblemUrlCharacters; } |
||
269 | set { _encodeProblemUrlCharacters = value; } |
||
270 | } |
||
271 | private bool _encodeProblemUrlCharacters = false; |
||
272 | |||
273 | #endregion |
||
274 | |||
275 | private enum TokenType { Text, Tag } |
||
276 | |||
277 | private struct Token |
||
278 | { |
||
279 | public Token(TokenType type, string value) |
||
280 | { |
||
281 | this.Type = type; |
||
282 | this.Value = value; |
||
283 | } |
||
284 | public TokenType Type; |
||
285 | public string Value; |
||
286 | } |
||
287 | |||
288 | /// <summary> |
||
289 | /// maximum nested depth of [] and () supported by the transform; implementation detail |
||
290 | /// </summary> |
||
291 | private const int _nestDepth = 6; |
||
292 | |||
293 | /// <summary> |
||
294 | /// Tabs are automatically converted to spaces as part of the transform |
||
295 | /// this constant determines how "wide" those tabs become in spaces |
||
296 | /// </summary> |
||
297 | private const int _tabWidth = 4; |
||
298 | |||
299 | private const string _markerUL = @"[*+-]"; |
||
300 | private const string _markerOL = @"\d+[.]"; |
||
301 | |||
302 | private static readonly Dictionary<string, string> _escapeTable; |
||
303 | private static readonly Dictionary<string, string> _invertedEscapeTable; |
||
304 | private static readonly Dictionary<string, string> _backslashEscapeTable; |
||
305 | |||
306 | private readonly Dictionary<string, string> _urls = new Dictionary<string, string>(); |
||
307 | private readonly Dictionary<string, string> _titles = new Dictionary<string, string>(); |
||
308 | private readonly Dictionary<string, string> _htmlBlocks = new Dictionary<string, string>(); |
||
309 | |||
310 | private int _listLevel; |
||
311 | private static string AutoLinkPreventionMarker = "\x1AP"; // temporarily replaces "://" where auto-linking shouldn't happen; |
||
312 | |||
313 | /// <summary> |
||
314 | /// In the static constuctor we'll initialize what stays the same across all transforms. |
||
315 | /// </summary> |
||
316 | static Markdown() |
||
317 | { |
||
318 | // Table of hash values for escaped characters: |
||
319 | _escapeTable = new Dictionary<string, string>(); |
||
320 | _invertedEscapeTable = new Dictionary<string, string>(); |
||
321 | // Table of hash value for backslash escaped characters: |
||
322 | _backslashEscapeTable = new Dictionary<string, string>(); |
||
323 | |||
324 | string backslashPattern = ""; |
||
325 | |||
326 | foreach (char c in @"\`*_{}[]()>#+-.!/") |
||
327 | { |
||
328 | string key = c.ToString(); |
||
329 | string hash = GetHashKey(key, isHtmlBlock: false); |
||
330 | _escapeTable.Add(key, hash); |
||
331 | _invertedEscapeTable.Add(hash, key); |
||
332 | _backslashEscapeTable.Add(@"\" + key, hash); |
||
333 | backslashPattern += Regex.Escape(@"\" + key) + "|"; |
||
334 | } |
||
335 | |||
336 | _backslashEscapes = new Regex(backslashPattern.Substring(0, backslashPattern.Length - 1), RegexOptions.Compiled); |
||
337 | } |
||
338 | |||
339 | /// <summary> |
||
340 | /// current version of MarkdownSharp; |
||
341 | /// see http://code.google.com/p/markdownsharp/ for the latest code or to contribute |
||
342 | /// </summary> |
||
343 | public string Version |
||
344 | { |
||
345 | get { return _version; } |
||
346 | } |
||
347 | |||
348 | /// <summary> |
||
349 | /// Transforms the provided Markdown-formatted text to HTML; |
||
350 | /// see http://en.wikipedia.org/wiki/Markdown |
||
351 | /// </summary> |
||
352 | /// <remarks> |
||
353 | /// The order in which other subs are called here is |
||
354 | /// essential. Link and image substitutions need to happen before |
||
355 | /// EscapeSpecialChars(), so that any *'s or _'s in the a |
||
356 | /// and img tags get encoded. |
||
357 | /// </remarks> |
||
358 | public string Transform(string text) |
||
359 | { |
||
360 | if (String.IsNullOrEmpty(text)) return ""; |
||
361 | |||
362 | Setup(); |
||
363 | |||
364 | text = Normalize(text); |
||
365 | |||
366 | text = HashHTMLBlocks(text); |
||
367 | text = StripLinkDefinitions(text); |
||
368 | text = RunBlockGamut(text); |
||
369 | text = Unescape(text); |
||
370 | |||
371 | Cleanup(); |
||
372 | |||
373 | return text + "\n"; |
||
374 | } |
||
375 | |||
376 | |||
377 | /// <summary> |
||
378 | /// Perform transformations that form block-level tags like paragraphs, headers, and list items. |
||
379 | /// </summary> |
||
380 | private string RunBlockGamut(string text, bool unhash = true) |
||
381 | { |
||
382 | text = DoHeaders(text); |
||
383 | text = DoHorizontalRules(text); |
||
384 | text = DoLists(text); |
||
385 | text = DoCodeBlocks(text); |
||
386 | text = DoBlockQuotes(text); |
||
387 | |||
388 | // We already ran HashHTMLBlocks() before, in Markdown(), but that |
||
389 | // was to escape raw HTML in the original Markdown source. This time, |
||
390 | // we're escaping the markup we've just created, so that we don't wrap |
||
391 | // <p> tags around block-level tags. |
||
392 | text = HashHTMLBlocks(text); |
||
393 | |||
394 | text = FormParagraphs(text, unhash: unhash); |
||
395 | |||
396 | return text; |
||
397 | } |
||
398 | |||
399 | |||
400 | /// <summary> |
||
401 | /// Perform transformations that occur *within* block-level tags like paragraphs, headers, and list items. |
||
402 | /// </summary> |
||
403 | private string RunSpanGamut(string text) |
||
404 | { |
||
405 | text = DoCodeSpans(text); |
||
406 | text = EscapeSpecialCharsWithinTagAttributes(text); |
||
407 | text = EscapeBackslashes(text); |
||
408 | |||
409 | // Images must come first, because ![foo][f] looks like an anchor. |
||
410 | text = DoImages(text); |
||
411 | text = DoAnchors(text); |
||
412 | |||
413 | // Must come after DoAnchors(), because you can use < and > |
||
414 | // delimiters in inline links like [this](<url>). |
||
415 | text = DoAutoLinks(text); |
||
416 | |||
417 | text = text.Replace(AutoLinkPreventionMarker, "://"); |
||
418 | |||
419 | text = EncodeAmpsAndAngles(text); |
||
420 | text = DoItalicsAndBold(text); |
||
421 | text = DoHardBreaks(text); |
||
422 | |||
423 | return text; |
||
424 | } |
||
425 | |||
426 | private static Regex _newlinesLeadingTrailing = new Regex(@"^\n+|\n+\z", RegexOptions.Compiled); |
||
427 | private static Regex _newlinesMultiple = new Regex(@"\n{2,}", RegexOptions.Compiled); |
||
428 | private static Regex _leadingWhitespace = new Regex(@"^[ ]*", RegexOptions.Compiled); |
||
429 | |||
430 | private static Regex _htmlBlockHash = new Regex("\x1AH\\d+H", RegexOptions.Compiled); |
||
431 | |||
432 | /// <summary> |
||
433 | /// splits on two or more newlines, to form "paragraphs"; |
||
434 | /// each paragraph is then unhashed (if it is a hash and unhashing isn't turned off) or wrapped in HTML p tag |
||
435 | /// </summary> |
||
436 | private string FormParagraphs(string text, bool unhash = true) |
||
437 | { |
||
438 | // split on two or more newlines |
||
439 | string[] grafs = _newlinesMultiple.Split(_newlinesLeadingTrailing.Replace(text, "")); |
||
440 | |||
441 | for (int i = 0; i < grafs.Length; i++) |
||
442 | { |
||
443 | if (grafs[i].StartsWith("\x1AH")) |
||
444 | { |
||
445 | // unhashify HTML blocks |
||
446 | if (unhash) |
||
447 | { |
||
448 | int sanityCheck = 50; // just for safety, guard against an infinite loop |
||
449 | bool keepGoing = true; // as long as replacements where made, keep going |
||
450 | while (keepGoing && sanityCheck > 0) |
||
451 | { |
||
452 | keepGoing = false; |
||
453 | grafs[i] = _htmlBlockHash.Replace(grafs[i], match => |
||
454 | { |
||
455 | keepGoing = true; |
||
456 | return _htmlBlocks[match.Value]; |
||
457 | }); |
||
458 | sanityCheck--; |
||
459 | } |
||
460 | /* if (keepGoing) |
||
461 | { |
||
462 | // Logging of an infinite loop goes here. |
||
463 | // If such a thing should happen, please open a new issue on http://code.google.com/p/markdownsharp/ |
||
464 | // with the input that caused it. |
||
465 | }*/ |
||
466 | } |
||
467 | } |
||
468 | else |
||
469 | { |
||
470 | // do span level processing inside the block, then wrap result in <p> tags |
||
471 | grafs[i] = _leadingWhitespace.Replace(RunSpanGamut(grafs[i]), "<p>") + "</p>"; |
||
472 | } |
||
473 | } |
||
474 | |||
475 | return string.Join("\n\n", grafs); |
||
476 | } |
||
477 | |||
478 | |||
479 | private void Setup() |
||
480 | { |
||
481 | // Clear the global hashes. If we don't clear these, you get conflicts |
||
482 | // from other articles when generating a page which contains more than |
||
483 | // one article (e.g. an index page that shows the N most recent |
||
484 | // articles): |
||
485 | _urls.Clear(); |
||
486 | _titles.Clear(); |
||
487 | _htmlBlocks.Clear(); |
||
488 | _listLevel = 0; |
||
489 | } |
||
490 | |||
491 | private void Cleanup() |
||
492 | { |
||
493 | Setup(); |
||
494 | } |
||
495 | |||
496 | private static string _nestedBracketsPattern; |
||
497 | |||
498 | /// <summary> |
||
499 | /// Reusable pattern to match balanced [brackets]. See Friedl's |
||
500 | /// "Mastering Regular Expressions", 2nd Ed., pp. 328-331. |
||
501 | /// </summary> |
||
502 | private static string GetNestedBracketsPattern() |
||
503 | { |
||
504 | // in other words [this] and [this[also]] and [this[also[too]]] |
||
505 | // up to _nestDepth |
||
506 | if (_nestedBracketsPattern == null) |
||
507 | _nestedBracketsPattern = |
||
508 | RepeatString(@" |
||
509 | (?> # Atomic matching |
||
510 | [^\[\]]+ # Anything other than brackets |
||
511 | | |
||
512 | \[ |
||
513 | ", _nestDepth) + RepeatString( |
||
514 | @" \] |
||
515 | )*" |
||
516 | , _nestDepth); |
||
517 | return _nestedBracketsPattern; |
||
518 | } |
||
519 | |||
520 | private static string _nestedParensPattern; |
||
521 | |||
522 | /// <summary> |
||
523 | /// Reusable pattern to match balanced (parens). See Friedl's |
||
524 | /// "Mastering Regular Expressions", 2nd Ed., pp. 328-331. |
||
525 | /// </summary> |
||
526 | private static string GetNestedParensPattern() |
||
527 | { |
||
528 | // in other words (this) and (this(also)) and (this(also(too))) |
||
529 | // up to _nestDepth |
||
530 | if (_nestedParensPattern == null) |
||
531 | _nestedParensPattern = |
||
532 | RepeatString(@" |
||
533 | (?> # Atomic matching |
||
534 | [^()\s]+ # Anything other than parens or whitespace |
||
535 | | |
||
536 | \( |
||
537 | ", _nestDepth) + RepeatString( |
||
538 | @" \) |
||
539 | )*" |
||
540 | , _nestDepth); |
||
541 | return _nestedParensPattern; |
||
542 | } |
||
543 | |||
544 | private static Regex _linkDef = new Regex(string.Format(@" |
||
545 | ^[ ]{{0,{0}}}\[(.+)\]: # id = $1 |
||
546 | [ ]* |
||
547 | \n? # maybe *one* newline |
||
548 | [ ]* |
||
549 | <?(\S+?)>? # url = $2 |
||
550 | [ ]* |
||
551 | \n? # maybe one newline |
||
552 | [ ]* |
||
553 | (?: |
||
554 | (?<=\s) # lookbehind for whitespace |
||
555 | [""(] |
||
556 | (.+?) # title = $3 |
||
557 | ["")] |
||
558 | [ ]* |
||
559 | )? # title is optional |
||
560 | (?:\n+|\Z)", _tabWidth - 1), RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); |
||
561 | |||
562 | /// <summary> |
||
563 | /// Strips link definitions from text, stores the URLs and titles in hash references. |
||
564 | /// </summary> |
||
565 | /// <remarks> |
||
566 | /// ^[id]: url "optional title" |
||
567 | /// </remarks> |
||
568 | private string StripLinkDefinitions(string text) |
||
569 | { |
||
570 | return _linkDef.Replace(text, new MatchEvaluator(LinkEvaluator)); |
||
571 | } |
||
572 | |||
573 | private string LinkEvaluator(Match match) |
||
574 | { |
||
575 | string linkID = match.Groups[1].Value.ToLowerInvariant(); |
||
576 | _urls[linkID] = EncodeAmpsAndAngles(match.Groups[2].Value); |
||
577 | |||
578 | if (match.Groups[3] != null && match.Groups[3].Length > 0) |
||
579 | _titles[linkID] = match.Groups[3].Value.Replace("\"", """); |
||
580 | |||
581 | return ""; |
||
582 | } |
||
583 | |||
584 | // compiling this monster regex results in worse performance. trust me. |
||
585 | private static Regex _blocksHtml = new Regex(GetBlockPattern(), RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace); |
||
586 | |||
587 | |||
588 | /// <summary> |
||
589 | /// derived pretty much verbatim from PHP Markdown |
||
590 | /// </summary> |
||
591 | private static string GetBlockPattern() |
||
592 | { |
||
593 | |||
594 | // Hashify HTML blocks: |
||
595 | // We only want to do this for block-level HTML tags, such as headers, |
||
596 | // lists, and tables. That's because we still want to wrap <p>s around |
||
597 | // "paragraphs" that are wrapped in non-block-level tags, such as anchors, |
||
598 | // phrase emphasis, and spans. The list of tags we're looking for is |
||
599 | // hard-coded: |
||
600 | // |
||
601 | // * List "a" is made of tags which can be both inline or block-level. |
||
602 | // These will be treated block-level when the start tag is alone on |
||
603 | // its line, otherwise they're not matched here and will be taken as |
||
604 | // inline later. |
||
605 | // * List "b" is made of tags which are always block-level; |
||
606 | // |
||
607 | string blockTagsA = "ins|del"; |
||
608 | string blockTagsB = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|script|noscript|form|fieldset|iframe|math"; |
||
609 | |||
610 | // Regular expression for the content of a block tag. |
||
611 | string attr = @" |
||
612 | (?> # optional tag attributes |
||
613 | \s # starts with whitespace |
||
614 | (?> |
||
615 | [^>""/]+ # text outside quotes |
||
616 | | |
||
617 | /+(?!>) # slash not followed by > |
||
618 | | |
||
619 | ""[^""]*"" # text inside double quotes (tolerate >) |
||
620 | | |
||
621 | '[^']*' # text inside single quotes (tolerate >) |
||
622 | )* |
||
623 | )? |
||
624 | "; |
||
625 | |||
626 | string content = RepeatString(@" |
||
627 | (?> |
||
628 | [^<]+ # content without tag |
||
629 | | |
||
630 | <\2 # nested opening tag |
||
631 | " + attr + @" # attributes |
||
632 | (?> |
||
633 | /> |
||
634 | | |
||
635 | >", _nestDepth) + // end of opening tag |
||
636 | ".*?" + // last level nested tag content |
||
637 | RepeatString(@" |
||
638 | </\2\s*> # closing nested tag |
||
639 | ) |
||
640 | | |
||
641 | <(?!/\2\s*> # other tags with a different name |
||
642 | ) |
||
643 | )*", _nestDepth); |
||
644 | |||
645 | string content2 = content.Replace(@"\2", @"\3"); |
||
646 | |||
647 | // First, look for nested blocks, e.g.: |
||
648 | // <div> |
||
649 | // <div> |
||
650 | // tags for inner block must be indented. |
||
651 | // </div> |
||
652 | // </div> |
||
653 | // |
||
654 | // The outermost tags must start at the left margin for this to match, and |
||
655 | // the inner nested divs must be indented. |
||
656 | // We need to do this before the next, more liberal match, because the next |
||
657 | // match will start at the first `<div>` and stop at the first `</div>`. |
||
658 | string pattern = @" |
||
659 | (?> |
||
660 | (?> |
||
661 | (?<=\n) # Starting at the beginning of a line |
||
662 | | # or |
||
663 | \A\n? # the beginning of the doc |
||
664 | ) |
||
665 | ( # save in $1 |
||
666 | |||
667 | # Match from `\n<tag>` to `</tag>\n`, handling nested tags |
||
668 | # in between. |
||
669 | |||
670 | <($block_tags_b_re) # start tag = $2 |
||
671 | $attr> # attributes followed by > and \n |
||
672 | $content # content, support nesting |
||
673 | </\2> # the matching end tag |
||
674 | [ ]* # trailing spaces |
||
675 | (?=\n+|\Z) # followed by a newline or end of document |
||
676 | |||
677 | | # Special version for tags of group a. |
||
678 | |||
679 | <($block_tags_a_re) # start tag = $3 |
||
680 | $attr>[ ]*\n # attributes followed by > |
||
681 | $content2 # content, support nesting |
||
682 | </\3> # the matching end tag |
||
683 | [ ]* # trailing spaces |
||
684 | (?=\n+|\Z) # followed by a newline or end of document |
||
685 | |||
686 | | # Special case just for <hr />. It was easier to make a special |
||
687 | # case than to make the other regex more complicated. |
||
688 | |||
689 | [ ]{0,$less_than_tab} |
||
690 | <hr |
||
691 | $attr # attributes |
||
692 | /?> # the matching end tag |
||
693 | [ ]* |
||
694 | (?=\n{2,}|\Z) # followed by a blank line or end of document |
||
695 | |||
696 | | # Special case for standalone HTML comments: |
||
697 | |||
698 | (?<=\n\n|\A) # preceded by a blank line or start of document |
||
699 | [ ]{0,$less_than_tab} |
||
700 | (?s: |
||
701 | <!--(?:|(?:[^>-]|-[^>])(?:[^-]|-[^-])*)--> |
||
702 | ) |
||
703 | [ ]* |
||
704 | (?=\n{2,}|\Z) # followed by a blank line or end of document |
||
705 | |||
706 | | # PHP and ASP-style processor instructions (<? and <%) |
||
707 | |||
708 | [ ]{0,$less_than_tab} |
||
709 | (?s: |
||
710 | <([?%]) # $4 |
||
711 | .*? |
||
712 | \4> |
||
713 | ) |
||
714 | [ ]* |
||
715 | (?=\n{2,}|\Z) # followed by a blank line or end of document |
||
716 | |||
717 | ) |
||
718 | )"; |
||
719 | |||
720 | pattern = pattern.Replace("$less_than_tab", (_tabWidth - 1).ToString()); |
||
721 | pattern = pattern.Replace("$block_tags_b_re", blockTagsB); |
||
722 | pattern = pattern.Replace("$block_tags_a_re", blockTagsA); |
||
723 | pattern = pattern.Replace("$attr", attr); |
||
724 | pattern = pattern.Replace("$content2", content2); |
||
725 | pattern = pattern.Replace("$content", content); |
||
726 | |||
727 | return pattern; |
||
728 | } |
||
729 | |||
730 | /// <summary> |
||
731 | /// replaces any block-level HTML blocks with hash entries |
||
732 | /// </summary> |
||
733 | private string HashHTMLBlocks(string text) |
||
734 | { |
||
735 | return _blocksHtml.Replace(text, new MatchEvaluator(HtmlEvaluator)); |
||
736 | } |
||
737 | |||
738 | private string HtmlEvaluator(Match match) |
||
739 | { |
||
740 | string text = match.Groups[1].Value; |
||
741 | string key = GetHashKey(text, isHtmlBlock: true); |
||
742 | _htmlBlocks[key] = text; |
||
743 | |||
744 | return string.Concat("\n\n", key, "\n\n"); |
||
745 | } |
||
746 | |||
747 | private static string GetHashKey(string s, bool isHtmlBlock) |
||
748 | { |
||
749 | var delim = isHtmlBlock ? 'H' : 'E'; |
||
750 | return "\x1A" + delim + Math.Abs(s.GetHashCode()).ToString() + delim; |
||
751 | } |
||
752 | |||
753 | private static Regex _htmlTokens = new Regex(@" |
||
754 | (<!--(?:|(?:[^>-]|-[^>])(?:[^-]|-[^-])*)-->)| # match <!-- foo --> |
||
755 | (<\?.*?\?>)| # match <?foo?> " + |
||
756 | RepeatString(@" |
||
757 | (<[A-Za-z\/!$](?:[^<>]|", _nestDepth) + RepeatString(@")*>)", _nestDepth) + |
||
758 | " # match <tag> and </tag>", |
||
759 | RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.ExplicitCapture | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); |
||
760 | |||
761 | /// <summary> |
||
762 | /// returns an array of HTML tokens comprising the input string. Each token is |
||
763 | /// either a tag (possibly with nested, tags contained therein, such |
||
764 | /// as <a href="<MTFoo>">, or a run of text between tags. Each element of the |
||
765 | /// array is a two-element array; the first is either 'tag' or 'text'; the second is |
||
766 | /// the actual value. |
||
767 | /// </summary> |
||
768 | private List<Token> TokenizeHTML(string text) |
||
769 | { |
||
770 | int pos = 0; |
||
771 | int tagStart = 0; |
||
772 | var tokens = new List<Token>(); |
||
773 | |||
774 | // this regex is derived from the _tokenize() subroutine in Brad Choate's MTRegex plugin. |
||
775 | // http://www.bradchoate.com/past/mtregex.php |
||
776 | foreach (Match m in _htmlTokens.Matches(text)) |
||
777 | { |
||
778 | tagStart = m.Index; |
||
779 | |||
780 | if (pos < tagStart) |
||
781 | tokens.Add(new Token(TokenType.Text, text.Substring(pos, tagStart - pos))); |
||
782 | |||
783 | tokens.Add(new Token(TokenType.Tag, m.Value)); |
||
784 | pos = tagStart + m.Length; |
||
785 | } |
||
786 | |||
787 | if (pos < text.Length) |
||
788 | tokens.Add(new Token(TokenType.Text, text.Substring(pos, text.Length - pos))); |
||
789 | |||
790 | return tokens; |
||
791 | } |
||
792 | |||
793 | |||
794 | private static Regex _anchorRef = new Regex(string.Format(@" |
||
795 | ( # wrap whole match in $1 |
||
796 | \[ |
||
797 | ({0}) # link text = $2 |
||
798 | \] |
||
799 | |||
800 | [ ]? # one optional space |
||
801 | (?:\n[ ]*)? # one optional newline followed by spaces |
||
802 | |||
803 | \[ |
||
804 | (.*?) # id = $3 |
||
805 | \] |
||
806 | )", GetNestedBracketsPattern()), RegexOptions.Singleline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); |
||
807 | |||
808 | private static Regex _anchorInline = new Regex(string.Format(@" |
||
809 | ( # wrap whole match in $1 |
||
810 | \[ |
||
811 | ({0}) # link text = $2 |
||
812 | \] |
||
813 | \( # literal paren |
||
814 | [ ]* |
||
815 | ({1}) # href = $3 |
||
816 | [ ]* |
||
817 | ( # $4 |
||
818 | (['""]) # quote char = $5 |
||
819 | (.*?) # title = $6 |
||
820 | \5 # matching quote |
||
821 | [ ]* # ignore any spaces between closing quote and ) |
||
822 | )? # title is optional |
||
823 | \) |
||
824 | )", GetNestedBracketsPattern(), GetNestedParensPattern()), |
||
825 | RegexOptions.Singleline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); |
||
826 | |||
827 | private static Regex _anchorRefShortcut = new Regex(@" |
||
828 | ( # wrap whole match in $1 |
||
829 | \[ |
||
830 | ([^\[\]]+) # link text = $2; can't contain [ or ] |
||
831 | \] |
||
832 | )", RegexOptions.Singleline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); |
||
833 | |||
834 | /// <summary> |
||
835 | /// Turn Markdown link shortcuts into HTML anchor tags |
||
836 | /// </summary> |
||
837 | /// <remarks> |
||
838 | /// [link text](url "title") |
||
839 | /// [link text][id] |
||
840 | /// [id] |
||
841 | /// </remarks> |
||
842 | private string DoAnchors(string text) |
||
843 | { |
||
844 | // First, handle reference-style links: [link text] [id] |
||
845 | text = _anchorRef.Replace(text, new MatchEvaluator(AnchorRefEvaluator)); |
||
846 | |||
847 | // Next, inline-style links: [link text](url "optional title") or [link text](url "optional title") |
||
848 | text = _anchorInline.Replace(text, new MatchEvaluator(AnchorInlineEvaluator)); |
||
849 | |||
850 | // Last, handle reference-style shortcuts: [link text] |
||
851 | // These must come last in case you've also got [link test][1] |
||
852 | // or [link test](/foo) |
||
853 | text = _anchorRefShortcut.Replace(text, new MatchEvaluator(AnchorRefShortcutEvaluator)); |
||
854 | return text; |
||
855 | } |
||
856 | |||
857 | private string SaveFromAutoLinking(string s) |
||
858 | { |
||
859 | return s.Replace("://", AutoLinkPreventionMarker); |
||
860 | } |
||
861 | |||
862 | private string AnchorRefEvaluator(Match match) |
||
863 | { |
||
864 | string wholeMatch = match.Groups[1].Value; |
||
865 | string linkText = SaveFromAutoLinking(match.Groups[2].Value); |
||
866 | string linkID = match.Groups[3].Value.ToLowerInvariant(); |
||
867 | |||
868 | string result; |
||
869 | |||
870 | // for shortcut links like [this][]. |
||
871 | if (linkID == "") |
||
872 | linkID = linkText.ToLowerInvariant(); |
||
873 | |||
874 | if (_urls.ContainsKey(linkID)) |
||
875 | { |
||
876 | string url = _urls[linkID]; |
||
877 | |||
878 | url = EncodeProblemUrlChars(url); |
||
879 | url = EscapeBoldItalic(url); |
||
880 | result = "<a href=\"" + url + "\""; |
||
881 | |||
882 | if (_titles.ContainsKey(linkID)) |
||
883 | { |
||
884 | string title = AttributeEncode(_titles[linkID]); |
||
885 | title = AttributeEncode(EscapeBoldItalic(title)); |
||
886 | result += " title=\"" + title + "\""; |
||
887 | } |
||
888 | |||
889 | result += ">" + linkText + "</a>"; |
||
890 | } |
||
891 | else |
||
892 | result = wholeMatch; |
||
893 | |||
894 | return result; |
||
895 | } |
||
896 | |||
897 | private string AnchorRefShortcutEvaluator(Match match) |
||
898 | { |
||
899 | string wholeMatch = match.Groups[1].Value; |
||
900 | string linkText = SaveFromAutoLinking(match.Groups[2].Value); |
||
901 | string linkID = Regex.Replace(linkText.ToLowerInvariant(), @"[ ]*\n[ ]*", " "); // lower case and remove newlines / extra spaces |
||
902 | |||
903 | string result; |
||
904 | |||
905 | if (_urls.ContainsKey(linkID)) |
||
906 | { |
||
907 | string url = _urls[linkID]; |
||
908 | |||
909 | url = EncodeProblemUrlChars(url); |
||
910 | url = EscapeBoldItalic(url); |
||
911 | result = "<a href=\"" + url + "\""; |
||
912 | |||
913 | if (_titles.ContainsKey(linkID)) |
||
914 | { |
||
915 | string title = AttributeEncode(_titles[linkID]); |
||
916 | title = EscapeBoldItalic(title); |
||
917 | result += " title=\"" + title + "\""; |
||
918 | } |
||
919 | |||
920 | result += ">" + linkText + "</a>"; |
||
921 | } |
||
922 | else |
||
923 | result = wholeMatch; |
||
924 | |||
925 | return result; |
||
926 | } |
||
927 | |||
928 | |||
929 | private string AnchorInlineEvaluator(Match match) |
||
930 | { |
||
931 | string linkText = SaveFromAutoLinking(match.Groups[2].Value); |
||
932 | string url = match.Groups[3].Value; |
||
933 | string title = match.Groups[6].Value; |
||
934 | string result; |
||
935 | |||
936 | url = EncodeProblemUrlChars(url); |
||
937 | url = EscapeBoldItalic(url); |
||
938 | if (url.StartsWith("<") && url.EndsWith(">")) |
||
939 | url = url.Substring(1, url.Length - 2); // remove <>'s surrounding URL, if present |
||
940 | |||
941 | result = string.Format("<a href=\"{0}\"", url); |
||
942 | |||
943 | if (!String.IsNullOrEmpty(title)) |
||
944 | { |
||
945 | title = AttributeEncode(title); |
||
946 | title = EscapeBoldItalic(title); |
||
947 | result += string.Format(" title=\"{0}\"", title); |
||
948 | } |
||
949 | |||
950 | result += string.Format(">{0}</a>", linkText); |
||
951 | return result; |
||
952 | } |
||
953 | |||
954 | private static Regex _imagesRef = new Regex(@" |
||
955 | ( # wrap whole match in $1 |
||
956 | !\[ |
||
957 | (.*?) # alt text = $2 |
||
958 | \] |
||
959 | |||
960 | [ ]? # one optional space |
||
961 | (?:\n[ ]*)? # one optional newline followed by spaces |
||
962 | |||
963 | \[ |
||
964 | (.*?) # id = $3 |
||
965 | \] |
||
966 | |||
967 | )", RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline | RegexOptions.Compiled); |
||
968 | |||
969 | private static Regex _imagesInline = new Regex(String.Format(@" |
||
970 | ( # wrap whole match in $1 |
||
971 | !\[ |
||
972 | (.*?) # alt text = $2 |
||
973 | \] |
||
974 | \s? # one optional whitespace character |
||
975 | \( # literal paren |
||
976 | [ ]* |
||
977 | ({0}) # href = $3 |
||
978 | [ ]* |
||
979 | ( # $4 |
||
980 | (['""]) # quote char = $5 |
||
981 | (.*?) # title = $6 |
||
982 | \5 # matching quote |
||
983 | [ ]* |
||
984 | )? # title is optional |
||
985 | \) |
||
986 | )", GetNestedParensPattern()), |
||
987 | RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline | RegexOptions.Compiled); |
||
988 | |||
989 | /// <summary> |
||
990 | /// Turn Markdown image shortcuts into HTML img tags. |
||
991 | /// </summary> |
||
992 | /// <remarks> |
||
993 | /// ![alt text][id] |
||
994 | /// ![alt text](url "optional title") |
||
995 | /// </remarks> |
||
996 | private string DoImages(string text) |
||
997 | { |
||
998 | // First, handle reference-style labeled images: ![alt text][id] |
||
999 | text = _imagesRef.Replace(text, new MatchEvaluator(ImageReferenceEvaluator)); |
||
1000 | |||
1001 | // Next, handle inline images: ![alt text](url "optional title") |
||
1002 | // Don't forget: encode * and _ |
||
1003 | text = _imagesInline.Replace(text, new MatchEvaluator(ImageInlineEvaluator)); |
||
1004 | |||
1005 | return text; |
||
1006 | } |
||
1007 | |||
1008 | // This prevents the creation of horribly broken HTML when some syntax ambiguities |
||
1009 | // collide. It likely still doesn't do what the user meant, but at least we're not |
||
1010 | // outputting garbage. |
||
1011 | private string EscapeImageAltText(string s) |
||
1012 | { |
||
1013 | s = EscapeBoldItalic(s); |
||
1014 | s = Regex.Replace(s, @"[\[\]()]", m => _escapeTable[m.ToString()]); |
||
1015 | return s; |
||
1016 | } |
||
1017 | |||
1018 | private string ImageReferenceEvaluator(Match match) |
||
1019 | { |
||
1020 | string wholeMatch = match.Groups[1].Value; |
||
1021 | string altText = match.Groups[2].Value; |
||
1022 | string linkID = match.Groups[3].Value.ToLowerInvariant(); |
||
1023 | |||
1024 | // for shortcut links like ![this][]. |
||
1025 | if (linkID == "") |
||
1026 | linkID = altText.ToLowerInvariant(); |
||
1027 | |||
1028 | if (_urls.ContainsKey(linkID)) |
||
1029 | { |
||
1030 | string url = _urls[linkID]; |
||
1031 | string title = null; |
||
1032 | |||
1033 | if (_titles.ContainsKey(linkID)) |
||
1034 | title = _titles[linkID]; |
||
1035 | |||
1036 | return ImageTag(url, altText, title); |
||
1037 | } |
||
1038 | else |
||
1039 | { |
||
1040 | // If there's no such link ID, leave intact: |
||
1041 | return wholeMatch; |
||
1042 | } |
||
1043 | } |
||
1044 | |||
1045 | private string ImageInlineEvaluator(Match match) |
||
1046 | { |
||
1047 | string alt = match.Groups[2].Value; |
||
1048 | string url = match.Groups[3].Value; |
||
1049 | string title = match.Groups[6].Value; |
||
1050 | |||
1051 | if (url.StartsWith("<") && url.EndsWith(">")) |
||
1052 | url = url.Substring(1, url.Length - 2); // Remove <>'s surrounding URL, if present |
||
1053 | |||
1054 | return ImageTag(url, alt, title); |
||
1055 | } |
||
1056 | |||
1057 | private string ImageTag(string url, string altText, string title) |
||
1058 | { |
||
1059 | altText = EscapeImageAltText(AttributeEncode(altText)); |
||
1060 | url = EncodeProblemUrlChars(url); |
||
1061 | url = EscapeBoldItalic(url); |
||
1062 | var result = string.Format("<img src=\"{0}\" alt=\"{1}\"", url, altText); |
||
1063 | if (!String.IsNullOrEmpty(title)) |
||
1064 | { |
||
1065 | title = AttributeEncode(EscapeBoldItalic(title)); |
||
1066 | result += string.Format(" title=\"{0}\"", title); |
||
1067 | } |
||
1068 | result += _emptyElementSuffix; |
||
1069 | return result; |
||
1070 | } |
||
1071 | |||
1072 | private static Regex _headerSetext = new Regex(@" |
||
1073 | ^(.+?) |
||
1074 | [ ]* |
||
1075 | \n |
||
1076 | (=+|-+) # $1 = string of ='s or -'s |
||
1077 | [ ]* |
||
1078 | \n+", |
||
1079 | RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); |
||
1080 | |||
1081 | private static Regex _headerAtx = new Regex(@" |
||
1082 | ^(\#{1,6}) # $1 = string of #'s |
||
1083 | [ ]* |
||
1084 | (.+?) # $2 = Header text |
||
1085 | [ ]* |
||
1086 | \#* # optional closing #'s (not counted) |
||
1087 | \n+", |
||
1088 | RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); |
||
1089 | |||
1090 | /// <summary> |
||
1091 | /// Turn Markdown headers into HTML header tags |
||
1092 | /// </summary> |
||
1093 | /// <remarks> |
||
1094 | /// Header 1 |
||
1095 | /// ======== |
||
1096 | /// |
||
1097 | /// Header 2 |
||
1098 | /// -------- |
||
1099 | /// |
||
1100 | /// # Header 1 |
||
1101 | /// ## Header 2 |
||
1102 | /// ## Header 2 with closing hashes ## |
||
1103 | /// ... |
||
1104 | /// ###### Header 6 |
||
1105 | /// </remarks> |
||
1106 | private string DoHeaders(string text) |
||
1107 | { |
||
1108 | text = _headerSetext.Replace(text, new MatchEvaluator(SetextHeaderEvaluator)); |
||
1109 | text = _headerAtx.Replace(text, new MatchEvaluator(AtxHeaderEvaluator)); |
||
1110 | return text; |
||
1111 | } |
||
1112 | |||
1113 | private string SetextHeaderEvaluator(Match match) |
||
1114 | { |
||
1115 | string header = match.Groups[1].Value; |
||
1116 | int level = match.Groups[2].Value.StartsWith("=") ? 1 : 2; |
||
1117 | return string.Format("<h{1}>{0}</h{1}>\n\n", RunSpanGamut(header), level); |
||
1118 | } |
||
1119 | |||
1120 | private string AtxHeaderEvaluator(Match match) |
||
1121 | { |
||
1122 | string header = match.Groups[2].Value; |
||
1123 | int level = match.Groups[1].Value.Length; |
||
1124 | return string.Format("<h{1}>{0}</h{1}>\n\n", RunSpanGamut(header), level); |
||
1125 | } |
||
1126 | |||
1127 | |||
1128 | private static Regex _horizontalRules = new Regex(@" |
||
1129 | ^[ ]{0,3} # Leading space |
||
1130 | ([-*_]) # $1: First marker |
||
1131 | (?> # Repeated marker group |
||
1132 | [ ]{0,2} # Zero, one, or two spaces. |
||
1133 | \1 # Marker character |
||
1134 | ){2,} # Group repeated at least twice |
||
1135 | [ ]* # Trailing spaces |
||
1136 | $ # End of line. |
||
1137 | ", RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); |
||
1138 | |||
1139 | /// <summary> |
||
1140 | /// Turn Markdown horizontal rules into HTML hr tags |
||
1141 | /// </summary> |
||
1142 | /// <remarks> |
||
1143 | /// *** |
||
1144 | /// * * * |
||
1145 | /// --- |
||
1146 | /// - - - |
||
1147 | /// </remarks> |
||
1148 | private string DoHorizontalRules(string text) |
||
1149 | { |
||
1150 | return _horizontalRules.Replace(text, "<hr" + _emptyElementSuffix + "\n"); |
||
1151 | } |
||
1152 | |||
1153 | private static string _wholeList = string.Format(@" |
||
1154 | ( # $1 = whole list |
||
1155 | ( # $2 |
||
1156 | [ ]{{0,{1}}} |
||
1157 | ({0}) # $3 = first list item marker |
||
1158 | [ ]+ |
||
1159 | ) |
||
1160 | (?s:.+?) |
||
1161 | ( # $4 |
||
1162 | \z |
||
1163 | | |
||
1164 | \n{{2,}} |
||
1165 | (?=\S) |
||
1166 | (?! # Negative lookahead for another list item marker |
||
1167 | [ ]* |
||
1168 | {0}[ ]+ |
||
1169 | ) |
||
1170 | ) |
||
1171 | )", string.Format("(?:{0}|{1})", _markerUL, _markerOL), _tabWidth - 1); |
||
1172 | |||
1173 | private static Regex _listNested = new Regex(@"^" + _wholeList, |
||
1174 | RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); |
||
1175 | |||
1176 | private static Regex _listTopLevel = new Regex(@"(?:(?<=\n\n)|\A\n?)" + _wholeList, |
||
1177 | RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); |
||
1178 | |||
1179 | /// <summary> |
||
1180 | /// Turn Markdown lists into HTML ul and ol and li tags |
||
1181 | /// </summary> |
||
1182 | private string DoLists(string text) |
||
1183 | { |
||
1184 | // We use a different prefix before nested lists than top-level lists. |
||
1185 | // See extended comment in _ProcessListItems(). |
||
1186 | if (_listLevel > 0) |
||
1187 | text = _listNested.Replace(text, new MatchEvaluator(ListEvaluator)); |
||
1188 | else |
||
1189 | text = _listTopLevel.Replace(text, new MatchEvaluator(ListEvaluator)); |
||
1190 | |||
1191 | return text; |
||
1192 | } |
||
1193 | |||
1194 | private string ListEvaluator(Match match) |
||
1195 | { |
||
1196 | string list = match.Groups[1].Value; |
||
1197 | string listType = Regex.IsMatch(match.Groups[3].Value, _markerUL) ? "ul" : "ol"; |
||
1198 | string result; |
||
1199 | |||
1200 | result = ProcessListItems(list, listType == "ul" ? _markerUL : _markerOL); |
||
1201 | |||
1202 | result = string.Format("<{0}>\n{1}</{0}>\n", listType, result); |
||
1203 | return result; |
||
1204 | } |
||
1205 | |||
1206 | /// <summary> |
||
1207 | /// Process the contents of a single ordered or unordered list, splitting it |
||
1208 | /// into individual list items. |
||
1209 | /// </summary> |
||
1210 | private string ProcessListItems(string list, string marker) |
||
1211 | { |
||
1212 | // The listLevel global keeps track of when we're inside a list. |
||
1213 | // Each time we enter a list, we increment it; when we leave a list, |
||
1214 | // we decrement. If it's zero, we're not in a list anymore. |
||
1215 | |||
1216 | // We do this because when we're not inside a list, we want to treat |
||
1217 | // something like this: |
||
1218 | |||
1219 | // I recommend upgrading to version |
||
1220 | // 8. Oops, now this line is treated |
||
1221 | // as a sub-list. |
||
1222 | |||
1223 | // As a single paragraph, despite the fact that the second line starts |
||
1224 | // with a digit-period-space sequence. |
||
1225 | |||
1226 | // Whereas when we're inside a list (or sub-list), that line will be |
||
1227 | // treated as the start of a sub-list. What a kludge, huh? This is |
||
1228 | // an aspect of Markdown's syntax that's hard to parse perfectly |
||
1229 | // without resorting to mind-reading. Perhaps the solution is to |
||
1230 | // change the syntax rules such that sub-lists must start with a |
||
1231 | // starting cardinal number; e.g. "1." or "a.". |
||
1232 | |||
1233 | _listLevel++; |
||
1234 | |||
1235 | // Trim trailing blank lines: |
||
1236 | list = Regex.Replace(list, @"\n{2,}\z", "\n"); |
||
1237 | |||
1238 | string pattern = string.Format( |
||
1239 | @"(^[ ]*) # leading whitespace = $1 |
||
1240 | ({0}) [ ]+ # list marker = $2 |
||
1241 | ((?s:.+?) # list item text = $3 |
||
1242 | (\n+)) |
||
1243 | (?= (\z | \1 ({0}) [ ]+))", marker); |
||
1244 | |||
1245 | bool lastItemHadADoubleNewline = false; |
||
1246 | |||
1247 | // has to be a closure, so subsequent invocations can share the bool |
||
1248 | MatchEvaluator ListItemEvaluator = (Match match) => |
||
1249 | { |
||
1250 | string item = match.Groups[3].Value; |
||
1251 | |||
1252 | bool endsWithDoubleNewline = item.EndsWith("\n\n"); |
||
1253 | bool containsDoubleNewline = endsWithDoubleNewline || item.Contains("\n\n"); |
||
1254 | |||
1255 | if (containsDoubleNewline || lastItemHadADoubleNewline) |
||
1256 | // we could correct any bad indentation here.. |
||
1257 | item = RunBlockGamut(Outdent(item) + "\n", unhash: false); |
||
1258 | else |
||
1259 | { |
||
1260 | // recursion for sub-lists |
||
1261 | item = DoLists(Outdent(item)); |
||
1262 | item = item.TrimEnd('\n'); |
||
1263 | item = RunSpanGamut(item); |
||
1264 | } |
||
1265 | lastItemHadADoubleNewline = endsWithDoubleNewline; |
||
1266 | return string.Format("<li>{0}</li>\n", item); |
||
1267 | }; |
||
1268 | |||
1269 | list = Regex.Replace(list, pattern, new MatchEvaluator(ListItemEvaluator), |
||
1270 | RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline); |
||
1271 | _listLevel--; |
||
1272 | return list; |
||
1273 | } |
||
1274 | |||
1275 | private static Regex _codeBlock = new Regex(string.Format(@" |
||
1276 | (?:\n\n|\A\n?) |
||
1277 | ( # $1 = the code block -- one or more lines, starting with a space |
||
1278 | (?: |
||
1279 | (?:[ ]{{{0}}}) # Lines must start with a tab-width of spaces |
||
1280 | .*\n+ |
||
1281 | )+ |
||
1282 | ) |
||
1283 | ((?=^[ ]{{0,{0}}}[^ \t\n])|\Z) # Lookahead for non-space at line-start, or end of doc", |
||
1284 | _tabWidth), RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); |
||
1285 | |||
1286 | /// <summary> |
||
1287 | /// /// Turn Markdown 4-space indented code into HTML pre code blocks |
||
1288 | /// </summary> |
||
1289 | private string DoCodeBlocks(string text) |
||
1290 | { |
||
1291 | text = _codeBlock.Replace(text, new MatchEvaluator(CodeBlockEvaluator)); |
||
1292 | return text; |
||
1293 | } |
||
1294 | |||
1295 | private string CodeBlockEvaluator(Match match) |
||
1296 | { |
||
1297 | string codeBlock = match.Groups[1].Value; |
||
1298 | |||
1299 | codeBlock = EncodeCode(Outdent(codeBlock)); |
||
1300 | codeBlock = _newlinesLeadingTrailing.Replace(codeBlock, ""); |
||
1301 | |||
1302 | return string.Concat("\n\n<pre><code>", codeBlock, "\n</code></pre>\n\n"); |
||
1303 | } |
||
1304 | |||
1305 | private static Regex _codeSpan = new Regex(@" |
||
1306 | (?<!\\) # Character before opening ` can't be a backslash |
||
1307 | (`+) # $1 = Opening run of ` |
||
1308 | (.+?) # $2 = The code block |
||
1309 | (?<!`) |
||
1310 | \1 |
||
1311 | (?!`)", RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline | RegexOptions.Compiled); |
||
1312 | |||
1313 | /// <summary> |
||
1314 | /// Turn Markdown `code spans` into HTML code tags |
||
1315 | /// </summary> |
||
1316 | private string DoCodeSpans(string text) |
||
1317 | { |
||
1318 | // * You can use multiple backticks as the delimiters if you want to |
||
1319 | // include literal backticks in the code span. So, this input: |
||
1320 | // |
||
1321 | // Just type ``foo `bar` baz`` at the prompt. |
||
1322 | // |
||
1323 | // Will translate to: |
||
1324 | // |
||
1325 | // <p>Just type <code>foo `bar` baz</code> at the prompt.</p> |
||
1326 | // |
||
1327 | // There's no arbitrary limit to the number of backticks you |
||
1328 | // can use as delimters. If you need three consecutive backticks |
||
1329 | // in your code, use four for delimiters, etc. |
||
1330 | // |
||
1331 | // * You can use spaces to get literal backticks at the edges: |
||
1332 | // |
||
1333 | // ... type `` `bar` `` ... |
||
1334 | // |
||
1335 | // Turns to: |
||
1336 | // |
||
1337 | // ... type <code>`bar`</code> ... |
||
1338 | // |
||
1339 | |||
1340 | return _codeSpan.Replace(text, new MatchEvaluator(CodeSpanEvaluator)); |
||
1341 | } |
||
1342 | |||
1343 | private string CodeSpanEvaluator(Match match) |
||
1344 | { |
||
1345 | string span = match.Groups[2].Value; |
||
1346 | span = Regex.Replace(span, @"^[ ]*", ""); // leading whitespace |
||
1347 | span = Regex.Replace(span, @"[ ]*$", ""); // trailing whitespace |
||
1348 | span = EncodeCode(span); |
||
1349 | span = SaveFromAutoLinking(span); // to prevent auto-linking. Not necessary in code *blocks*, but in code spans. |
||
1350 | |||
1351 | return string.Concat("<code>", span, "</code>"); |
||
1352 | } |
||
1353 | |||
1354 | |||
1355 | private static Regex _bold = new Regex(@"(\*\*|__) (?=\S) (.+?[*_]*) (?<=\S) \1", |
||
1356 | RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline | RegexOptions.Compiled); |
||
1357 | private static Regex _strictBold = new Regex(@"([\W_]|^) (\*\*|__) (?=\S) ([^\r]*?\S[\*_]*) \2 ([\W_]|$)", |
||
1358 | RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline | RegexOptions.Compiled); |
||
1359 | |||
1360 | private static Regex _italic = new Regex(@"(\*|_) (?=\S) (.+?) (?<=\S) \1", |
||
1361 | RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline | RegexOptions.Compiled); |
||
1362 | private static Regex _strictItalic = new Regex(@"([\W_]|^) (\*|_) (?=\S) ([^\r\*_]*?\S) \2 ([\W_]|$)", |
||
1363 | RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline | RegexOptions.Compiled); |
||
1364 | |||
1365 | /// <summary> |
||
1366 | /// Turn Markdown *italics* and **bold** into HTML strong and em tags |
||
1367 | /// </summary> |
||
1368 | private string DoItalicsAndBold(string text) |
||
1369 | { |
||
1370 | |||
1371 | // <strong> must go first, then <em> |
||
1372 | if (_strictBoldItalic) |
||
1373 | { |
||
1374 | text = _strictBold.Replace(text, "$1<strong>$3</strong>$4"); |
||
1375 | text = _strictItalic.Replace(text, "$1<em>$3</em>$4"); |
||
1376 | } |
||
1377 | else |
||
1378 | { |
||
1379 | text = _bold.Replace(text, "<strong>$2</strong>"); |
||
1380 | text = _italic.Replace(text, "<em>$2</em>"); |
||
1381 | } |
||
1382 | return text; |
||
1383 | } |
||
1384 | |||
1385 | /// <summary> |
||
1386 | /// Turn markdown line breaks (two space at end of line) into HTML break tags |
||
1387 | /// </summary> |
||
1388 | private string DoHardBreaks(string text) |
||
1389 | { |
||
1390 | if (_autoNewlines) |
||
1391 | text = Regex.Replace(text, @"\n", string.Format("<br{0}\n", _emptyElementSuffix)); |
||
1392 | else |
||
1393 | text = Regex.Replace(text, @" {2,}\n", string.Format("<br{0}\n", _emptyElementSuffix)); |
||
1394 | return text; |
||
1395 | } |
||
1396 | |||
1397 | private static Regex _blockquote = new Regex(@" |
||
1398 | ( # Wrap whole match in $1 |
||
1399 | ( |
||
1400 | ^[ ]*>[ ]? # '>' at the start of a line |
||
1401 | .+\n # rest of the first line |
||
1402 | (.+\n)* # subsequent consecutive lines |
||
1403 | \n* # blanks |
||
1404 | )+ |
||
1405 | )", RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline | RegexOptions.Compiled); |
||
1406 | |||
1407 | /// <summary> |
||
1408 | /// Turn Markdown > quoted blocks into HTML blockquote blocks |
||
1409 | /// </summary> |
||
1410 | private string DoBlockQuotes(string text) |
||
1411 | { |
||
1412 | return _blockquote.Replace(text, new MatchEvaluator(BlockQuoteEvaluator)); |
||
1413 | } |
||
1414 | |||
1415 | private string BlockQuoteEvaluator(Match match) |
||
1416 | { |
||
1417 | string bq = match.Groups[1].Value; |
||
1418 | |||
1419 | bq = Regex.Replace(bq, @"^[ ]*>[ ]?", "", RegexOptions.Multiline); // trim one level of quoting |
||
1420 | bq = Regex.Replace(bq, @"^[ ]+$", "", RegexOptions.Multiline); // trim whitespace-only lines |
||
1421 | bq = RunBlockGamut(bq); // recurse |
||
1422 | |||
1423 | bq = Regex.Replace(bq, @"^", " ", RegexOptions.Multiline); |
||
1424 | |||
1425 | // These leading spaces screw with <pre> content, so we need to fix that: |
||
1426 | bq = Regex.Replace(bq, @"(\s*<pre>.+?</pre>)", new MatchEvaluator(BlockQuoteEvaluator2), RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline); |
||
1427 | |||
1428 | bq = string.Format("<blockquote>\n{0}\n</blockquote>", bq); |
||
1429 | string key = GetHashKey(bq, isHtmlBlock: true); |
||
1430 | _htmlBlocks[key] = bq; |
||
1431 | |||
1432 | return "\n\n" + key + "\n\n"; |
||
1433 | } |
||
1434 | |||
1435 | private string BlockQuoteEvaluator2(Match match) |
||
1436 | { |
||
1437 | return Regex.Replace(match.Groups[1].Value, @"^ ", "", RegexOptions.Multiline); |
||
1438 | } |
||
1439 | |||
1440 | private static Regex _autolinkBare = new Regex(@"(<|="")?\b(https?|ftp)(://[-A-Z0-9+&@#/%?=~_|\[\]\(\)!:,\.;]*[-A-Z0-9+&@#/%=~_|\[\])])(?=$|\W)", |
||
1441 | RegexOptions.IgnoreCase | RegexOptions.Compiled); |
||
1442 | |||
1443 | private static string handleTrailingParens(Match match) |
||
1444 | { |
||
1445 | // The first group is essentially a negative lookbehind -- if there's a < or a =", we don't touch this. |
||
1446 | // We're not using a *real* lookbehind, because of links with in links, like <a href="http://web.archive.org/web/20121130000728/http://www.google.com/"> |
||
1447 | // With a real lookbehind, the full link would never be matched, and thus the http://www.google.com *would* be matched. |
||
1448 | // With the simulated lookbehind, the full link *is* matched (just not handled, because of this early return), causing |
||
1449 | // the google link to not be matched again. |
||
1450 | if (match.Groups[1].Success) |
||
1451 | return match.Value; |
||
1452 | |||
1453 | var protocol = match.Groups[2].Value; |
||
1454 | var link = match.Groups[3].Value; |
||
1455 | if (!link.EndsWith(")")) |
||
1456 | return "<" + protocol + link + ">"; |
||
1457 | var level = 0; |
||
1458 | foreach (Match c in Regex.Matches(link, "[()]")) |
||
1459 | { |
||
1460 | if (c.Value == "(") |
||
1461 | { |
||
1462 | if (level <= 0) |
||
1463 | level = 1; |
||
1464 | else |
||
1465 | level++; |
||
1466 | } |
||
1467 | else |
||
1468 | { |
||
1469 | level--; |
||
1470 | } |
||
1471 | } |
||
1472 | var tail = ""; |
||
1473 | if (level < 0) |
||
1474 | { |
||
1475 | link = Regex.Replace(link, @"\){1," + (-level) + "}$", m => { tail = m.Value; return ""; }); |
||
1476 | } |
||
1477 | return "<" + protocol + link + ">" + tail; |
||
1478 | } |
||
1479 | |||
1480 | /// <summary> |
||
1481 | /// Turn angle-delimited URLs into HTML anchor tags |
||
1482 | /// </summary> |
||
1483 | /// <remarks> |
||
1484 | /// <http://www.example.com> |
||
1485 | /// </remarks> |
||
1486 | private string DoAutoLinks(string text) |
||
1487 | { |
||
1488 | |||
1489 | if (_autoHyperlink) |
||
1490 | { |
||
1491 | // fixup arbitrary URLs by adding Markdown < > so they get linked as well |
||
1492 | // note that at this point, all other URL in the text are already hyperlinked as <a href=""></a> |
||
1493 | // *except* for the <http://www.foo.com> case |
||
1494 | text = _autolinkBare.Replace(text, handleTrailingParens); |
||
1495 | } |
||
1496 | |||
1497 | // Hyperlinks: <http://foo.com> |
||
1498 | text = Regex.Replace(text, "<((https?|ftp):[^'\">\\s]+)>", new MatchEvaluator(HyperlinkEvaluator)); |
||
1499 | |||
1500 | if (_linkEmails) |
||
1501 | { |
||
1502 | // Email addresses: <address@domain.foo> |
||
1503 | string pattern = |
||
1504 | @"< |
||
1505 | (?:mailto:)? |
||
1506 | ( |
||
1507 | [-.\w]+ |
||
1508 | \@ |
||
1509 | [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+ |
||
1510 | ) |
||
1511 | >"; |
||
1512 | text = Regex.Replace(text, pattern, new MatchEvaluator(EmailEvaluator), RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); |
||
1513 | } |
||
1514 | |||
1515 | return text; |
||
1516 | } |
||
1517 | |||
1518 | private string HyperlinkEvaluator(Match match) |
||
1519 | { |
||
1520 | string link = match.Groups[1].Value; |
||
1521 | return string.Format("<a href=\"{0}\">{0}</a>", link); |
||
1522 | } |
||
1523 | |||
1524 | private string EmailEvaluator(Match match) |
||
1525 | { |
||
1526 | string email = Unescape(match.Groups[1].Value); |
||
1527 | |||
1528 | // |
||
1529 | // Input: an email address, e.g. "foo@example.com" |
||
1530 | // |
||
1531 | // Output: the email address as a mailto link, with each character |
||
1532 | // of the address encoded as either a decimal or hex entity, in |
||
1533 | // the hopes of foiling most address harvesting spam bots. E.g.: |
||
1534 | // |
||
1535 | // <a href="mailto:foo@e |
||
1536 | // xample.com">foo |
||
1537 | // @example.com</a> |
||
1538 | // |
||
1539 | // Based by a filter by Matthew Wickline, posted to the BBEdit-Talk |
||
1540 | // mailing list: <http://tinyurl.com/yu7ue> |
||
1541 | // |
||
1542 | email = "mailto:" + email; |
||
1543 | |||
1544 | // leave ':' alone (to spot mailto: later) |
||
1545 | email = EncodeEmailAddress(email); |
||
1546 | |||
1547 | email = string.Format("<a href=\"{0}\">{0}</a>", email); |
||
1548 | |||
1549 | // strip the mailto: from the visible part |
||
1550 | email = Regex.Replace(email, "\">.+?:", "\">"); |
||
1551 | return email; |
||
1552 | } |
||
1553 | |||
1554 | |||
1555 | private static Regex _outDent = new Regex(@"^[ ]{1," + _tabWidth + @"}", RegexOptions.Multiline | RegexOptions.Compiled); |
||
1556 | |||
1557 | /// <summary> |
||
1558 | /// Remove one level of line-leading spaces |
||
1559 | /// </summary> |
||
1560 | private string Outdent(string block) |
||
1561 | { |
||
1562 | return _outDent.Replace(block, ""); |
||
1563 | } |
||
1564 | |||
1565 | |||
1566 | #region Encoding and Normalization |
||
1567 | |||
1568 | |||
1569 | /// <summary> |
||
1570 | /// encodes email address randomly |
||
1571 | /// roughly 10% raw, 45% hex, 45% dec |
||
1572 | /// note that @ is always encoded and : never is |
||
1573 | /// </summary> |
||
1574 | private string EncodeEmailAddress(string addr) |
||
1575 | { |
||
1576 | var sb = new StringBuilder(addr.Length * 5); |
||
1577 | var rand = new Random(); |
||
1578 | int r; |
||
1579 | foreach (char c in addr) |
||
1580 | { |
||
1581 | r = rand.Next(1, 100); |
||
1582 | if ((r > 90 || c == ':') && c != '@') |
||
1583 | sb.Append(c); // m |
||
1584 | else if (r < 45) |
||
1585 | sb.AppendFormat("&#x{0:x};", (int)c); // m |
||
1586 | else |
||
1587 | sb.AppendFormat("&#{0};", (int)c); // m |
||
1588 | } |
||
1589 | return sb.ToString(); |
||
1590 | } |
||
1591 | |||
1592 | private static Regex _codeEncoder = new Regex(@"&|<|>|\\|\*|_|\{|\}|\[|\]", RegexOptions.Compiled); |
||
1593 | |||
1594 | /// <summary> |
||
1595 | /// Encode/escape certain Markdown characters inside code blocks and spans where they are literals |
||
1596 | /// </summary> |
||
1597 | private string EncodeCode(string code) |
||
1598 | { |
||
1599 | return _codeEncoder.Replace(code, EncodeCodeEvaluator); |
||
1600 | } |
||
1601 | private string EncodeCodeEvaluator(Match match) |
||
1602 | { |
||
1603 | switch (match.Value) |
||
1604 | { |
||
1605 | // Encode all ampersands; HTML entities are not |
||
1606 | // entities within a Markdown code span. |
||
1607 | case "&": |
||
1608 | return "&"; |
||
1609 | // Do the angle bracket song and dance |
||
1610 | case "<": |
||
1611 | return "<"; |
||
1612 | case ">": |
||
1613 | return ">"; |
||
1614 | // escape characters that are magic in Markdown |
||
1615 | default: |
||
1616 | return _escapeTable[match.Value]; |
||
1617 | } |
||
1618 | } |
||
1619 | |||
1620 | |||
1621 | private static Regex _amps = new Regex(@"&(?!((#[0-9]+)|(#[xX][a-fA-F0-9]+)|([a-zA-Z][a-zA-Z0-9]*));)", RegexOptions.ExplicitCapture | RegexOptions.Compiled); |
||
1622 | private static Regex _angles = new Regex(@"<(?![A-Za-z/?\$!])", RegexOptions.ExplicitCapture | RegexOptions.Compiled); |
||
1623 | |||
1624 | /// <summary> |
||
1625 | /// Encode any ampersands (that aren't part of an HTML entity) and left or right angle brackets |
||
1626 | /// </summary> |
||
1627 | private string EncodeAmpsAndAngles(string s) |
||
1628 | { |
||
1629 | s = _amps.Replace(s, "&"); |
||
1630 | s = _angles.Replace(s, "<"); |
||
1631 | return s; |
||
1632 | } |
||
1633 | |||
1634 | private static Regex _backslashEscapes; |
||
1635 | |||
1636 | /// <summary> |
||
1637 | /// Encodes any escaped characters such as \`, \*, \[ etc |
||
1638 | /// </summary> |
||
1639 | private string EscapeBackslashes(string s) |
||
1640 | { |
||
1641 | return _backslashEscapes.Replace(s, new MatchEvaluator(EscapeBackslashesEvaluator)); |
||
1642 | } |
||
1643 | private string EscapeBackslashesEvaluator(Match match) |
||
1644 | { |
||
1645 | return _backslashEscapeTable[match.Value]; |
||
1646 | } |
||
1647 | |||
1648 | private static Regex _unescapes = new Regex("\x1A" + "E\\d+E", RegexOptions.Compiled); |
||
1649 | |||
1650 | /// <summary> |
||
1651 | /// swap back in all the special characters we've hidden |
||
1652 | /// </summary> |
||
1653 | private string Unescape(string s) |
||
1654 | { |
||
1655 | return _unescapes.Replace(s, new MatchEvaluator(UnescapeEvaluator)); |
||
1656 | } |
||
1657 | private string UnescapeEvaluator(Match match) |
||
1658 | { |
||
1659 | return _invertedEscapeTable[match.Value]; |
||
1660 | } |
||
1661 | |||
1662 | |||
1663 | /// <summary> |
||
1664 | /// escapes Bold [ * ] and Italic [ _ ] characters |
||
1665 | /// </summary> |
||
1666 | private string EscapeBoldItalic(string s) |
||
1667 | { |
||
1668 | s = s.Replace("*", _escapeTable["*"]); |
||
1669 | s = s.Replace("_", _escapeTable["_"]); |
||
1670 | return s; |
||
1671 | } |
||
1672 | |||
1673 | private static string AttributeEncode(string s) |
||
1674 | { |
||
1675 | return s.Replace(">", ">").Replace("<", "<").Replace("\"", """); |
||
1676 | } |
||
1677 | |||
1678 | private static char[] _problemUrlChars = @"""'*()[]$:".ToCharArray(); |
||
1679 | |||
1680 | /// <summary> |
||
1681 | /// hex-encodes some unusual "problem" chars in URLs to avoid URL detection problems |
||
1682 | /// </summary> |
||
1683 | private string EncodeProblemUrlChars(string url) |
||
1684 | { |
||
1685 | if (!_encodeProblemUrlCharacters) return url; |
||
1686 | |||
1687 | var sb = new StringBuilder(url.Length); |
||
1688 | bool encode; |
||
1689 | char c; |
||
1690 | |||
1691 | for (int i = 0; i < url.Length; i++) |
||
1692 | { |
||
1693 | c = url[i]; |
||
1694 | encode = Array.IndexOf(_problemUrlChars, c) != -1; |
||
1695 | if (encode && c == ':' && i < url.Length - 1) |
||
1696 | encode = !(url[i + 1] == '/') && !(url[i + 1] >= '0' && url[i + 1] <= '9'); |
||
1697 | |||
1698 | if (encode) |
||
1699 | sb.Append("%" + String.Format("{0:x}", (byte)c)); |
||
1700 | else |
||
1701 | sb.Append(c); |
||
1702 | } |
||
1703 | |||
1704 | return sb.ToString(); |
||
1705 | } |
||
1706 | |||
1707 | |||
1708 | /// <summary> |
||
1709 | /// Within tags -- meaning between < and > -- encode [\ ` * _] so they |
||
1710 | /// don't conflict with their use in Markdown for code, italics and strong. |
||
1711 | /// We're replacing each such character with its corresponding hash |
||
1712 | /// value; this is likely overkill, but it should prevent us from colliding |
||
1713 | /// with the escape values by accident. |
||
1714 | /// </summary> |
||
1715 | private string EscapeSpecialCharsWithinTagAttributes(string text) |
||
1716 | { |
||
1717 | var tokens = TokenizeHTML(text); |
||
1718 | |||
1719 | // now, rebuild text from the tokens |
||
1720 | var sb = new StringBuilder(text.Length); |
||
1721 | |||
1722 | foreach (var token in tokens) |
||
1723 | { |
||
1724 | string value = token.Value; |
||
1725 | |||
1726 | if (token.Type == TokenType.Tag) |
||
1727 | { |
||
1728 | value = value.Replace(@"\", _escapeTable[@"\"]); |
||
1729 | |||
1730 | if (_autoHyperlink && value.StartsWith("<!")) // escape slashes in comments to prevent autolinking there -- http://meta.stackoverflow.com/questions/95987/html-comment-containing-url-breaks-if-followed-by-another-html-comment |
||
1731 | value = value.Replace("/", _escapeTable["/"]); |
||
1732 | |||
1733 | value = Regex.Replace(value, "(?<=.)</?code>(?=.)", _escapeTable[@"`"]); |
||
1734 | value = EscapeBoldItalic(value); |
||
1735 | } |
||
1736 | |||
1737 | sb.Append(value); |
||
1738 | } |
||
1739 | |||
1740 | return sb.ToString(); |
||
1741 | } |
||
1742 | |||
1743 | /// <summary> |
||
1744 | /// convert all tabs to _tabWidth spaces; |
||
1745 | /// standardizes line endings from DOS (CR LF) or Mac (CR) to UNIX (LF); |
||
1746 | /// makes sure text ends with a couple of newlines; |
||
1747 | /// removes any blank lines (only spaces) in the text |
||
1748 | /// </summary> |
||
1749 | private string Normalize(string text) |
||
1750 | { |
||
1751 | var output = new StringBuilder(text.Length); |
||
1752 | var line = new StringBuilder(); |
||
1753 | bool valid = false; |
||
1754 | |||
1755 | for (int i = 0; i < text.Length; i++) |
||
1756 | { |
||
1757 | switch (text[i]) |
||
1758 | { |
||
1759 | case '\n': |
||
1760 | if (valid) output.Append(line); |
||
1761 | output.Append('\n'); |
||
1762 | line.Length = 0; valid = false; |
||
1763 | break; |
||
1764 | case '\r': |
||
1765 | if ((i < text.Length - 1) && (text[i + 1] != '\n')) |
||
1766 | { |
||
1767 | if (valid) output.Append(line); |
||
1768 | output.Append('\n'); |
||
1769 | line.Length = 0; valid = false; |
||
1770 | } |
||
1771 | break; |
||
1772 | case '\t': |
||
1773 | int width = (_tabWidth - line.Length % _tabWidth); |
||
1774 | for (int k = 0; k < width; k++) |
||
1775 | line.Append(' '); |
||
1776 | break; |
||
1777 | case '\x1A': |
||
1778 | break; |
||
1779 | default: |
||
1780 | if (!valid && text[i] != ' ') valid = true; |
||
1781 | line.Append(text[i]); |
||
1782 | break; |
||
1783 | } |
||
1784 | } |
||
1785 | |||
1786 | if (valid) output.Append(line); |
||
1787 | output.Append('\n'); |
||
1788 | |||
1789 | // add two newlines to the end before return |
||
1790 | return output.Append("\n\n").ToString(); |
||
1791 | } |
||
1792 | |||
1793 | #endregion |
||
1794 | |||
1795 | /// <summary> |
||
1796 | /// this is to emulate what's evailable in PHP |
||
1797 | /// </summary> |
||
1798 | private static string RepeatString(string text, int count) |
||
1799 | { |
||
1800 | var sb = new StringBuilder(text.Length * count); |
||
1801 | for (int i = 0; i < count; i++) |
||
1802 | sb.Append(text); |
||
1803 | return sb.ToString(); |
||
1804 | } |
||
1805 | |||
1806 | } |
||
1807 | } |