From ac8b7934d731dd2ba78dc68fa77dc0ab8666e647 Mon Sep 17 00:00:00 2001
From: Elivo <actell@yeah.net>
Date: Tue, 12 Jan 2021 23:39:35 +0800
Subject: [PATCH] =?UTF-8?q?Apewer-6.1.0=EF=BC=9AWebAPI=20=E6=96=B0?=
 =?UTF-8?q?=E5=A2=9E=E5=8A=9F=E8=83=BD=EF=BC=9BTextUtility=20=E6=96=B0?=
 =?UTF-8?q?=E5=A2=9E=E6=94=AF=E6=8C=81=20MD=E3=80=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 Apewer/Apewer.csproj                          |   26 +-
 Apewer/DateTimeUtility.cs                     |    3 +
 Apewer/Externals/MarkdownSharp-1.13/Demo.cs   |   20 +
 Apewer/Externals/MarkdownSharp-1.13/Info.cs   |   84 +
 .../Externals/MarkdownSharp-1.13/Markdown.cs  | 1656 +++++++++++++++++
 .../MarkdownSharp-1.13/MarkdownOptions.cs     |   45 +
 Apewer/Externals/MarkdownSharp-1.13/Token.cs  |   28 +
 Apewer/Internals/Constant.cs                  |    3 +
 Apewer/Json.cs                                |   45 +
 Apewer/Models/StringPairs.cs                  |    2 +-
 Apewer/NetworkUtility.cs                      |    4 +-
 Apewer/Source/ColumnAttribute.cs              |   54 +-
 Apewer/Source/MySql.cs                        |  128 +-
 Apewer/Source/Record.cs                       |    6 +-
 Apewer/Source/TableAttribute.cs               |   22 +-
 Apewer/Source/TableStructure.cs               |  148 +-
 Apewer/TextUtility.cs                         |   16 +-
 Apewer/Web/ApiInternals.cs                    |  176 +-
 Apewer/Web/ApiInvoker.cs                      |  195 +-
 Apewer/Web/ApiRequest.cs                      |    3 +
 Apewer/Web/ApiResponse.cs                     |    9 +-
 Apewer/Web/WebUtility.cs                      |  108 +-
 Apewer/_ChangeLog.md                          |   14 +
 Apewer/_Class.cs                              |   12 +-
 Apewer/_Extensions.cs                         |    7 +-
 25 files changed, 2593 insertions(+), 221 deletions(-)
 create mode 100644 Apewer/Externals/MarkdownSharp-1.13/Demo.cs
 create mode 100644 Apewer/Externals/MarkdownSharp-1.13/Info.cs
 create mode 100644 Apewer/Externals/MarkdownSharp-1.13/Markdown.cs
 create mode 100644 Apewer/Externals/MarkdownSharp-1.13/MarkdownOptions.cs
 create mode 100644 Apewer/Externals/MarkdownSharp-1.13/Token.cs

diff --git a/Apewer/Apewer.csproj b/Apewer/Apewer.csproj
index 8385b24..10fa249 100644
--- a/Apewer/Apewer.csproj
+++ b/Apewer/Apewer.csproj
@@ -2,15 +2,15 @@
 
   <!--<Import Sdk="Microsoft.NET.Sdk" Project="Sdk.props" />-->
 
-  <!-- Info -->
+  <!-- Info: Package -->
   <PropertyGroup>
     <AssemblyName>Apewer</AssemblyName>
     <PackageId>Apewer</PackageId>
     <Title>Apewer</Title>
-    <Version>6.0.14</Version>
+    <Version>6.1.0</Version>
   </PropertyGroup>
 
-  <!-- Info -->
+  <!-- Info: Copyright -->
   <PropertyGroup>
     <Authors>Elivo</Authors>
     <Company>Apewer Lab</Company>
@@ -21,7 +21,7 @@
     <Product>Apewer Libraries</Product>
   </PropertyGroup>
 
-  <!-- Info -->
+  <!-- Info: Build -->
   <PropertyGroup>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
     <DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
@@ -30,19 +30,19 @@
     <OutputType>Library</OutputType>
   </PropertyGroup>
 
-  <!-- NuGet -->
+  <!-- Info: NuGet -->
   <PropertyGroup Condition="'$(Configuration)'=='Release'">
     <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
     <IsPackable>true</IsPackable>
   </PropertyGroup>
 
-  <!-- For .NET Standard 2.1 -->
+  <!-- .NET Standard 2.1 -->
   <PropertyGroup Condition="'$(TargetFramework)'=='netstandard2.1'">
     <AssemblyTitle>$(AssemblyName) - .NET Standard 2.1</AssemblyTitle>
     <DefineConstants>NETSTD;HAVE_ASYNC;HAVE_BIG_INTEGER;$(AdditionalConstants)</DefineConstants>
   </PropertyGroup>
 
-  <!-- For .NET Framework 2.0 -->
+  <!-- .NET Framework 2.0 -->
   <PropertyGroup Condition="'$(TargetFramework)'=='net20'">
     <AssemblyTitle>$(AssemblyName) - .NET Framework 2.0</AssemblyTitle>
     <DefineConstants>NETFX;NET20;$(AdditionalConstants)</DefineConstants>
@@ -50,7 +50,6 @@
   <ItemGroup Condition="'$(TargetFramework)'=='net20'">
     <Reference Include="CustomMarshalers" />
     <Reference Include="System.Configuration" />
-    <Reference Include="System.Configuration.Install" />
     <Reference Include="System.Design" />
     <Reference Include="System.Management" />
     <Reference Include="System.Security" />
@@ -59,7 +58,7 @@
     <Reference Include="System.Windows.Forms" />
   </ItemGroup>
 
-  <!-- For .NET Framework 4.0 -->
+  <!-- .NET Framework 4.0 -->
   <PropertyGroup Condition="'$(TargetFramework)'=='net40'">
     <AssemblyTitle>$(AssemblyName) - .NET Framework 4.0</AssemblyTitle>
     <DefineConstants>NETFX;NET40;HAVE_BIG_INTEGER;$(AdditionalConstants)</DefineConstants>
@@ -70,7 +69,6 @@
     <Reference Include="PresentationCore" />
     <Reference Include="System.Core" />
     <Reference Include="System.Configuration" />
-    <Reference Include="System.Configuration.Install" />
     <Reference Include="System.Design" />
     <Reference Include="System.Drawing" />
     <Reference Include="System.Management" />
@@ -83,7 +81,7 @@
     <Reference Include="WindowsBase" />
   </ItemGroup>
 
-  <!-- For .NET Framework 4.6.1 -->
+  <!-- .NET Framework 4.6.1 -->
   <PropertyGroup Condition="'$(TargetFramework)'=='net461'">
     <AssemblyTitle>$(AssemblyName) - .NET Framework 4.6.1</AssemblyTitle>
     <DefineConstants>NETFX;NET461;NET45;HAVE_ASYNC;HAVE_BIG_INTEGER;$(AdditionalConstants)</DefineConstants>
@@ -94,7 +92,6 @@
     <Reference Include="PresentationCore" />
     <Reference Include="System.Core" />
     <Reference Include="System.Configuration" />
-    <Reference Include="System.Configuration.Install" />
     <Reference Include="System.Design" />
     <Reference Include="System.Drawing" />
     <Reference Include="System.Management" />
@@ -107,7 +104,7 @@
     <Reference Include="WindowsBase" />
   </ItemGroup>
 
-  <!-- For .NET Core 3.1 -->
+  <!-- .NET Core 3.1 -->
   <PropertyGroup Condition="'$(TargetFramework)'=='netcoreapp3.1'">
     <AssemblyTitle>$(AssemblyName) - .NET Core 3.1</AssemblyTitle>
     <DefineConstants>NETCORE;HAVE_ASYNC;HAVE_BIG_INTEGER;$(AdditionalConstants)</DefineConstants>
@@ -115,9 +112,6 @@
     <!--<UseWindowsForms>true</UseWindowsForms>-->
   </PropertyGroup>
   <ItemGroup Condition="'$(TargetFramework)'=='netcoreapp3.1'">
-    <!-- Condition="'$(DisableImplicitFrameworkReferences)' != 'true' And '$(TargetFrameworkIdentifier)' == '.NETCoreApp' And '$(_TargetFrameworkVersionWithoutV)' &gt;= '3.0'" -->
-    <!-- IsImplicitlyDefined="true" -->
-    <!-- Condition="('$(UseWPF)' == 'true') And ('$(UseWindowsForms)' == 'true')" -->
     <FrameworkReference Include="Microsoft.AspNetCore.App" />
     <FrameworkReference Include="Microsoft.WindowsDesktop.App" />
   </ItemGroup>
diff --git a/Apewer/DateTimeUtility.cs b/Apewer/DateTimeUtility.cs
index 25ba2ce..933c44f 100644
--- a/Apewer/DateTimeUtility.cs
+++ b/Apewer/DateTimeUtility.cs
@@ -276,6 +276,9 @@ namespace Apewer
             return new Nullable<DateTime>(entity);
         }
 
+        /// <summary>创建新的零值 DateTime 对象。</summary>
+        public static DateTime Zero() => new DateTime(0L, DateTimeKind.Utc);
+
         #endregion
 
     }
diff --git a/Apewer/Externals/MarkdownSharp-1.13/Demo.cs b/Apewer/Externals/MarkdownSharp-1.13/Demo.cs
new file mode 100644
index 0000000..8b44e0e
--- /dev/null
+++ b/Apewer/Externals/MarkdownSharp-1.13/Demo.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MarkdownSharp
+{
+
+    internal class Demo
+    {
+
+        public static string ToHtml(string markdown)
+        {
+            var processor = new Markdown();
+            var html = processor.Transform(markdown);
+            return html;
+        }
+
+    }
+
+}
diff --git a/Apewer/Externals/MarkdownSharp-1.13/Info.cs b/Apewer/Externals/MarkdownSharp-1.13/Info.cs
new file mode 100644
index 0000000..cee53e2
--- /dev/null
+++ b/Apewer/Externals/MarkdownSharp-1.13/Info.cs
@@ -0,0 +1,84 @@
+/*
+ * MarkdownSharp
+ * -------------
+ * a C# Markdown processors
+ * 
+ * Markdown is a text-to-HTML conversion tool for web writers
+ * Copyright (c) 2004 John Gruber
+ * https://daringfireball.net/projects/markdown/
+ * 
+ * Markdown.NET
+ * Copyright (c) 2004-2009 Milan Negovan
+ * https://www.aspnetresources.com
+ * https://aspnetresources.com/blog/markdown_announced.aspx
+ * 
+ * MarkdownSharp
+ * Copyright (c) 2009-2011 Jeff Atwood
+ * https://stackoverflow.com
+ * https://blog.codinghorror.com
+ * https://github.com/StackExchange/MarkdownSharp
+ * 
+ * History: Milan ported the Markdown processor to C#. He granted license to me so I can open source it
+ * and let the community contribute to and improve MarkdownSharp.
+ * 
+ */
+
+#region Copyright and license
+
+/*
+
+Copyright (c) 2009 - 2010 Jeff Atwood
+
+http://www.opensource.org/licenses/mit-license.php
+  
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+Copyright (c) 2003-2004 John Gruber
+<http://daringfireball.net/>   
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright notice,
+  this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+  notice, this list of conditions and the following disclaimer in the
+  documentation and/or other materials provided with the distribution.
+
+* Neither the name "Markdown" nor the names of its contributors may
+  be used to endorse or promote products derived from this software
+  without specific prior written permission.
+
+This software is provided by the copyright holders and contributors "as
+is" and any express or implied warranties, including, but not limited
+to, the implied warranties of merchantability and fitness for a
+particular purpose are disclaimed. In no event shall the copyright owner
+or contributors be liable for any direct, indirect, incidental, special,
+exemplary, or consequential damages (including, but not limited to,
+procurement of substitute goods or services; loss of use, data, or
+profits; or business interruption) however caused and on any theory of
+liability, whether in contract, strict liability, or tort (including
+negligence or otherwise) arising in any way out of the use of this
+software, even if advised of the possibility of such damage.
+*/
+
+#endregion
\ No newline at end of file
diff --git a/Apewer/Externals/MarkdownSharp-1.13/Markdown.cs b/Apewer/Externals/MarkdownSharp-1.13/Markdown.cs
new file mode 100644
index 0000000..58165c3
--- /dev/null
+++ b/Apewer/Externals/MarkdownSharp-1.13/Markdown.cs
@@ -0,0 +1,1656 @@
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace MarkdownSharp
+{
+
+    /// <summary>
+    /// Markdown is a text-to-HTML conversion tool for web writers. 
+    /// Markdown allows you to write using an easy-to-read, easy-to-write plain text format, 
+    /// then convert it to structurally valid XHTML (or HTML).
+    /// </summary>
+    internal class Markdown
+    {
+        private const string _version = "1.13";
+
+        #region Constructors and Options
+
+        /// <summary>
+        /// Create a new Markdown instance using default options
+        /// </summary>
+        public Markdown()
+        {
+        }
+
+#if NETFX || NETCORE
+
+        /// <summary>
+        /// Create a new Markdown instance and optionally load options from a configuration
+        /// file. There they should be stored in the appSettings section, available options are:
+        ///     Markdown.StrictBoldItalic (true/false)
+        ///     Markdown.EmptyElementSuffix (">" or " />" without the quotes)
+        ///     Markdown.LinkEmails (true/false)
+        ///     Markdown.AutoNewLines (true/false)
+        ///     Markdown.AutoHyperlink (true/false)
+        ///     Markdown.AsteriskIntraWordEmphasis (true/false)
+        /// </summary>
+        public Markdown(bool loadOptionsFromConfigFile)
+        {
+            if (!loadOptionsFromConfigFile) return;
+
+            var settings = ConfigurationManager.AppSettings;
+            foreach (string key in settings.Keys)
+            {
+                switch (key)
+                {
+                    case "Markdown.AutoHyperlink":
+                        AutoHyperlink = Convert.ToBoolean(settings[key]);
+                        break;
+                    case "Markdown.AutoNewlines":
+                        AutoNewLines = Convert.ToBoolean(settings[key]);
+                        break;
+                    case "Markdown.EmptyElementSuffix":
+                        EmptyElementSuffix = settings[key];
+                        break;
+                    case "Markdown.LinkEmails":
+                        LinkEmails = Convert.ToBoolean(settings[key]);
+                        break;
+                    case "Markdown.StrictBoldItalic":
+                        StrictBoldItalic = Convert.ToBoolean(settings[key]);
+                        break;
+                    case "Markdown.AsteriskIntraWordEmphasis":
+                        AsteriskIntraWordEmphasis = Convert.ToBoolean(settings[key]);
+                        break;
+                }
+            }
+        }
+
+#endif
+
+        /// <summary>
+        /// Create a new Markdown instance and set the options from the MarkdownOptions object.
+        /// </summary>
+        public Markdown(MarkdownOptions options)
+        {
+            AutoHyperlink = options.AutoHyperlink;
+            AutoNewLines = options.AutoNewlines;
+            if (!string.IsNullOrEmpty(options.EmptyElementSuffix))
+                EmptyElementSuffix = options.EmptyElementSuffix;
+            LinkEmails = options.LinkEmails;
+            StrictBoldItalic = options.StrictBoldItalic;
+            AsteriskIntraWordEmphasis = options.AsteriskIntraWordEmphasis;
+        }
+
+        /// <summary>
+        /// use ">" for HTML output, or " />" for XHTML output
+        /// </summary>
+        public string EmptyElementSuffix { get; set; } = " />";
+
+        /// <summary>
+        /// when false, email addresses will never be auto-linked  
+        /// WARNING: this is a significant deviation from the markdown spec
+        /// </summary>
+        public bool LinkEmails { get; set; } = true;
+
+        /// <summary>
+        /// when true, bold and italic require non-word characters on either side  
+        /// WARNING: this is a significant deviation from the markdown spec
+        /// </summary>
+        public bool StrictBoldItalic { get; set; } = false;
+
+        /// <summary>
+        /// when true, asterisks may be used for intraword emphasis
+        /// this does nothing if StrictBoldItalic is false
+        /// </summary>
+        public bool AsteriskIntraWordEmphasis { get; set; } = false;
+
+        /// <summary>
+        /// when true, RETURN becomes a literal newline  
+        /// WARNING: this is a significant deviation from the markdown spec
+        /// </summary>
+        public bool AutoNewLines { get; set; } = false;
+
+        /// <summary>
+        /// when true, (most) bare plain URLs are auto-hyperlinked  
+        /// WARNING: this is a significant deviation from the markdown spec
+        /// </summary>
+        public bool AutoHyperlink { get; set; } = false;
+
+        #endregion
+
+        /// <summary>
+        /// maximum nested depth of [] and () supported by the transform; implementation detail
+        /// </summary>
+        private const int _nestDepth = 6;
+
+        /// <summary>
+        /// Tabs are automatically converted to spaces as part of the transform  
+        /// this constant determines how "wide" those tabs become in spaces  
+        /// </summary>
+        private const int _tabWidth = 4;
+
+        private const string _markerUL = "[*+-]";
+        private const string _markerOL = @"\d+[.]";
+
+        private static readonly Dictionary<string, string> _escapeTable;
+        private static readonly Dictionary<string, string> _invertedEscapeTable;
+        private static readonly Dictionary<string, string> _backslashEscapeTable;
+
+        private readonly Dictionary<string, string> _urls = new Dictionary<string, string>();
+        private readonly Dictionary<string, string> _titles = new Dictionary<string, string>();
+        private readonly Dictionary<string, string> _htmlBlocks = new Dictionary<string, string>();
+
+        private int _listLevel;
+        private const string AutoLinkPreventionMarker = "\x1AP"; // temporarily replaces "://" where auto-linking shouldn't happen
+
+        /// <summary>
+        /// In the static constuctor we'll initialize what stays the same across all transforms.
+        /// </summary>
+        static Markdown()
+        {
+            // Table of hash values for escaped characters:
+            _escapeTable = new Dictionary<string, string>();
+            _invertedEscapeTable = new Dictionary<string, string>();
+            // Table of hash value for backslash escaped characters:
+            _backslashEscapeTable = new Dictionary<string, string>();
+
+            string backslashPattern = "";
+
+            foreach (char c in @"\`*_{}[]()>#+-.!/:")
+            {
+                string key = c.ToString();
+                string hash = GetHashKey(key, isHtmlBlock: false);
+                _escapeTable.Add(key, hash);
+                _invertedEscapeTable.Add(hash, key);
+                _backslashEscapeTable.Add(@"\" + key, hash);
+                backslashPattern += Regex.Escape(@"\" + key) + "|";
+            }
+
+            _backslashEscapes = new Regex(backslashPattern.Substring(0, backslashPattern.Length - 1), RegexOptions.Compiled);
+        }
+
+        /// <summary>
+        /// current version of MarkdownSharp;  
+        /// see http://code.google.com/p/markdownsharp/ for the latest code or to contribute
+        /// </summary>
+        public string Version
+        {
+            get { return _version; }
+        }
+
+        /// <summary>
+        /// Transforms the provided Markdown-formatted text to HTML;  
+        /// see http://en.wikipedia.org/wiki/Markdown
+        /// </summary>
+        /// <remarks>
+        /// The order in which other subs are called here is
+        /// essential. Link and image substitutions need to happen before
+        /// EscapeSpecialChars(), so that any *'s or _'s in the a
+        /// and img tags get encoded.
+        /// </remarks>
+        public string Transform(string text)
+        {
+            if (string.IsNullOrEmpty(text)) return "";
+
+            Setup();
+
+            text = Normalize(text);
+
+            text = HashHTMLBlocks(text);
+            text = StripLinkDefinitions(text);
+            text = RunBlockGamut(text);
+            text = Unescape(text);
+
+            Cleanup();
+
+            return text + "\n";
+        }
+
+        /// <summary>
+        /// Perform transformations that form block-level tags like paragraphs, headers, and list items.
+        /// </summary>
+        private string RunBlockGamut(string text, bool unhash = true, bool createParagraphs = true)
+        {
+            text = DoHeaders(text);
+            text = DoHorizontalRules(text);
+            text = DoLists(text);
+            text = DoCodeBlocks(text);
+            text = DoBlockQuotes(text);
+
+            // We already ran HashHTMLBlocks() before, in Markdown(), but that
+            // was to escape raw HTML in the original Markdown source. This time,
+            // we're escaping the markup we've just created, so that we don't wrap
+            // <p> tags around block-level tags.
+            text = HashHTMLBlocks(text);
+
+            return FormParagraphs(text, unhash: unhash, createParagraphs: createParagraphs);
+        }
+
+        /// <summary>
+        /// Perform transformations that occur *within* block-level tags like paragraphs, headers, and list items.
+        /// </summary>
+        private string RunSpanGamut(string text)
+        {
+            text = DoCodeSpans(text);
+            text = EscapeSpecialCharsWithinTagAttributes(text);
+            text = EscapeBackslashes(text);
+
+            // Images must come first, because ![foo][f] looks like an anchor.
+            text = DoImages(text);
+            text = DoAnchors(text);
+
+            // Must come after DoAnchors(), because you can use < and >
+            // delimiters in inline links like [this](<url>).
+            text = DoAutoLinks(text);
+
+            text = text.Replace(AutoLinkPreventionMarker, "://");
+
+            text = EncodeAmpsAndAngles(text);
+            text = DoItalicsAndBold(text);
+            return DoHardBreaks(text);
+        }
+
+        private static readonly Regex _newlinesLeadingTrailing = new Regex(@"^\n+|\n+\z", RegexOptions.Compiled);
+        private static readonly Regex _newlinesMultiple = new Regex(@"\n{2,}", RegexOptions.Compiled);
+        private static readonly Regex _leadingWhitespace = new Regex("^[ ]*", RegexOptions.Compiled);
+
+        private static readonly Regex _htmlBlockHash = new Regex("\x1AH\\d+H", RegexOptions.Compiled);
+
+        /// <summary>
+        /// splits on two or more newlines, to form "paragraphs";    
+        /// each paragraph is then unhashed (if it is a hash and unhashing isn't turned off) or wrapped in HTML p tag
+        /// </summary>
+        private string FormParagraphs(string text, bool unhash = true, bool createParagraphs = true)
+        {
+            // split on two or more newlines
+            string[] grafs = _newlinesMultiple.Split(_newlinesLeadingTrailing.Replace(text, ""));
+
+            for (int i = 0; i < grafs.Length; i++)
+            {
+                if (grafs[i].Contains("\x1AH"))
+                {
+                    // unhashify HTML blocks
+                    if (unhash)
+                    {
+                        int sanityCheck = 50; // just for safety, guard against an infinite loop
+                        bool keepGoing = true; // as long as replacements where made, keep going
+                        while (keepGoing && sanityCheck > 0)
+                        {
+                            keepGoing = false;
+                            grafs[i] = _htmlBlockHash.Replace(grafs[i], match =>
+                            {
+                                keepGoing = true;
+                                return _htmlBlocks[match.Value];
+                            });
+                            sanityCheck--;
+                        }
+                        /* if (keepGoing)
+                        {
+                            // Logging of an infinite loop goes here.
+                            // If such a thing should happen, please open a new issue on http://code.google.com/p/markdownsharp/
+                            // with the input that caused it.
+                        }*/
+                    }
+                }
+                else
+                {
+                    // do span level processing inside the block, then wrap result in <p> tags
+                    grafs[i] = _leadingWhitespace.Replace(RunSpanGamut(grafs[i]), createParagraphs ? "<p>" : "") + (createParagraphs ? "</p>" : "");
+                }
+            }
+
+            return string.Join("\n\n", grafs);
+        }
+
+        private void Setup()
+        {
+            // Clear the global hashes. If we don't clear these, you get conflicts
+            // from other articles when generating a page which contains more than
+            // one article (e.g. an index page that shows the N most recent
+            // articles):
+            _urls.Clear();
+            _titles.Clear();
+            _htmlBlocks.Clear();
+            _listLevel = 0;
+        }
+
+        private void Cleanup()
+        {
+            Setup();
+        }
+
+        private static string _nestedBracketsPattern;
+
+        /// <summary>
+        /// Reusable pattern to match balanced [brackets]. See Friedl's 
+        /// "Mastering Regular Expressions", 2nd Ed., pp. 328-331.
+        /// </summary>
+        private static string GetNestedBracketsPattern()
+        {
+            // in other words [this] and [this[also]] and [this[also[too]]]
+            // up to _nestDepth
+            if (_nestedBracketsPattern == null)
+            {
+                _nestedBracketsPattern =
+                    RepeatString(@"
+                    (?>              # Atomic matching
+                       [^\[\]]+      # Anything other than brackets
+                     |
+                       \[
+                           ", _nestDepth) + RepeatString(
+                    @" \]
+                    )*"
+                    , _nestDepth);
+            }
+
+            return _nestedBracketsPattern;
+        }
+
+        private static string _nestedParensPattern;
+
+        /// <summary>
+        /// Reusable pattern to match balanced (parens). See Friedl's 
+        /// "Mastering Regular Expressions", 2nd Ed., pp. 328-331.
+        /// </summary>
+        private static string GetNestedParensPattern()
+        {
+            // in other words (this) and (this(also)) and (this(also(too)))
+            // up to _nestDepth
+            if (_nestedParensPattern == null)
+            {
+                _nestedParensPattern =
+                    RepeatString(@"
+                    (?>              # Atomic matching
+                       [^()\s]+      # Anything other than parens or whitespace
+                     |
+                       \(
+                           ", _nestDepth) + RepeatString(
+                    @" \)
+                    )*"
+                    , _nestDepth);
+            }
+
+            return _nestedParensPattern;
+        }
+
+        private static readonly Regex _linkDef = new Regex(string.Format(@"
+                        ^[ ]{{0,{0}}}\[([^\[\]]+)\]:  # id = $1
+                          [ ]*
+                          \n?                   # maybe *one* newline
+                          [ ]*
+                        <?(\S+?)>?              # url = $2
+                          [ ]*
+                          \n?                   # maybe one newline
+                          [ ]*
+                        (?:
+                            (?<=\s)             # lookbehind for whitespace
+                            [""(]
+                            (.+?)               # title = $3
+                            ["")]
+                            [ ]*
+                        )?                      # title is optional
+                        (?:\n+|\Z)", _tabWidth - 1), RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
+
+        /// <summary>
+        /// Strips link definitions from text, stores the URLs and titles in hash references.
+        /// </summary>
+        /// <remarks>
+        /// ^[id]: url "optional title"
+        /// </remarks>
+        private string StripLinkDefinitions(string text) => _linkDef.Replace(text, new MatchEvaluator(LinkEvaluator));
+
+        private string LinkEvaluator(Match match)
+        {
+            string linkID = match.Groups[1].Value.ToLowerInvariant();
+            _urls[linkID] = EncodeAmpsAndAngles(match.Groups[2].Value);
+
+            if (match.Groups[3]?.Length > 0)
+                _titles[linkID] = match.Groups[3].Value.Replace("\"", "&quot;");
+
+            return "";
+        }
+
+        // compiling this monster regex results in worse performance. trust me.
+        private static readonly Regex _blocksHtml = new Regex(GetBlockPattern(), RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace);
+
+        /// <summary>
+        /// derived pretty much verbatim from PHP Markdown
+        /// </summary>
+        private static string GetBlockPattern()
+        {
+            // Hashify HTML blocks:
+            // We only want to do this for block-level HTML tags, such as headers,
+            // lists, and tables. That's because we still want to wrap <p>s around
+            // "paragraphs" that are wrapped in non-block-level tags, such as anchors,
+            // phrase emphasis, and spans. The list of tags we're looking for is
+            // hard-coded:
+            //
+            // *  List "a" is made of tags which can be both inline or block-level.
+            //    These will be treated block-level when the start tag is alone on 
+            //    its line, otherwise they're not matched here and will be taken as 
+            //    inline later.
+            // *  List "b" is made of tags which are always block-level;
+            //
+            const string blockTagsA = "ins|del";
+            const string blockTagsB = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|script|noscript|form|fieldset|iframe|math";
+
+            // Regular expression for the content of a block tag.
+            const string attr = @"
+            (?>				            # optional tag attributes
+              \s			            # starts with whitespace
+              (?>
+                [^>""/]+	            # text outside quotes
+              |
+                /+(?!>)		            # slash not followed by >
+              |
+                ""[^""]*""		        # text inside double quotes (tolerate >)
+              |
+                '[^']*'	                # text inside single quotes (tolerate >)
+              )*
+            )?	
+            ";
+
+            string content = RepeatString(@"
+                (?>
+                  [^<]+			        # content without tag
+                |
+                  <\2			        # nested opening tag
+                    " + attr + @"       # attributes
+                  (?>
+                      />
+                  |
+                      >", _nestDepth) +   // end of opening tag
+                      ".*?" +             // last level nested tag content
+            RepeatString(@"
+                      </\2\s*>	        # closing nested tag
+                  )
+                  |				
+                  <(?!/\2\s*>           # other tags with a different name
+                  )
+                )*", _nestDepth);
+
+            string content2 = content.Replace(@"\2", @"\3");
+
+            // First, look for nested blocks, e.g.:
+            // 	<div>
+            // 		<div>
+            // 		tags for inner block must be indented.
+            // 		</div>
+            // 	</div>
+            //
+            // The outermost tags must start at the left margin for this to match, and
+            // the inner nested divs must be indented.
+            // We need to do this before the next, more liberal match, because the next
+            // match will start at the first `<div>` and stop at the first `</div>`.
+            string pattern = @"
+            (?>
+                  (?>
+                    (?<=\n)     # Starting at the beginning of a line
+                    |           # or
+                    \A\n?       # the beginning of the doc
+                  )
+                  (             # save in $1
+
+                    # Match from `\n<tag>` to `</tag>\n`, handling nested tags 
+                    # in between.
+                      
+                        <($block_tags_b_re)   # start tag = $2
+                        $attr>                # attributes followed by > and \n
+                        $content              # content, support nesting
+                        </\2>                 # the matching end tag
+                        [ ]*                  # trailing spaces
+                        (?=\n+|\Z)            # followed by a newline or end of document
+
+                  | # Special version for tags of group a.
+
+                        <($block_tags_a_re)   # start tag = $3
+                        $attr>[ ]*\n          # attributes followed by >
+                        $content2             # content, support nesting
+                        </\3>                 # the matching end tag
+                        [ ]*                  # trailing spaces
+                        (?=\n+|\Z)            # followed by a newline or end of document
+                      
+                  | # Special case just for <hr />. It was easier to make a special 
+                    # case than to make the other regex more complicated.
+                  
+                        [ ]{0,$less_than_tab}
+                        <hr
+                        $attr                 # attributes
+                        /?>                   # the matching end tag
+                        [ ]*
+                        (?=\n{2,}|\Z)         # followed by a blank line or end of document
+                  
+                  | # Special case for standalone HTML comments:
+                  
+                      (?<=\n\n|\A)            # preceded by a blank line or start of document
+                      [ ]{0,$less_than_tab}
+                      (?s:
+                        <!--(?:|(?:[^>-]|-[^>])(?:[^-]|-[^-])*)-->
+                      )
+                      [ ]*
+                      (?=\n{2,}|\Z)            # followed by a blank line or end of document
+                  
+                  | # PHP and ASP-style processor instructions (<? and <%)
+                  
+                      [ ]{0,$less_than_tab}
+                      (?s:
+                        <([?%])                # $4
+                        .*?
+                        \4>
+                      )
+                      [ ]*
+                      (?=\n{2,}|\Z)            # followed by a blank line or end of document
+                      
+                  )
+            )";
+
+            pattern = pattern.Replace("$less_than_tab", (_tabWidth - 1).ToString());
+            pattern = pattern.Replace("$block_tags_b_re", blockTagsB);
+            pattern = pattern.Replace("$block_tags_a_re", blockTagsA);
+            pattern = pattern.Replace("$attr", attr);
+            pattern = pattern.Replace("$content2", content2);
+            return pattern.Replace("$content", content);
+        }
+
+        /// <summary>
+        /// replaces any block-level HTML blocks with hash entries
+        /// </summary>
+        private string HashHTMLBlocks(string text)
+        {
+            return _blocksHtml.Replace(text, new MatchEvaluator(HtmlEvaluator));
+        }
+
+        private string HtmlEvaluator(Match match)
+        {
+            string text = match.Groups[1].Value;
+            string key = GetHashKey(text, isHtmlBlock: true);
+            _htmlBlocks[key] = text;
+
+            return string.Concat("\n\n", key, "\n\n");
+        }
+
+        private static string GetHashKey(string s, bool isHtmlBlock)
+        {
+            var delim = isHtmlBlock ? 'H' : 'E';
+            return "\x1A" + delim + Math.Abs(s.GetHashCode()).ToString() + delim;
+        }
+
+        private static readonly Regex _htmlTokens = new Regex(@"
+            (<!--(?:|(?:[^>-]|-[^>])(?:[^-]|-[^-])*)-->)|        # match <!-- foo -->
+            (<\?.*?\?>)|                 # match <?foo?> " +
+            RepeatString(@" 
+            (<[A-Za-z\/!$](?:[^<>]|", _nestDepth - 1) + @" 
+            (<[A-Za-z\/!$](?:[^<>]"
+            + RepeatString(")*>)", _nestDepth) +
+                                       " # match <tag> and </tag>",
+            RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.ExplicitCapture | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
+
+        /// <summary>
+        /// returns an array of HTML tokens comprising the input string. Each token is 
+        /// either a tag (possibly with nested, tags contained therein, such 
+        /// as &lt;a href="&lt;MTFoo&gt;"&gt;, or a run of text between tags. Each element of the 
+        /// array is a two-element array; the first is either 'tag' or 'text'; the second is 
+        /// the actual value.
+        /// </summary>
+        private List<Token> TokenizeHTML(string text)
+        {
+            int pos = 0;
+            int tagStart = 0;
+            var tokens = new List<Token>();
+
+            // this regex is derived from the _tokenize() subroutine in Brad Choate's MTRegex plugin.
+            // http://www.bradchoate.com/past/mtregex.php
+            foreach (Match m in _htmlTokens.Matches(text))
+            {
+                tagStart = m.Index;
+
+                if (pos < tagStart)
+                    tokens.Add(new Token(TokenType.Text, text.Substring(pos, tagStart - pos)));
+
+                tokens.Add(new Token(TokenType.Tag, m.Value));
+                pos = tagStart + m.Length;
+            }
+
+            if (pos < text.Length)
+                tokens.Add(new Token(TokenType.Text, text.Substring(pos, text.Length - pos)));
+
+            return tokens;
+        }
+
+        private static readonly Regex _anchorRef = new Regex(string.Format(@"
+            (                               # wrap whole match in $1
+                \[
+                    ({0})                   # link text = $2
+                \]
+
+                [ ]?                        # one optional space
+                (?:\n[ ]*)?                 # one optional newline followed by spaces
+
+                \[
+                    (.*?)                   # id = $3
+                \]
+            )", GetNestedBracketsPattern()), RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
+
+        private static readonly Regex _anchorInline = new Regex(string.Format(@"
+                (                           # wrap whole match in $1
+                    \[
+                        ({0})               # link text = $2
+                    \]
+                    \(                      # literal paren
+                        [ ]*
+                        ({1})               # href = $3
+                        [ ]*
+                        (                   # $4
+                        (['""])           # quote char = $5
+                        (.*?)               # title = $6
+                        \5                  # matching quote
+                        [ ]*                # ignore any spaces between closing quote and )
+                        )?                  # title is optional
+                    \)
+                )", GetNestedBracketsPattern(), GetNestedParensPattern()),
+                  RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
+
+        private static readonly Regex _anchorRefShortcut = new Regex(@"
+            (                               # wrap whole match in $1
+              \[
+                 ([^\[\]]+)                 # link text = $2; can't contain [ or ]
+              \]
+            )", RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
+
+        /// <summary>
+        /// Turn Markdown link shortcuts into HTML anchor tags
+        /// </summary>
+        /// <remarks>
+        /// [link text](url "title") 
+        /// [link text][id] 
+        /// [id] 
+        /// </remarks>
+        private string DoAnchors(string text)
+        {
+            if (!text.Contains("["))
+                return text;
+
+            // First, handle reference-style links: [link text] [id]
+            text = _anchorRef.Replace(text, new MatchEvaluator(AnchorRefEvaluator));
+
+            // Next, inline-style links: [link text](url "optional title") or [link text](url "optional title")
+            text = _anchorInline.Replace(text, new MatchEvaluator(AnchorInlineEvaluator));
+
+            //  Last, handle reference-style shortcuts: [link text]
+            //  These must come last in case you've also got [link test][1]
+            //  or [link test](/foo)
+            return _anchorRefShortcut.Replace(text, new MatchEvaluator(AnchorRefShortcutEvaluator));
+        }
+
+        private string SaveFromAutoLinking(string s)
+        {
+            return s.Replace("://", AutoLinkPreventionMarker);
+        }
+
+        private string AnchorRefEvaluator(Match match)
+        {
+            string wholeMatch = match.Groups[1].Value;
+            string linkText = SaveFromAutoLinking(match.Groups[2].Value);
+            string linkID = match.Groups[3].Value.ToLowerInvariant();
+
+            string result;
+
+            // for shortcut links like [this][].
+            if (linkID?.Length == 0)
+                linkID = linkText.ToLowerInvariant();
+
+            if (_urls.ContainsKey(linkID))
+            {
+                string url = _urls[linkID];
+
+                url = AttributeSafeUrl(url);
+
+                result = "<a href=\"" + url + "\"";
+
+                if (_titles.ContainsKey(linkID))
+                {
+                    string title = AttributeEncode(_titles[linkID]);
+                    title = AttributeEncode(EscapeBoldItalic(title));
+                    result += " title=\"" + title + "\"";
+                }
+
+                result += ">" + linkText + "</a>";
+            }
+            else
+            {
+                result = wholeMatch;
+            }
+
+            return result;
+        }
+
+        private string AnchorRefShortcutEvaluator(Match match)
+        {
+            string wholeMatch = match.Groups[1].Value;
+            string linkText = SaveFromAutoLinking(match.Groups[2].Value);
+            string linkID = Regex.Replace(linkText.ToLowerInvariant(), @"[ ]*\n[ ]*", " ");  // lower case and remove newlines / extra spaces
+
+            string result;
+
+            if (_urls.ContainsKey(linkID))
+            {
+                string url = _urls[linkID];
+
+                url = AttributeSafeUrl(url);
+
+                result = "<a href=\"" + url + "\"";
+
+                if (_titles.ContainsKey(linkID))
+                {
+                    string title = AttributeEncode(_titles[linkID]);
+                    title = EscapeBoldItalic(title);
+                    result += " title=\"" + title + "\"";
+                }
+
+                result += ">" + linkText + "</a>";
+            }
+            else
+            {
+                result = wholeMatch;
+            }
+
+            return result;
+        }
+
+        private string AnchorInlineEvaluator(Match match)
+        {
+            string linkText = SaveFromAutoLinking(match.Groups[2].Value);
+            string url = match.Groups[3].Value;
+            string title = match.Groups[6].Value;
+            string result;
+
+            if (url.StartsWith("<") && url.EndsWith(">"))
+                url = url.Substring(1, url.Length - 2); // remove <>'s surrounding URL, if present            
+
+            url = AttributeSafeUrl(url);
+
+            result = string.Format("<a href=\"{0}\"", url);
+
+            if (!string.IsNullOrEmpty(title))
+            {
+                title = AttributeEncode(title);
+                title = EscapeBoldItalic(title);
+                result += string.Format(" title=\"{0}\"", title);
+            }
+
+            result += string.Format(">{0}</a>", linkText);
+            return result;
+        }
+
+        private static readonly Regex _imagesRef = new Regex(@"
+                    (               # wrap whole match in $1
+                    !\[
+                        (.*?)       # alt text = $2
+                    \]
+
+                    [ ]?            # one optional space
+                    (?:\n[ ]*)?     # one optional newline followed by spaces
+
+                    \[
+                        (.*?)       # id = $3
+                    \]
+
+                    )", RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
+
+        private static readonly Regex _imagesInline = new Regex(string.Format(@"
+              (                     # wrap whole match in $1
+                !\[
+                    (.*?)           # alt text = $2
+                \]
+                \s?                 # one optional whitespace character
+                \(                  # literal paren
+                    [ ]*
+                    ({0})           # href = $3
+                    [ ]*
+                    (               # $4
+                    (['""])       # quote char = $5
+                    (.*?)           # title = $6
+                    \5              # matching quote
+                    [ ]*
+                    )?              # title is optional
+                \)
+              )", GetNestedParensPattern()),
+                  RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
+
+        /// <summary>
+        /// Turn Markdown image shortcuts into HTML img tags. 
+        /// </summary>
+        /// <remarks>
+        /// ![alt text][id]
+        /// ![alt text](url "optional title")
+        /// </remarks>
+        private string DoImages(string text)
+        {
+            if (!text.Contains("!["))
+                return text;
+
+            // First, handle reference-style labeled images: ![alt text][id]
+            text = _imagesRef.Replace(text, new MatchEvaluator(ImageReferenceEvaluator));
+
+            // Next, handle inline images:  ![alt text](url "optional title")
+            // Don't forget: encode * and _
+            return _imagesInline.Replace(text, new MatchEvaluator(ImageInlineEvaluator));
+        }
+
+        // This prevents the creation of horribly broken HTML when some syntax ambiguities
+        // collide. It likely still doesn't do what the user meant, but at least we're not
+        // outputting garbage.
+        private string EscapeImageAltText(string s)
+        {
+            s = EscapeBoldItalic(s);
+            return Regex.Replace(s, @"[\[\]()]", m => _escapeTable[m.ToString()]);
+        }
+
+        private string ImageReferenceEvaluator(Match match)
+        {
+            string wholeMatch = match.Groups[1].Value;
+            string altText = match.Groups[2].Value;
+            string linkID = match.Groups[3].Value.ToLowerInvariant();
+
+            // for shortcut links like ![this][].
+            if (linkID?.Length == 0)
+                linkID = altText.ToLowerInvariant();
+
+            if (_urls.ContainsKey(linkID))
+            {
+                string url = _urls[linkID];
+                string title = null;
+
+                if (_titles.ContainsKey(linkID))
+                    title = _titles[linkID];
+
+                return ImageTag(url, altText, title);
+            }
+            else
+            {
+                // If there's no such link ID, leave intact:
+                return wholeMatch;
+            }
+        }
+
+        private string ImageInlineEvaluator(Match match)
+        {
+            string alt = match.Groups[2].Value;
+            string url = match.Groups[3].Value;
+            string title = match.Groups[6].Value;
+
+            if (url.StartsWith("<") && url.EndsWith(">"))
+                url = url.Substring(1, url.Length - 2);    // Remove <>'s surrounding URL, if present
+
+            return ImageTag(url, alt, title);
+        }
+
+        private string ImageTag(string url, string altText, string title)
+        {
+            altText = EscapeImageAltText(AttributeEncode(altText));
+            url = AttributeSafeUrl(url);
+            var result = string.Format("<img src=\"{0}\" alt=\"{1}\"", url, altText);
+            if (!string.IsNullOrEmpty(title))
+            {
+                title = AttributeEncode(EscapeBoldItalic(title));
+                result += string.Format(" title=\"{0}\"", title);
+            }
+            result += EmptyElementSuffix;
+            return result;
+        }
+
+        private static readonly Regex _headerSetext = new Regex(@"
+                ^(.+?)
+                [ ]*
+                \n
+                (=+|-+)     # $1 = string of ='s or -'s
+                [ ]*
+                \n+",
+            RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
+
+        private static readonly Regex _headerAtx = new Regex(@"
+                ^(\#{1,6})  # $1 = string of #'s
+                [ ]*
+                (.+?)       # $2 = Header text
+                [ ]*
+                \#*         # optional closing #'s (not counted)
+                \n+",
+            RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
+
+        /// <summary>
+        /// Turn Markdown headers into HTML header tags
+        /// </summary>
+        /// <remarks>
+        /// <para>
+        /// Header 1  
+        /// ========  
+        /// </para>
+        /// <para>
+        /// Header 2  
+        /// --------  
+        /// </para>
+        /// <para>
+        /// # Header 1  
+        /// ## Header 2  
+        /// ## Header 2 with closing hashes ##  
+        /// ...  
+        /// ###### Header 6  
+        /// </para>
+        /// </remarks>
+        private string DoHeaders(string text)
+        {
+            text = _headerSetext.Replace(text, new MatchEvaluator(SetextHeaderEvaluator));
+            return _headerAtx.Replace(text, new MatchEvaluator(AtxHeaderEvaluator));
+        }
+
+        private string SetextHeaderEvaluator(Match match)
+        {
+            string header = match.Groups[1].Value;
+            int level = match.Groups[2].Value.StartsWith("=") ? 1 : 2;
+            return string.Format("<h{1}>{0}</h{1}>\n\n", RunSpanGamut(header), level);
+        }
+
+        private string AtxHeaderEvaluator(Match match)
+        {
+            string header = match.Groups[2].Value;
+            int level = match.Groups[1].Value.Length;
+            return string.Format("<h{1}>{0}</h{1}>\n\n", RunSpanGamut(header), level);
+        }
+
+        private static readonly Regex _horizontalRules = new Regex(@"
+            ^[ ]{0,3}         # Leading space
+                ([-*_])       # $1: First marker
+                (?>           # Repeated marker group
+                    [ ]{0,2}  # Zero, one, or two spaces.
+                    \1        # Marker character
+                ){2,}         # Group repeated at least twice
+                [ ]*          # Trailing spaces
+                $             # End of line.
+            ", RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
+
+        /// <summary>
+        /// Turn Markdown horizontal rules into HTML hr tags
+        /// </summary>
+        /// <remarks>
+        /// ***  
+        /// * * *  
+        /// ---
+        /// - - -
+        /// </remarks>
+        private string DoHorizontalRules(string text)
+        {
+            return _horizontalRules.Replace(text, "<hr" + EmptyElementSuffix + "\n");
+        }
+
+        private static readonly string _wholeList = string.Format(@"
+            (                               # $1 = whole list
+              (                             # $2
+                [ ]{{0,{1}}}
+                ({0})                       # $3 = first list item marker
+                [ ]+
+              )
+              (?s:.+?)
+              (                             # $4
+                  \z
+                |
+                  \n{{2,}}
+                  (?=\S)
+                  (?!                       # Negative lookahead for another list item marker
+                    [ ]*
+                    {0}[ ]+
+                  )
+              )
+            )", string.Format("(?:{0}|{1})", _markerUL, _markerOL), _tabWidth - 1);
+
+        private static readonly Regex _listNested = new Regex("^" + _wholeList,
+            RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
+
+        private static readonly Regex _listTopLevel = new Regex(@"(?:(?<=\n\n)|\A\n?)" + _wholeList,
+            RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
+
+        /// <summary>
+        /// Turn Markdown lists into HTML ul and ol and li tags
+        /// </summary>
+        private string DoLists(string text)
+        {
+            // We use a different prefix before nested lists than top-level lists.
+            // See extended comment in _ProcessListItems().
+            if (_listLevel > 0)
+            {
+                return _listNested.Replace(text, new MatchEvaluator(ListEvaluator));
+            }
+            else
+            {
+                return _listTopLevel.Replace(text, new MatchEvaluator(ListEvaluator));
+            }
+        }
+
+        private string ListEvaluator(Match match)
+        {
+            string list = match.Groups[1].Value;
+            string marker = match.Groups[3].Value;
+            string listType = Regex.IsMatch(marker, _markerUL) ? "ul" : "ol";
+            string result;
+            string start = "";
+            if (listType == "ol")
+            {
+                int.TryParse(marker.Substring(0, marker.Length - 1), out int firstNumber);
+                if (firstNumber != 1 && firstNumber != 0)
+                    start = " start=\"" + firstNumber + "\"";
+            }
+
+            result = ProcessListItems(list, listType == "ul" ? _markerUL : _markerOL);
+
+            return string.Format("<{0}{1}>\n{2}</{0}>\n", listType, start, result);
+        }
+
+        /// <summary>
+        /// Process the contents of a single ordered or unordered list, splitting it
+        /// into individual list items.
+        /// </summary>
+        private string ProcessListItems(string list, string marker)
+        {
+            // The listLevel global keeps track of when we're inside a list.
+            // Each time we enter a list, we increment it; when we leave a list,
+            // we decrement. If it's zero, we're not in a list anymore.
+
+            // We do this because when we're not inside a list, we want to treat
+            // something like this:
+
+            //    I recommend upgrading to version
+            //    8. Oops, now this line is treated
+            //    as a sub-list.
+
+            // As a single paragraph, despite the fact that the second line starts
+            // with a digit-period-space sequence.
+
+            // Whereas when we're inside a list (or sub-list), that line will be
+            // treated as the start of a sub-list. What a kludge, huh? This is
+            // an aspect of Markdown's syntax that's hard to parse perfectly
+            // without resorting to mind-reading. Perhaps the solution is to
+            // change the syntax rules such that sub-lists must start with a
+            // starting cardinal number; e.g. "1." or "a.".
+
+            _listLevel++;
+
+            // Trim trailing blank lines:
+            list = Regex.Replace(list, @"\n{2,}\z", "\n");
+
+            string pattern = string.Format(
+              @"(^[ ]*)                    # leading whitespace = $1
+                ({0}) [ ]+                 # list marker = $2
+                ((?s:.+?)                  # list item text = $3
+                (\n+))      
+                (?= (\z | \1 ({0}) [ ]+))", marker);
+
+            bool lastItemHadADoubleNewline = false;
+
+            // has to be a closure, so subsequent invocations can share the bool
+            string ListItemEvaluator(Match match)
+            {
+                string item = match.Groups[3].Value;
+
+                bool endsWithDoubleNewline = item.EndsWith("\n\n");
+                bool containsDoubleNewline = endsWithDoubleNewline || item.Contains("\n\n");
+
+                var loose = containsDoubleNewline || lastItemHadADoubleNewline;
+                // we could correct any bad indentation here..
+                item = RunBlockGamut(Outdent(item) + "\n", unhash: false, createParagraphs: loose);
+
+                lastItemHadADoubleNewline = endsWithDoubleNewline;
+                return string.Format("<li>{0}</li>\n", item);
+            }
+
+            list = Regex.Replace(list, pattern, new MatchEvaluator(ListItemEvaluator),
+                                  RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline);
+            _listLevel--;
+            return list;
+        }
+
+        private static readonly Regex _codeBlock = new Regex(string.Format(@"
+                    (?:\n\n|\A\n?)
+                    (                        # $1 = the code block -- one or more lines, starting with a space
+                    (?:
+                        (?:[ ]{{{0}}})       # Lines must start with a tab-width of spaces
+                        .*\n+
+                    )+
+                    )
+                    ((?=^[ ]{{0,{0}}}[^ \t\n])|\Z) # Lookahead for non-space at line-start, or end of doc",
+                    _tabWidth), RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
+
+        /// <summary>
+        /// /// Turn Markdown 4-space indented code into HTML pre code blocks
+        /// </summary>
+        private string DoCodeBlocks(string text)
+        {
+            return _codeBlock.Replace(text, new MatchEvaluator(CodeBlockEvaluator));
+        }
+
+        private string CodeBlockEvaluator(Match match)
+        {
+            string codeBlock = match.Groups[1].Value;
+
+            codeBlock = EncodeCode(Outdent(codeBlock));
+            codeBlock = _newlinesLeadingTrailing.Replace(codeBlock, "");
+
+            return string.Concat("\n\n<pre><code>", codeBlock, "\n</code></pre>\n\n");
+        }
+
+        private static readonly Regex _codeSpan = new Regex(@"
+                    (?<![\\`])   # Character before opening ` can't be a backslash or backtick
+                    (`+)      # $1 = Opening run of `
+                    (?!`)     # and no more backticks -- match the full run
+                    (.+?)     # $2 = The code block
+                    (?<!`)
+                    \1
+                    (?!`)", RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline | RegexOptions.Compiled);
+
+        /// <summary>
+        /// Turn Markdown `code spans` into HTML code tags
+        /// </summary>
+        private string DoCodeSpans(string text)
+        {
+            //    * You can use multiple backticks as the delimiters if you want to
+            //        include literal backticks in the code span. So, this input:
+            //
+            //        Just type ``foo `bar` baz`` at the prompt.
+            //
+            //        Will translate to:
+            //
+            //          <p>Just type <code>foo `bar` baz</code> at the prompt.</p>
+            //
+            //        There's no arbitrary limit to the number of backticks you
+            //        can use as delimters. If you need three consecutive backticks
+            //        in your code, use four for delimiters, etc.
+            //
+            //    * You can use spaces to get literal backticks at the edges:
+            //
+            //          ... type `` `bar` `` ...
+            //
+            //        Turns to:
+            //
+            //          ... type <code>`bar`</code> ...         
+            //
+
+            return _codeSpan.Replace(text, new MatchEvaluator(CodeSpanEvaluator));
+        }
+
+        private string CodeSpanEvaluator(Match match)
+        {
+            string span = match.Groups[2].Value;
+            span = Regex.Replace(span, "^[ ]*", ""); // leading whitespace
+            span = Regex.Replace(span, "[ ]*$", ""); // trailing whitespace
+            span = EncodeCode(span);
+            span = SaveFromAutoLinking(span); // to prevent auto-linking. Not necessary in code *blocks*, but in code spans.
+
+            return string.Concat("<code>", span, "</code>");
+        }
+
+        private static readonly Regex _bold = new Regex(@"(\*\*|__) (?=\S) (.+?[*_]*) (?<=\S) \1",
+            RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline | RegexOptions.Compiled);
+
+        private static readonly Regex _semiStrictBold = new Regex(@"(?=.[*_]|[*_])(^|(?=\W__|(?!\*)[\W_]\*\*|\w\*\*\w).)(\*\*|__)(?!\2)(?=\S)((?:|.*?(?!\2).)(?=\S_|\w|\S\*\*(?:[\W_]|$)).)(?=__(?:\W|$)|\*\*(?:[^*]|$))\2",
+            RegexOptions.Singleline | RegexOptions.Compiled);
+
+        private static readonly Regex _strictBold = new Regex(@"(^|[\W_])(?:(?!\1)|(?=^))(\*|_)\2(?=\S)(.*?\S)\2\2(?!\2)(?=[\W_]|$)",
+            RegexOptions.Singleline | RegexOptions.Compiled);
+
+        private static readonly Regex _italic = new Regex(@"(\*|_) (?=\S) (.+?) (?<=\S) \1",
+            RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline | RegexOptions.Compiled);
+
+        private static readonly Regex _semiStrictItalic = new Regex(@"(?=.[*_]|[*_])(^|(?=\W_|(?!\*)(?:[\W_]\*|\D\*(?=\w)\D)).)(\*|_)(?!\2\2\2)(?=\S)((?:(?!\2).)*?(?=[^\s_]_|(?=\w)\D\*\D|[^\s*]\*(?:[\W_]|$)).)(?=_(?:\W|$)|\*(?:[^*]|$))\2",
+            RegexOptions.Singleline | RegexOptions.Compiled);
+
+        private static readonly Regex _strictItalic = new Regex(@"(^|[\W_])(?:(?!\1)|(?=^))(\*|_)(?=\S)((?:(?!\2).)*?\S)\2(?!\2)(?=[\W_]|$)",
+            RegexOptions.Singleline | RegexOptions.Compiled);
+
+        /// <summary>
+        /// Turn Markdown *italics* and **bold** into HTML strong and em tags
+        /// </summary>
+        private string DoItalicsAndBold(string text)
+        {
+            if (!(text.Contains("*") || text.Contains("_")))
+                return text;
+            // <strong> must go first, then <em>
+            if (StrictBoldItalic)
+            {
+                if (AsteriskIntraWordEmphasis)
+                {
+                    text = _semiStrictBold.Replace(text, "$1<strong>$3</strong>");
+                    text = _semiStrictItalic.Replace(text, "$1<em>$3</em>");
+                }
+                else
+                {
+                    text = _strictBold.Replace(text, "$1<strong>$3</strong>");
+                    text = _strictItalic.Replace(text, "$1<em>$3</em>");
+                }
+            }
+            else
+            {
+                text = _bold.Replace(text, "<strong>$2</strong>");
+                text = _italic.Replace(text, "<em>$2</em>");
+            }
+            return text;
+        }
+
+        /// <summary>
+        /// Turn markdown line breaks (two space at end of line) into HTML break tags
+        /// </summary>
+        private string DoHardBreaks(string text)
+        {
+            if (AutoNewLines)
+            {
+                return Regex.Replace(text, @"\n", string.Format("<br{0}\n", EmptyElementSuffix));
+            }
+            else
+            {
+                return Regex.Replace(text, @" {2,}\n", string.Format("<br{0}\n", EmptyElementSuffix));
+            }
+        }
+
+        private static readonly Regex _blockquote = new Regex(@"
+            (                           # Wrap whole match in $1
+                (
+                ^[ ]*>[ ]?              # '>' at the start of a line
+                    .+\n                # rest of the first line
+                (.+\n)*                 # subsequent consecutive lines
+                \n*                     # blanks
+                )+
+            )", RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline | RegexOptions.Compiled);
+
+        /// <summary>
+        /// Turn Markdown > quoted blocks into HTML blockquote blocks
+        /// </summary>
+        private string DoBlockQuotes(string text)
+        {
+            return _blockquote.Replace(text, new MatchEvaluator(BlockQuoteEvaluator));
+        }
+
+        private string BlockQuoteEvaluator(Match match)
+        {
+            string bq = match.Groups[1].Value;
+
+            bq = Regex.Replace(bq, "^[ ]*>[ ]?", "", RegexOptions.Multiline);       // trim one level of quoting
+            bq = Regex.Replace(bq, "^[ ]+$", "", RegexOptions.Multiline);           // trim whitespace-only lines
+            bq = RunBlockGamut(bq);                                                  // recurse
+
+            bq = Regex.Replace(bq, "^", "  ", RegexOptions.Multiline);
+
+            // These leading spaces screw with <pre> content, so we need to fix that:
+            bq = Regex.Replace(bq, @"(\s*<pre>.+?</pre>)", new MatchEvaluator(BlockQuoteEvaluator2), RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline);
+
+            bq = string.Format("<blockquote>\n{0}\n</blockquote>", bq);
+            string key = GetHashKey(bq, isHtmlBlock: true);
+            _htmlBlocks[key] = bq;
+
+            return "\n\n" + key + "\n\n";
+        }
+
+        private string BlockQuoteEvaluator2(Match match)
+        {
+            return Regex.Replace(match.Groups[1].Value, "^  ", "", RegexOptions.Multiline);
+        }
+
+        private const string _charInsideUrl = @"[-A-Z0-9+&@#/%?=~_|\[\]\(\)!:,\.;" + "\x1a]";
+        private const string _charEndingUrl = "[-A-Z0-9+&@#/%=~_|\\[\\])]";
+
+        private static readonly Regex _autolinkBare = new Regex(@"(<|="")?\b(https?|ftp)(://" + _charInsideUrl + "*" + _charEndingUrl + ")(?=$|\\W)",
+            RegexOptions.IgnoreCase | RegexOptions.Compiled);
+
+        private static readonly Regex _endCharRegex = new Regex(_charEndingUrl, RegexOptions.IgnoreCase | RegexOptions.Compiled);
+
+        private static string HandleTrailingParens(Match match)
+        {
+            // The first group is essentially a negative lookbehind -- if there's a < or a =", we don't touch this.
+            // 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/">
+            // With a real lookbehind, the full link would never be matched, and thus the http://www.google.com *would* be matched.
+            // With the simulated lookbehind, the full link *is* matched (just not handled, because of this early return), causing
+            // the google link to not be matched again.
+            if (match.Groups[1].Success)
+                return match.Value;
+
+            var protocol = match.Groups[2].Value;
+            var link = match.Groups[3].Value;
+            if (!link.EndsWith(")"))
+                return "<" + protocol + link + ">";
+            var level = 0;
+            foreach (Match c in Regex.Matches(link, "[()]"))
+            {
+                if (c.Value == "(")
+                {
+                    if (level <= 0)
+                        level = 1;
+                    else
+                        level++;
+                }
+                else
+                {
+                    level--;
+                }
+            }
+            var tail = "";
+            if (level < 0)
+            {
+                link = Regex.Replace(link, @"\){1," + (-level) + "}$", m => { tail = m.Value; return ""; });
+            }
+            if (tail.Length > 0)
+            {
+                var lastChar = link[link.Length - 1];
+                if (!_endCharRegex.IsMatch(lastChar.ToString()))
+                {
+                    tail = lastChar + tail;
+                    link = link.Substring(0, link.Length - 1);
+                }
+            }
+            return "<" + protocol + link + ">" + tail;
+        }
+
+        private static readonly Regex _autoEmailBare = new Regex(@"(<|="")?(?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
+
+        private static string EmailBareLinkEvaluator(Match match)
+        {
+            // We matched an opening <, so it's already enclosed
+            if (match.Groups[1].Success)
+            {
+                return match.Value;
+            }
+            return "<" + match.Value + ">";
+        }
+
+        private readonly static Regex _linkEmail = new Regex(@"<
+                      (?:mailto:)?
+                      (
+                        [-.\w]+
+                        \@
+                        [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+
+                      )
+                      >", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
+
+        /// <summary>
+        /// Turn angle-delimited URLs into HTML anchor tags
+        /// </summary>
+        /// <remarks>
+        /// &lt;http://www.example.com&gt;
+        /// </remarks>
+        private string DoAutoLinks(string text)
+        {
+            if (AutoHyperlink)
+            {
+                // fixup arbitrary URLs by adding Markdown < > so they get linked as well
+                // note that at this point, all other URL in the text are already hyperlinked as <a href=""></a>
+                // *except* for the <http://www.foo.com> case
+                text = _autolinkBare.Replace(text, HandleTrailingParens);
+            }
+
+            // Hyperlinks: <http://foo.com>
+            text = Regex.Replace(text, "<((https?|ftp):[^'\">\\s]+)>", new MatchEvaluator(HyperlinkEvaluator));
+
+            if (LinkEmails)
+            {
+                // Email addresses: <address@domain.foo> or <mailto:address@domain.foo>
+                // Also allow "address@domain.foo" and "mailto:address@domain.foo", without the <>
+                //text = _autoEmailBare.Replace(text, EmailBareLinkEvaluator);
+                text = _linkEmail.Replace(text, new MatchEvaluator(EmailEvaluator));
+            }
+
+            return text;
+        }
+
+        private string HyperlinkEvaluator(Match match)
+        {
+            string link = match.Groups[1].Value;
+            string url = AttributeSafeUrl(link);
+            return string.Format("<a href=\"{0}\">{1}</a>", url, link);
+        }
+
+        private string EmailEvaluator(Match match)
+        {
+            string email = Unescape(match.Groups[1].Value);
+
+            //
+            //    Input: an email address, e.g. "foo@example.com"
+            //
+            //    Output: the email address as a mailto link, with each character
+            //            of the address encoded as either a decimal or hex entity, in
+            //            the hopes of foiling most address harvesting spam bots. E.g.:
+            //
+            //      <a href="&#x6D;&#97;&#105;&#108;&#x74;&#111;:&#102;&#111;&#111;&#64;&#101;
+            //        x&#x61;&#109;&#x70;&#108;&#x65;&#x2E;&#99;&#111;&#109;">&#102;&#111;&#111;
+            //        &#64;&#101;x&#x61;&#109;&#x70;&#108;&#x65;&#x2E;&#99;&#111;&#109;</a>
+            //
+            //    Based by a filter by Matthew Wickline, posted to the BBEdit-Talk
+            //    mailing list: <http://tinyurl.com/yu7ue>
+            //
+            email = "mailto:" + email;
+
+            // leave ':' alone (to spot mailto: later) 
+            email = EncodeEmailAddress(email);
+
+            email = string.Format("<a href=\"{0}\">{0}</a>", email);
+
+            // strip the mailto: from the visible part
+            return Regex.Replace(email, "\">.+?:", "\">");
+        }
+
+        private static readonly Regex _outDent = new Regex("^[ ]{1," + _tabWidth + "}", RegexOptions.Multiline | RegexOptions.Compiled);
+
+        /// <summary>
+        /// Remove one level of line-leading spaces
+        /// </summary>
+        private string Outdent(string block)
+        {
+            return _outDent.Replace(block, "");
+        }
+
+        #region Encoding and Normalization
+
+        /// <summary>
+        /// encodes email address randomly  
+        /// roughly 10% raw, 45% hex, 45% dec 
+        /// note that @ is always encoded and : never is
+        /// </summary>
+        private string EncodeEmailAddress(string addr)
+        {
+            var sb = new StringBuilder(addr.Length * 5);
+            var rand = new Random();
+            int r;
+            foreach (char c in addr)
+            {
+                r = rand.Next(1, 100);
+                if ((r > 90 || c == ':') && c != '@')
+                    sb.Append(c);                         // m
+                else if (r < 45)
+                    sb.AppendFormat("&#x{0:x};", (int)c); // &#x6D
+                else
+                    sb.AppendFormat("&#{0};", (int)c);    // &#109
+            }
+            return sb.ToString();
+        }
+
+        private static readonly Regex _codeEncoder = new Regex(@"&|<|>|\\|\*|_|\{|\}|\[|\]", RegexOptions.Compiled);
+
+        /// <summary>
+        /// Encode/escape certain Markdown characters inside code blocks and spans where they are literals
+        /// </summary>
+        private string EncodeCode(string code)
+        {
+            return _codeEncoder.Replace(code, EncodeCodeEvaluator);
+        }
+
+        private string EncodeCodeEvaluator(Match match)
+        {
+            switch (match.Value)
+            {
+                // Encode all ampersands; HTML entities are not
+                // entities within a Markdown code span.
+                case "&":
+                    return "&amp;";
+                // Do the angle bracket song and dance
+                case "<":
+                    return "&lt;";
+                case ">":
+                    return "&gt;";
+                // escape characters that are magic in Markdown
+                default:
+                    return _escapeTable[match.Value];
+            }
+        }
+
+        private static readonly Regex _amps = new Regex("&(?!((#[0-9]+)|(#[xX][a-fA-F0-9]+)|([a-zA-Z][a-zA-Z0-9]*));)", RegexOptions.ExplicitCapture | RegexOptions.Compiled);
+        private static readonly Regex _angles = new Regex(@"<(?![A-Za-z/?\$!])", RegexOptions.ExplicitCapture | RegexOptions.Compiled);
+
+        /// <summary>
+        /// Encode any ampersands (that aren't part of an HTML entity) and left or right angle brackets
+        /// </summary>
+        private string EncodeAmpsAndAngles(string s)
+        {
+            s = _amps.Replace(s, "&amp;");
+            return _angles.Replace(s, "&lt;");
+        }
+
+        private static readonly Regex _backslashEscapes;
+
+        /// <summary>
+        /// Encodes any escaped characters such as \`, \*, \[ etc
+        /// </summary>
+        private string EscapeBackslashes(string s) => _backslashEscapes.Replace(s, new MatchEvaluator(EscapeBackslashesEvaluator));
+
+        private string EscapeBackslashesEvaluator(Match match) => _backslashEscapeTable[match.Value];
+
+        // note: this space MATTERS - do not remove (hex / unicode) \|/
+#pragma warning disable RCS1190 // Join string expressions.
+        private static readonly Regex _unescapes = new Regex("\x1A" + "E\\d+E", RegexOptions.Compiled);
+#pragma warning restore RCS1190 // Join string expressions.
+
+        /// <summary>
+        /// swap back in all the special characters we've hidden
+        /// </summary>
+        private string Unescape(string s) => _unescapes.Replace(s, new MatchEvaluator(UnescapeEvaluator));
+
+        private string UnescapeEvaluator(Match match) => _invertedEscapeTable[match.Value];
+
+        /// <summary>
+        /// escapes Bold [ * ] and Italic [ _ ] characters
+        /// </summary>
+        private string EscapeBoldItalic(string s)
+        {
+            s = s.Replace("*", _escapeTable["*"]);
+            return s.Replace("_", _escapeTable["_"]);
+        }
+
+        private static string AttributeEncode(string s)
+        {
+            return s.Replace(">", "&gt;").Replace("<", "&lt;").Replace("\"", "&quot;").Replace("'", "&#39;");
+        }
+
+        private static string AttributeSafeUrl(string s)
+        {
+            s = AttributeEncode(s);
+            foreach (var c in "*_:()[]")
+                s = s.Replace(c.ToString(), _escapeTable[c.ToString()]);
+            return s;
+        }
+
+        /// <summary>
+        /// Within tags -- meaning between &lt; and &gt; -- encode [\ ` * _] so they 
+        /// don't conflict with their use in Markdown for code, italics and strong. 
+        /// We're replacing each such character with its corresponding hash 
+        /// value; this is likely overkill, but it should prevent us from colliding 
+        /// with the escape values by accident.
+        /// </summary>
+        private string EscapeSpecialCharsWithinTagAttributes(string text)
+        {
+            var tokens = TokenizeHTML(text);
+
+            // now, rebuild text from the tokens
+            var sb = new StringBuilder(text.Length);
+
+            foreach (var token in tokens)
+            {
+                string value = token.Value;
+
+                if (token.Type == TokenType.Tag)
+                {
+                    value = value.Replace(@"\", _escapeTable[@"\"]);
+
+                    if (AutoHyperlink && value.StartsWith("<!")) // escape slashes in comments to prevent autolinking there -- https://meta.stackexchange.com/questions/95987/html-comment-containing-url-breaks-if-followed-by-another-html-comment
+                        value = value.Replace("/", _escapeTable["/"]);
+
+                    value = Regex.Replace(value, "(?<=.)</?code>(?=.)", _escapeTable["`"]);
+                    value = EscapeBoldItalic(value);
+                }
+
+                sb.Append(value);
+            }
+
+            return sb.ToString();
+        }
+
+        /// <summary>
+        /// convert all tabs to _tabWidth spaces; 
+        /// standardizes line endings from DOS (CR LF) or Mac (CR) to UNIX (LF); 
+        /// makes sure text ends with a couple of newlines; 
+        /// removes any blank lines (only spaces) in the text
+        /// </summary>
+        private string Normalize(string text)
+        {
+            var output = new StringBuilder(text.Length);
+            var line = new StringBuilder();
+            bool valid = false;
+
+            for (int i = 0; i < text.Length; i++)
+            {
+                switch (text[i])
+                {
+                    case '\n':
+                        if (valid) output.Append(line);
+                        output.Append('\n');
+                        line.Length = 0; valid = false;
+                        break;
+                    case '\r':
+                        if ((i < text.Length - 1) && (text[i + 1] != '\n'))
+                        {
+                            if (valid) output.Append(line);
+                            output.Append('\n');
+                            line.Length = 0; valid = false;
+                        }
+                        break;
+                    case '\t':
+                        int width = (_tabWidth - (line.Length % _tabWidth));
+                        for (int k = 0; k < width; k++)
+                            line.Append(' ');
+                        break;
+                    case '\x1A':
+                        break;
+                    default:
+                        if (!valid && text[i] != ' ') valid = true;
+                        line.Append(text[i]);
+                        break;
+                }
+            }
+
+            if (valid) output.Append(line);
+            output.Append('\n');
+
+            // add two newlines to the end before return
+            return output.Append("\n\n").ToString();
+        }
+
+        #endregion
+
+        /// <summary>
+        /// this is to emulate what's evailable in PHP
+        /// </summary>
+        private static string RepeatString(string text, int count)
+        {
+            var sb = new StringBuilder(text.Length * count);
+            for (int i = 0; i < count; i++)
+                sb.Append(text);
+            return sb.ToString();
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/Apewer/Externals/MarkdownSharp-1.13/MarkdownOptions.cs b/Apewer/Externals/MarkdownSharp-1.13/MarkdownOptions.cs
new file mode 100644
index 0000000..b7818c5
--- /dev/null
+++ b/Apewer/Externals/MarkdownSharp-1.13/MarkdownOptions.cs
@@ -0,0 +1,45 @@
+namespace MarkdownSharp
+{
+
+    /// <summary>
+    /// Options for configuring MarkdownSharp.
+    /// </summary>
+    internal class MarkdownOptions
+    {
+        /// <summary>
+        /// when true, (most) bare plain URLs are auto-hyperlinked  
+        /// WARNING: this is a significant deviation from the markdown spec
+        /// </summary>
+        public bool AutoHyperlink { get; set; }
+
+        /// <summary>
+        /// when true, RETURN becomes a literal newline  
+        /// WARNING: this is a significant deviation from the markdown spec
+        /// </summary>
+        public bool AutoNewlines { get; set; }
+
+        /// <summary>
+        /// use ">" for HTML output, or " />" for XHTML output
+        /// </summary>
+        public string EmptyElementSuffix { get; set; }
+
+        /// <summary>
+        /// when false, email addresses will never be auto-linked  
+        /// WARNING: this is a significant deviation from the markdown spec
+        /// </summary>
+        public bool LinkEmails { get; set; }
+
+        /// <summary>
+        /// when true, bold and italic require non-word characters on either side  
+        /// WARNING: this is a significant deviation from the markdown spec
+        /// </summary>
+        public bool StrictBoldItalic { get; set; }
+
+        /// <summary>
+        /// when true, asterisks may be used for intraword emphasis
+        /// this does nothing if StrictBoldItalic is false
+        /// </summary>
+        public bool AsteriskIntraWordEmphasis { get; set; }
+    }
+
+}
diff --git a/Apewer/Externals/MarkdownSharp-1.13/Token.cs b/Apewer/Externals/MarkdownSharp-1.13/Token.cs
new file mode 100644
index 0000000..73e0d86
--- /dev/null
+++ b/Apewer/Externals/MarkdownSharp-1.13/Token.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MarkdownSharp
+{
+
+    internal enum TokenType
+    {
+        Text, 
+        Tag
+    }
+
+    internal struct Token
+    {
+
+        public Token(TokenType type, string value)
+        {
+            Type = type;
+            Value = value;
+        }
+
+        public TokenType Type;
+        public string Value;
+
+    }
+
+}
diff --git a/Apewer/Internals/Constant.cs b/Apewer/Internals/Constant.cs
index 462ae39..4a044b4 100644
--- a/Apewer/Internals/Constant.cs
+++ b/Apewer/Internals/Constant.cs
@@ -38,6 +38,9 @@ namespace Apewer.Internals
         /// <summary>所有 GUID 中的字符,包含字母、数字和连字符。</summary>
         public const string GuidCollection = "0123456789ABCDEFabcdef-";
 
+        /// <summary>所有主键字符。</summary>
+        public const string KeyCollection = "0123456789abcdefghijklmnopqrstuvwxyz";
+
         /// <summary>所有十六进制字符。</summary>
         public const string HexCollection = "0123456789abcdef";
 
diff --git a/Apewer/Json.cs b/Apewer/Json.cs
index d55e7dc..c37b908 100644
--- a/Apewer/Json.cs
+++ b/Apewer/Json.cs
@@ -16,6 +16,9 @@ namespace Apewer
     /// <summary>Json。</summary>
     [Serializable]
     public class Json
+#if !NET20
+        : DynamicObject
+#endif
     {
 
         #region 配置。
@@ -1698,6 +1701,48 @@ namespace Apewer
 
         #endregion
 
+        #region Dynamic
+
+#if !NET20
+
+        /// <summary></summary>
+        public override bool TryGetMember(GetMemberBinder binder, out object result)
+        {
+            var contains = false;
+            if (IsObject)
+            {
+                var property = GetProperty(binder.Name);
+                contains = property != null;
+                result = contains ? property.Value : null;
+                return contains;
+            }
+
+            result = null;
+            return contains;
+        }
+
+        /// <summary></summary>
+        public override bool TrySetMember(SetMemberBinder binder, object value)
+        {
+            var name = binder.Name;
+
+            if (value == null) return SetProperty(name);
+            if (value is Json) return SetProperty(name, (Json)value);
+            if (value is string) return SetProperty(name, (string)value);
+            if (value is bool) return SetProperty(name, (bool)value);
+            if (value is int) return SetProperty(name, (int)value);
+            if (value is long) return SetProperty(name, (long)value);
+            if (value is float) return SetProperty(name, (float)value);
+            if (value is double) return SetProperty(name, (double)value);
+            if (value is decimal) return SetProperty(name, (decimal)value);
+
+            return false;
+        }
+
+#endif
+
+        #endregion
+
         #region Statics
 
         #region 创建实例。
diff --git a/Apewer/Models/StringPairs.cs b/Apewer/Models/StringPairs.cs
index b64bfdd..b10dbf9 100644
--- a/Apewer/Models/StringPairs.cs
+++ b/Apewer/Models/StringPairs.cs
@@ -15,7 +15,7 @@ namespace Apewer.Models
         public string Add(string key, string value)
         {
             if (string.IsNullOrEmpty(key)) return "参数 Name 无效。";
-            if (string.IsNullOrEmpty(value)) return "参数 Value 无效。";
+            // if (string.IsNullOrEmpty(value)) return "参数 Value 无效。";
 
             var kvp = new KeyValuePair<string, string>(key, value);
             Add(kvp);
diff --git a/Apewer/NetworkUtility.cs b/Apewer/NetworkUtility.cs
index 77835f6..ba5ae8d 100644
--- a/Apewer/NetworkUtility.cs
+++ b/Apewer/NetworkUtility.cs
@@ -58,7 +58,7 @@ namespace Apewer
                 }
             }
             catch { }
-            return new DateTime(1, 0, 0, 0, 0, 0, 0, DateTimeKind.Utc);
+            return DateTimeUtility.Zero();
         }
 
         /// <summary>从 NTP 服务器获取 UTC 时间。</summary>
@@ -90,7 +90,7 @@ namespace Apewer
                 return utc;
             }
             catch { }
-            return new DateTime(1, 0, 0, 0, 0, 0, 0, DateTimeKind.Utc);
+            return DateTimeUtility.Zero();
         }
 
         private static uint SwapEndian(ulong x)
diff --git a/Apewer/Source/ColumnAttribute.cs b/Apewer/Source/ColumnAttribute.cs
index e89e199..6c9e04d 100644
--- a/Apewer/Source/ColumnAttribute.cs
+++ b/Apewer/Source/ColumnAttribute.cs
@@ -21,6 +21,8 @@ namespace Apewer.Source
 
         private bool _independent = false;
 
+        private bool _locked = false;
+
         /// <summary>使用自动的列名称。当类型为 VarChar 或 NVarChar 时必须指定长度。</summary>
         /// <exception cref="System.ArgumentException"></exception>
         public ColumnAttribute(ColumnType type = ColumnType.NVarChar255, int length = 0)
@@ -41,19 +43,55 @@ namespace Apewer.Source
         }
 
         /// <summary>属性。</summary>
-        public PropertyInfo Property { get { return _property; } internal set { _property = value; } }
+        public PropertyInfo Property
+        {
+            get { return _property; }
+            internal set
+            {
+                if (_locked) return;
+                _property = value;
+            }
+        }
 
         /// <summary>字段名。</summary>
-        public string Field { get { return _field; } set { _field = value; } }
+        public string Field
+        {
+            get { return _field; }
+            set
+            {
+                if (_locked) return;
+                _field = value;
+            }
+        }
 
         /// <summary>指定字段的最大长度。</summary>
-        public int Length { get { return _length; } set { _length = value; } }
+        public int Length
+        {
+            get { return _length; }
+            set
+            {
+                if (_locked) return;
+                _length = value;
+            }
+        }
 
         /// <summary>字段类型。</summary>
-        public ColumnType Type { get { return _type; } set { _type = value; } }
+        public ColumnType Type
+        {
+            get { return _type; }
+            set
+            {
+                if (_locked) return;
+                _type = value;
+            }
+        }
 
         /// <summary>Independent 特性。</summary>
-        public bool Independent { get { return _independent; } internal set { _independent = value; } }
+        public bool Independent
+        {
+            get { return _independent; }
+            internal set { _independent = value; }
+        }
 
         /// <exception cref="System.ArgumentException"></exception>
         private void New(string field, ColumnType type, int length, bool underline)
@@ -77,6 +115,12 @@ namespace Apewer.Source
             }
         }
 
+        /// <summary>锁定属性,阻止修改。</summary>
+        public void Lock()
+        {
+            _locked = true;
+        }
+
     }
 
 }
diff --git a/Apewer/Source/MySql.cs b/Apewer/Source/MySql.cs
index b74ab22..ad41c46 100644
--- a/Apewer/Source/MySql.cs
+++ b/Apewer/Source/MySql.cs
@@ -1,7 +1,5 @@
 #if NET40 || NET461
 
-/* 2020.9.4.0 */
-
 using Apewer;
 using Apewer.Source;
 using Externals.MySql.Data.MySqlClient;
@@ -415,12 +413,12 @@ namespace Apewer.Source
             return CreateTable(model.GetType());
         }
 
-        /// <summary></summary>
         /// <summary>插入记录。成功时候返回空字符串,发生异常时返回异常信息。</summary>
-        public string Insert(Record entity)
+        public string Insert(Record entity, bool resetKey = false)
         {
             if (entity == null) return "参数无效。";
             entity.FixProperties();
+            if (resetKey) Record.ResetKey(entity);
 
             var structure = null as TableStructure;
             try { structure = TableStructure.ParseModel(entity); }
@@ -477,7 +475,7 @@ namespace Apewer.Source
             }
         }
 
-        /// <summary></summary>
+        /// <summary>获取记录。</summary>
         public Result<T> QueryRecord<T>(string key, long flag = 0) where T : Record
         {
             var k = TextUtility.RestrictGuid(key);
@@ -515,6 +513,25 @@ namespace Apewer.Source
             }
         }
 
+        /// <summary>获取记录。</summary>
+        /// <param name="skip">要跳过的记录数,可用最小值为 0。</param>
+        /// <param name="count">要获取的记录数,可用最小值为 1。</param>
+        public Result<List<T>> QueryRecords<T>(int skip, int count) where T : Record
+        {
+            try
+            {
+                if (skip < 0) return new Result<List<T>>(new ArgumentOutOfRangeException(nameof(skip)));
+                if (count < 1) return new Result<List<T>>(new ArgumentOutOfRangeException(nameof(count)));
+                var tableName = TableStructure.ParseTable(typeof(T)).Name;
+                var sql = $"select * from `{tableName}` where _flag = 1 limit {skip}, {count}; ";
+                return QueryRecords<T>(sql);
+            }
+            catch (Exception exception)
+            {
+                return new Result<List<T>>(exception);
+            }
+        }
+
         /// <summary>查询有效的 Key 值。</summary>
         public Result<List<string>> QueryKeys(Type model, long flag = 0)
         {
@@ -562,10 +579,111 @@ namespace Apewer.Source
             }
         }
 
+        /// <summary>对表添加列,返回错误信息。</summary>
+        /// <typeparam name="T">记录类型。</typeparam>
+        /// <param name="column">列名称。</param>
+        /// <param name="type">字段类型。</param>
+        /// <param name="length">字段长度,仅对 VarChar 和 NVarChar 类型有效。</param>
+        /// <returns></returns>
+        public string AddColumn<T>(string column, ColumnType type, int length = 0) where T : Record
+        {
+            var columnName = SafeColumn(column);
+            if (columnName.IsEmpty()) return "列名无效。";
+
+            var tableName = TableStructure.ParseTable(typeof(T)).Name;
+
+            var columeType = "";
+            switch (type)
+            {
+                case ColumnType.Integer:
+                    columeType = "bigint";
+                    break;
+                case ColumnType.Float:
+                    columeType = "double";
+                    break;
+                case ColumnType.Binary:
+                    columeType = "longblob";
+                    break;
+                case ColumnType.DateTime:
+                    columeType = "datetime";
+                    break;
+                case ColumnType.VarChar:
+                case ColumnType.NVarChar:
+                    columeType = $"varchar({length})";
+                    break;
+                case ColumnType.VarChar255:
+                case ColumnType.NVarChar255:
+                    columeType = "varchar(255)";
+                    break;
+                case ColumnType.VarCharMax:
+                case ColumnType.NVarCharMax:
+                    columeType = "varchar(max)";
+                    break;
+                case ColumnType.Text:
+                    columeType = "longtext";
+                    break;
+            }
+            if (columeType.IsEmpty()) return "类型不支持。";
+
+            var sql = $"alter table `{tableName}` add {columnName} {columeType}; ";
+            var execute = Execute(sql) as Execute;
+            var error = execute.Error;
+            return error;
+        }
+
         #endregion
 
         #region static
 
+        /// <summary>对文本转义,符合 SQL 安全性。可根据字段类型限制 UTF-8 字节数,默认为 0 时不限制字节数。</summary>
+        public static string Escape(string text, int bytes = 0)
+        {
+            if (text.IsEmpty()) return "";
+
+            var t = text ?? "";
+            t = t.Replace("\\", "\\\\");
+            t = t.Replace("'", "\\'");
+            t = t.Replace("\n", "\\n");
+            t = t.Replace("\r", "\\r");
+            t = t.Replace("\b", "\\b");
+            t = t.Replace("\t", "\\t");
+            t = t.Replace("\f", "\\f");
+
+            if (bytes > 5)
+            {
+                if (t.GetBytes(Encoding.UTF8).Length > bytes)
+                {
+                    while (true)
+                    {
+                        t = t.Substring(0, t.Length - 1);
+                        if (t.GetBytes(Encoding.UTF8).Length <= (bytes - 4)) break;
+                    }
+                    t = t + " ...";
+                }
+            }
+
+            return t;
+        }
+
+        /// <summary></summary>
+        public static string SafeTable(string table)
+        {
+            const string chars = "0123456789_-ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+            var safety = TextUtility.RestrictCharacters(table, chars).SafeLower();
+            var pc = 0;
+            for (var i = 0; i < safety.Length; i++)
+            {
+                if (safety[i] == '-') pc += 1;
+                else break;
+            }
+            if (pc == safety.Length) return "";
+            if (pc > 0) safety = safety.Substring(pc);
+            return safety;
+        }
+
+        /// <summary></summary>
+        public static string SafeColumn(string column) => SafeTable(column);
+
         /// <summary></summary>
         /// <exception cref="ArgumentNullException"></exception>
         /// <exception cref="InvalidOperationException"></exception>
diff --git a/Apewer/Source/Record.cs b/Apewer/Source/Record.cs
index 195fa64..07d7fd0 100644
--- a/Apewer/Source/Record.cs
+++ b/Apewer/Source/Record.cs
@@ -46,13 +46,15 @@ namespace Apewer.Source
         [Column("_key", ColumnType.NVarChar, 128)]
         public virtual string Key { get { return _key; } set { _key = TextModifier.Compact(value, 128); } }
 
-        internal static void SetNewKey(Record record)
+        /// <summary>重置主键。</summary>
+        internal static void ResetKey(Record record)
         {
             if (record == null) return;
             record.Key = GenerateKey();
         }
 
-        internal static string GenerateKey() => Guid.NewGuid().ToString().ToLower().Replace("-", "");
+        /// <summary>生成新主键。</summary>
+        public static string GenerateKey() => Guid.NewGuid().ToString().ToLower().Replace("-", "");
 
         internal static void FixProperties(Record record)
         {
diff --git a/Apewer/Source/TableAttribute.cs b/Apewer/Source/TableAttribute.cs
index 1bdc775..0bbbf63 100644
--- a/Apewer/Source/TableAttribute.cs
+++ b/Apewer/Source/TableAttribute.cs
@@ -15,6 +15,8 @@ namespace Apewer.Source
         private string _name;
         private bool _independent = false;
 
+        private bool _locked = false;
+
         /// <summary></summary>
         public TableAttribute(string name = null)
         {
@@ -27,18 +29,26 @@ namespace Apewer.Source
             _name = TableStructure.RestrictName(name, underline);
         }
 
-        /// <summary></summary>
+        /// <summary>表名。</summary>
         public string Name
         {
             get { return _name; }
-            set { _name = TableStructure.RestrictName(value, false); }
+            set
+            {
+                if (_locked) return;
+                _name = TableStructure.RestrictName(value, false);
+            }
         }
 
         /// <summary></summary>
         public bool Independent
         {
             get { return _independent; }
-            internal set { _independent = value; }
+            internal set
+            {
+                if (_locked) return;
+                _independent = value;
+            }
         }
 
         /// <summary></summary>
@@ -47,6 +57,12 @@ namespace Apewer.Source
             return _name.GetHashCode();
         }
 
+        /// <summary>锁定属性,阻止修改。</summary>
+        public void Lock()
+        {
+            _locked = true;
+        }
+
     }
 
 }
diff --git a/Apewer/Source/TableStructure.cs b/Apewer/Source/TableStructure.cs
index 607c590..a65c51e 100644
--- a/Apewer/Source/TableStructure.cs
+++ b/Apewer/Source/TableStructure.cs
@@ -14,6 +14,8 @@ namespace Apewer.Source
     public sealed class TableStructure
     {
 
+        private bool _locked = false;
+
         private string _tablename = Constant.EmptyString;
         private bool _independent = false;
 
@@ -21,36 +23,95 @@ namespace Apewer.Source
 
         internal TableStructure() { }
 
-        /// <summary></summary>
-        public bool Independent { get { return _independent; } private set { _independent = value; } }
+        /// <summary>不依赖 Record 公共属性。</summary>
+        public bool Independent
+        {
+            get { return _independent; }
+            private set { _independent = value; }
+        }
 
-        /// <summary></summary>
-        public string Table { get { return _tablename; } set { _tablename = value ?? ""; } }
+        /// <summary>表名称。</summary>
+        public string Table
+        {
+            get { return _tablename; }
+            private set { _tablename = value ?? ""; }
+        }
 
-        /// <summary></summary>
-        public Dictionary<string, ColumnAttribute> Columns { get { return _columns; } set { _columns = value; } }
+        /// <summary>列信息。</summary>
+        public Dictionary<string, ColumnAttribute> Columns
+        {
+            get
+            {
+                if (_locked)
+                {
+                    var copy = new Dictionary<string, ColumnAttribute>(_columns.Count);
+                    foreach (var c in _columns) copy.Add(c.Key, c.Value);
+                    return copy;
+                }
+                return _columns;
+            }
+            private set { _columns = value; }
+        }
+
+        /// <summary>锁定属性,阻止修改。</summary>
+        public void Lock()
+        {
+            _locked = true;
+            foreach (var c in _columns) c.Value.Lock();
+        }
+
+        #region cache
+
+        private static Dictionary<string, TableStructure> _tsc = new Dictionary<string, TableStructure>();
+
+        private static Dictionary<string, TableAttribute> _tac = new Dictionary<string, TableAttribute>();
+
+        #endregion
 
         #region static
 
         /// <summary></summary>
         /// <exception cref="System.Exception"></exception>
         /// <exception cref="System.ArgumentNullException"></exception>
-        public static TableStructure ParseModel(object entity)
+        public static TableStructure ParseModel(object entity, bool useCache = true)
         {
             if (entity == null) throw new ArgumentNullException("参数无效");
             var type = entity.GetType();
-            var result = ParseModel(type);
+            var result = ParseModel(type, useCache);
             return result;
         }
 
         /// <summary></summary>
         /// <exception cref="System.Exception"></exception>
         /// <exception cref="System.ArgumentNullException"></exception>
-        public static TableStructure ParseModel(Type model)
+        public static TableStructure ParseModel<T>(bool useCache = true) where T : IRecord
+        {
+            return ParseModel(typeof(T), useCache);
+        }
+
+        /// <summary></summary>
+        /// <exception cref="System.Exception"></exception>
+        /// <exception cref="System.ArgumentNullException"></exception>
+        public static TableStructure ParseModel(Type model, bool useCache = true)
         {
             var type = model;
             if (type == null) throw new ArgumentNullException("参数无效");
 
+            // 使用缓存。
+            var cacheKey = type.FullName;
+            if (useCache)
+            {
+                var hint = null as TableStructure;
+                lock (_tsc)
+                {
+                    if (_tsc.ContainsKey(cacheKey))
+                    {
+                        hint = _tsc[cacheKey];
+                    }
+                }
+                if (hint != null) return hint;
+            }
+
             // 检查基类。
             // if (type.BaseType.FullName.Equals(typeof(DatabaseRecord).FullName) == false) return "基类不受支持。";
 
@@ -91,18 +152,57 @@ namespace Apewer.Source
             columns = SortColumns(columns);
 
             // 返回结果。
-            var result = new TableStructure();
-            result.Table = ta.Name;
-            result.Independent = ta.Independent;
-            result.Columns = columns;
-            return result;
+            var ts = new TableStructure();
+            ts.Table = ta.Name;
+            ts.Independent = ta.Independent;
+            ts.Columns = columns;
+
+            // 锁定属性。
+            ts.Lock();
+
+            // 加入缓存。
+            if (useCache)
+            {
+                lock (_tsc)
+                {
+                    if (!_tsc.ContainsKey(cacheKey))
+                    {
+                        _tsc.Add(cacheKey, ts);
+                    }
+                }
+            }
+
+            return ts;
+        }
+
+        /// <summary></summary>
+        /// <param name="type"></param>
+        /// <exception cref="Exception"></exception>"
+        public static TableAttribute ParseTable<T>(bool useCache = true) where T : IRecord
+        {
+            return ParseTable(typeof(T), useCache);
         }
 
         /// <summary></summary>
         /// <param name="type"></param>
         /// <exception cref="Exception"></exception>"
-        public static TableAttribute ParseTable(Type type)
+        public static TableAttribute ParseTable(Type type, bool useCache = true)
         {
+            // 使用缓存。
+            var cacheKey = type.FullName;
+            if (useCache)
+            {
+                var hint = null as TableAttribute;
+                lock (_tac)
+                {
+                    if (_tac.ContainsKey(cacheKey))
+                    {
+                        hint = _tac[cacheKey];
+                    }
+                }
+                if (hint != null) return hint;
+            }
+
             var tas = type.GetCustomAttributes(typeof(TableAttribute), false);
             if (tas.LongLength < 1L) throw new Exception(TextGenerator.Merge("类 ", type.FullName, " 不包含 ", typeof(TableAttribute).FullName, "。"));
             if (tas.LongLength > 1L) throw new Exception(TextGenerator.Merge("类 ", type.FullName, " 包含多个 ", typeof(TableAttribute).FullName, "。"));
@@ -116,6 +216,21 @@ namespace Apewer.Source
 
             ta.Independent = ClassUtility.ContainsAttribute<IndependentAttribute>(type, true);
 
+            // 锁定属性。
+            ta.Lock();
+
+            // 加入缓存。
+            if (useCache)
+            {
+                lock (_tac)
+                {
+                    if (!_tac.ContainsKey(cacheKey))
+                    {
+                        _tac.Add(cacheKey, ta);
+                    }
+                }
+            }
+
             return ta;
         }
 
@@ -176,6 +291,9 @@ namespace Apewer.Source
 
             ca.Property = property;
 
+            // 锁定属性。
+            ca.Lock();
+
             return ca;
         }
 
diff --git a/Apewer/TextUtility.cs b/Apewer/TextUtility.cs
index 0537fca..2601e46 100644
--- a/Apewer/TextUtility.cs
+++ b/Apewer/TextUtility.cs
@@ -354,17 +354,22 @@ namespace Apewer
             if (string.IsNullOrEmpty(text)) return Constant.EmptyString;
             var input = ToLower(text);
 
-            var max = input.Length;
-            if (maxLength > 0 && maxLength < input.Length) max = maxLength;
+            var max = maxLength;
+            if (max < 1 || max > input.Length) max = input.Length;
 
             var sb = new StringBuilder();
-            for (var i = 0; i < max; i++)
+            var total = input.Length;
+            var length = 0;
+            for (var i = 0; i < total; i++)
             {
                 var c = input[i];
-                if (Constant.HexCollection.IndexOf(c) < 0) continue;
+                if (Constant.KeyCollection.IndexOf(c) < 0) continue;
                 sb.Append(c);
+                length += 1;
+                if (length >= max) break;
             }
             var result = sb.ToString();
+            if (result.Length > max) result = result.Substring(0, max);
             return result;
         }
 
@@ -412,6 +417,9 @@ namespace Apewer
             return match.Success;
         }
 
+        /// <summary>渲染 Markdown 文本为 HTML 文本。</summary>
+        public static string RenderMarkdown(string markdown) => MarkdownSharp.Demo.ToHtml(markdown);
+
         /// <summary>合并用于启动进程的参数。</summary>
         public static string MergeProcessArgument(params object[] args)
         {
diff --git a/Apewer/Web/ApiInternals.cs b/Apewer/Web/ApiInternals.cs
index 2275411..f53dee7 100644
--- a/Apewer/Web/ApiInternals.cs
+++ b/Apewer/Web/ApiInternals.cs
@@ -1,14 +1,16 @@
 #if NETFX || NETCORE
 
 using Apewer;
-using Apewer.Network;
+using Apewer.Models;
 using System;
 using System.Collections.Generic;
 using System.Collections.Specialized;
 using System.IO;
 using System.Reflection;
+
+#if NETFX
 using System.Web;
-using Apewer.Models;
+#endif
 
 #if NETCORE
 using Microsoft.AspNetCore.Http;
@@ -45,6 +47,7 @@ namespace Apewer.Web
                 if (deduplicates.Contains(assembly)) continue;
                 deduplicates.Add(assembly);
             }
+            _assemblies = deduplicates;
 
             var aes = new ObjectSet<ApiApplication>();
             foreach (var assembly in deduplicates)
@@ -201,9 +204,9 @@ namespace Apewer.Web
 
         internal static StringPairs ParseUrlParameters
 #if NETFX
-        (System.Web.HttpRequest request) => WebUtility.ParseParameters(request.Url.Query);
+        (HttpRequest request) => WebUtility.ParseParameters(request.Url.Query);
 #else
-        (Microsoft.AspNetCore.Http.HttpRequest request)
+        (HttpRequest request)
         {
             var list = new StringPairs();
             foreach (var key in request.Query.Keys)
@@ -217,11 +220,7 @@ namespace Apewer.Web
         }
 #endif
 
-#if NETFX
-        internal static ApiRequest GetRequest(System.Web.HttpRequest request)
-#else
-        internal static ApiRequest GetRequest(Microsoft.AspNetCore.Http.HttpRequest request)
-#endif
+        internal static ApiRequest GetRequest(HttpRequest request)
         {
             if (request == null) return null;
             if (string.IsNullOrEmpty(request.Path)) return null;
@@ -232,7 +231,7 @@ namespace Apewer.Web
             apiRequest.Parameters = ParseUrlParameters(request);
             apiRequest.UserAgent = WebUtility.GetUserAgent(request);
 #if NETFX
-            apiRequest.Referrer = request.UrlReferrer;            
+            apiRequest.Referrer = request.UrlReferrer;
 #endif
 
             // 获取 Http Method。
@@ -258,8 +257,11 @@ namespace Apewer.Web
                 }
             }
 
+            // Cookies。
+            apiRequest.Cookies = WebUtility.GetCookies(request);
+
             // 解析 POST 请求。
-            if (apiRequest.Method == HttpMethod.POST)
+            if (apiRequest.Method == Apewer.Network.HttpMethod.POST)
             {
 #if NETFX
                 var post = BinaryUtility.Read(request.InputStream);
@@ -293,6 +295,9 @@ namespace Apewer.Web
             if (string.IsNullOrEmpty(session)) session = WebUtility.GetParameter(apiRequest.Parameters, "session");
             if (string.IsNullOrEmpty(page)) page = WebUtility.GetParameter(apiRequest.Parameters, "page");
 
+            // 从 Cookie 中获取 Ticket。
+            if (string.IsNullOrEmpty(ticket)) ticket = apiRequest.Cookies.GetValue("ticket");
+
             // 最后检查 URL 路径。
             var paths = request.Path.ToString().Split('/');
             if (string.IsNullOrEmpty(application) && paths.Length >= 2) application = TextUtility.DecodeUrl(paths[1]);
@@ -353,12 +358,12 @@ namespace Apewer.Web
         }
 
         /// <summary>输出 UTF-8 文本。</summary>
-        internal static void RespondText(ApiResponse response, string content, string type = "text/plain")
+        internal static void RespondText(ApiResponse response, string content, string type = "text/plain; charset=utf-8")
         {
             if (response == null) return;
             response.Type = ApiFormat.Text;
             response.TextString = content;
-            response.TextType = type ?? "text/plain";
+            response.TextType = type ?? "text/plain; charset=utf-8";
         }
 
         /// <summary>输出字节数组。</summary>
@@ -466,9 +471,7 @@ namespace Apewer.Web
 
         #region HttpResponse
 
-        public static void AddHeader
-#if NETFX
-        (System.Web.HttpResponse response, string name, string value)
+        public static void AddHeader(HttpResponse response, string name, string value)
         {
             if (response == null || string.IsNullOrEmpty(name) || string.IsNullOrEmpty(value)) return;
 #if NET20
@@ -477,33 +480,14 @@ namespace Apewer.Web
             try { response.Headers.Add(name, value); } catch { }
 #endif
         }
-#else
-        (Microsoft.AspNetCore.Http.HttpResponse response, string name, string value)
-        {
-            if (response == null || string.IsNullOrEmpty(name) || string.IsNullOrEmpty(value)) return;
-            try { response.Headers.Add(name, value); } catch { }
-        }
-#endif
 
-        public static void AddHeaders
-#if NETFX
-        (System.Web.HttpResponse response, NameValueCollection collection)
+        public static void AddHeaders(HttpResponse response, NameValueCollection collection)
         {
             if (response == null || collection == null) return;
             foreach (var key in collection.AllKeys) AddHeader(response, key, collection[key]);
         }
-#else
-        (Microsoft.AspNetCore.Http.HttpResponse response, NameValueCollection collection)
-        {
-            if (response == null) return;
-            if (collection == null) return;
-            foreach (var key in collection.AllKeys) AddHeader(response, key, collection[key]);
-        }
-#endif
 
-        public static void AddHeaders
-#if NETFX
-        (System.Web.HttpResponse response, StringPairs headers)
+        public static void AddHeaders(HttpResponse response, StringPairs headers)
         {
             if (response == null || headers == null) return;
             foreach (var key in headers.GetAllKeys())
@@ -512,73 +496,121 @@ namespace Apewer.Web
                 foreach (var value in values) AddHeader(response, key, value);
             }
         }
-#else
-        (Microsoft.AspNetCore.Http.HttpResponse response, StringPairs headers)
+
+#if NETFX
+
+        private static FieldInfo CacheControlField = null;
+
+        private static void SetCacheControlField(HttpResponse response, string value)
         {
-            if (response == null || headers == null) return;
-            foreach (var key in headers.GetAllKeys())
+            if (CacheControlField == null)
             {
-                var values = headers.GetValues(key);
-                foreach (var value in values) AddHeader(response, key, value);
+                var type = typeof(HttpResponse);
+                var field = type.GetField("_cacheControl", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
+                if (field == null) return;
+                CacheControlField = field;
             }
+            if (CacheControlField != null) CacheControlField.SetValue(response, value);
         }
+
 #endif
 
-        public static void SetTextPlain
-#if NETFX
-        (System.Web.HttpResponse response, string value = null)
+        /// <summary>设置缓存时间,单位为秒,最大为 2592000 秒(30 天)。</summary>
+        public static void SetCacheControl
+
+        (HttpResponse response, int seconds = 0)
         {
             if (response == null) return;
-            if (string.IsNullOrEmpty(value))
+            var s = seconds;
+            if (s < 0) s = 0;
+            if (s > 2592000) s = 2592000;
+#if NETFX
+            if (s > 0)
             {
-                response.ContentType = "text/plain";
-                response.Charset = "utf-8";
+                SetCacheControlField(response, $"public, max-age={s}, s-maxage={s}");
             }
             else
             {
-                response.ContentType = value;
+                var minutes = s < 60 ? 0 : (s / 60);
+                try { response.CacheControl = "no-cache"; } catch { }
+                try { response.Expires = minutes; } catch { }
+                AddHeader(response, "Pragma", "no-cache");
             }
-        }
 #else
-        (Microsoft.AspNetCore.Http.HttpResponse response, string value = null)
+            if (s > 0)
+            {
+                AddHeader(response, "Cache-Control", $"public, max-age={s}, s-maxage={s}");
+            }
+            else
+            {
+                var minutes = s < 60 ? 0 : (s / 60);
+                AddHeader(response, "Cache-Control", "no-cache, no-store, must-revalidate");
+                AddHeader(response, "Expires", minutes.ToString());
+                AddHeader(response, "Pragma", "no-cache");
+            }
+#endif
+        }
+
+        public static void SetTextPlain(HttpResponse response, string value = null)
         {
             if (response == null) return;
-            response.ContentType = string.IsNullOrEmpty(value) ? "text/plain; charset=utf-8" : value;
+            const string plain = "text/plain; charset=utf-8";
+            // response.ContentType = "text/plain";
+            // response.Charset = "utf-8";
+            response.ContentType = string.IsNullOrEmpty(value) ? plain : value;
         }
-#endif
 
-        public static void SetContentLength
-#if NETFX
-        (System.Web.HttpResponse response, long value)
+        public static void SetContentLength(HttpResponse response, long value)
         {
             if (response == null) return;
             if (value < 0L) return;
+#if NETFX
             response.AddHeader("Content-Length", value.ToString());
-        }
 #else
-        (Microsoft.AspNetCore.Http.HttpResponse response, long value)
-        {
-            if (response == null) return;
-            if (value < 0L) return;
             response.ContentLength = value;
-        }
-
 #endif
+        }
 
-        public static Stream GetStream
-#if NETFX
-        (System.Web.HttpResponse response)
+        public static Stream GetStream(HttpResponse response)
         {
             if (response == null) return null;
+#if NETFX
             return response.OutputStream;
-        }
 #else
-        (Microsoft.AspNetCore.Http.HttpResponse response)
-        {
-            if (response == null) return null;
             return response.Body;
-        }
 #endif
+        }
+
+        /// <summary>设置响应的 Cookies。</summary>
+        public static void SetCookies(HttpResponse response, IEnumerable<KeyValuePair<string, string>> cookies)
+        {
+            // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
+            // https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Set-Cookie
+
+            // <cookie-name> 可以是除了控制字符 (CTLs)、空格 (spaces) 或制表符 (tab)之外的任何 US-ASCII 字符。
+            // 同时不能包含以下分隔字符: ( ) < > @ , ; : \ " /  [ ] ? = { }.
+
+            // <cookie-value> 是可选的,如果存在的话,那么需要包含在双引号里面。
+            // 支持除了控制字符(CTLs)、空格(whitespace)、双引号(double quotes)、逗号(comma)、分号(semicolon)以及反斜线(backslash)之外的任意 US-ASCII 字符。
+
+            // 关于编码:许多应用会对 cookie 值按照URL编码(URL encoding)规则进行编码,但是按照 RFC 规范,这不是必须的。不过满足规范中对于 <cookie-value> 所允许使用的字符的要求是有用的。
+
+            if (cookies == null) return;
+
+            var ps = new List<string>();
+            foreach (var kvp in cookies)
+            {
+                var k = kvp.Key;
+                var v = kvp.Value;
+                if (k.IsEmpty()) continue;
+                ps.Add(TextUtility.Merge(k, "="), v);
+            }
+
+            if (ps.Count < 1) return;
+
+            var value = TextUtility.Merge("; ", ps);
+            AddHeader(response, "Set-Cookie", value);
+        }
 
         #endregion
 
diff --git a/Apewer/Web/ApiInvoker.cs b/Apewer/Web/ApiInvoker.cs
index bb120ea..34b4f66 100644
--- a/Apewer/Web/ApiInvoker.cs
+++ b/Apewer/Web/ApiInvoker.cs
@@ -233,7 +233,7 @@ namespace Apewer.Web
             if (request == null || response == null) return;
 
             // 创建应用程序实例。
-            var target = null as ApiController;
+            var controller = null as ApiController;
             try
             {
                 if (application == null)
@@ -246,7 +246,7 @@ namespace Apewer.Web
                     //var instance = assembly.CreateInstance(argApplication.FullName, false);
                     //var handler = Activator.CreateInstance(null, argApplication.FullName);
                     //target = (Application)handler.Unwrap();
-                    target = (ApiController)Activator.CreateInstance(application.Type);
+                    controller = (ApiController)Activator.CreateInstance(application.Type);
                 }
             }
             catch (Exception exception)
@@ -258,13 +258,13 @@ namespace Apewer.Web
             // 调用功能。
             try
             {
-                target.Request = request;
-                target.Response = response;
-                target.Context = Context;
+                controller.Request = request;
+                controller.Response = response;
+                controller.Context = Context;
 
-                if (target.AfterInitialized != null) target.AfterInitialized.Invoke();
+                if (controller.AfterInitialized != null) controller.AfterInitialized.Invoke();
 
-                var allowed = target.AllowFunction;
+                var allowed = controller.AllowFunction;
                 if (application.Independent) allowed = false;
 
                 if (allowed)
@@ -275,8 +275,8 @@ namespace Apewer.Web
                     }
                     else
                     {
-                        var result = function.Method.Invoke(target, null);
-                        target.Dispose();
+                        var result = function.Method.Invoke(controller, null);
+                        controller.Dispose();
                         if (response.Status.IsBlank()) response.Status = "ok";
                         if (function.Returnable && result != null)
                         {
@@ -288,102 +288,124 @@ namespace Apewer.Web
                         }
                     }
                 }
-
             }
             catch (Exception exception)
             {
                 response.Error(exception.InnerException);
             }
-            try { target.Dispose(); } catch { }
+
+            try { controller.Dispose(); } catch { }
         }
 
-        private string Output()
+        private void OutputMain()
         {
-            if (Context == null) return "Invoker 的 Context 无效。";
-            if (ApiResponse == null) return "Invoker 的 ApiResponse 无效。";
+            var response = Context.Response;
 
-            try
+            // 设置自定义头。
+            ApiInternals.AddHeaders(response, ApiResponse.Headers);
+            var setCookies = WebUtility.SetCookies(response, ApiResponse.Cookies);
+            if (setCookies.NotEmpty())
             {
-                var response = Context.Response;
-                var body = ApiInternals.GetStream(response);
-                ApiInternals.AddHeaders(response, ApiResponse.Headers);
-                switch (ApiResponse.Type)
-                {
-                    case ApiFormat.Json:
-                        {
-                            var text = ApiInternals.ExportJson(ApiResponse, ApiOptions.JsonIndent, ApiOptions.AllowException);
-                            var data = TextUtility.ToBinary(text);
-                            ApiInternals.SetTextPlain(response);
-                            ApiInternals.SetContentLength(response, data.LongLength);
-                            body.Write(data);
-                        }
-                        break;
-                    case ApiFormat.Text:
+                ApiResponse.Error("系统错误。");
+                ApiResponse.Data.Reset(Json.NewObject());
+                ApiResponse.Data["cookies"] = setCookies;
+            }
+
+            // 过期。
+            ApiInternals.SetCacheControl(response, ApiResponse.Expires);
+
+            // 判断内容类型输出。
+            var body = ApiInternals.GetStream(response);
+            switch (ApiResponse.Type)
+            {
+                case ApiFormat.Json:
+                    {
+                        var text = ApiInternals.ExportJson(ApiResponse, ApiOptions.JsonIndent, ApiOptions.AllowException);
+                        var data = TextUtility.ToBinary(text);
+                        ApiInternals.SetTextPlain(response);
+                        ApiInternals.SetContentLength(response, data.LongLength);
+                        body.Write(data);
+                    }
+                    break;
+                case ApiFormat.Text:
+                    {
+                        var data = TextUtility.ToBinary(ApiResponse.TextString);
+                        var type = ApiResponse.TextType;
+                        ApiInternals.SetTextPlain(response, type);
+                        ApiInternals.SetContentLength(response, data.LongLength);
+                        body.Write(data);
+                    }
+                    break;
+                case ApiFormat.Binary:
+                    {
+                        Context.Response.ContentType = ApiResponse.BinaryType ?? "application/octet-stream";
+                        if (ApiResponse.BinaryBytes != null)
                         {
-                            var data = TextUtility.ToBinary(ApiResponse.TextString);
-                            var type = ApiResponse.TextType;
-                            ApiInternals.SetTextPlain(response, type);
-                            ApiInternals.SetContentLength(response, data.LongLength);
-                            body.Write(data);
+                            var data = ApiResponse.BinaryBytes;
+                            var length = data.LongLength;
+                            ApiInternals.SetContentLength(response, length);
+                            if (length > 0L) body.Write(data);
                         }
-                        break;
-                    case ApiFormat.Binary:
+                        else if (ApiResponse.BinaryStream != null && ApiResponse.BinaryStream.CanRead)
                         {
-                            Context.Response.ContentType = ApiResponse.BinaryType ?? "application/octet-stream";
-                            if (ApiResponse.BinaryBytes != null)
+                            var source = ApiResponse.BinaryStream;
+                            try
                             {
-                                var data = ApiResponse.BinaryBytes;
-                                var length = data.LongLength;
+                                var length = source.Length - source.Position;
                                 ApiInternals.SetContentLength(response, length);
-                                if (length > 0L) body.Write(data);
-                            }
-                            else if (ApiResponse.BinaryStream != null && ApiResponse.BinaryStream.CanRead)
-                            {
-                                var source = ApiResponse.BinaryStream;
-                                try
-                                {
-                                    var length = source.Length - source.Position;
-                                    ApiInternals.SetContentLength(response, length);
-                                    if (length > 0) body.Write(source);
-                                }
-                                catch { }
-                                KernelUtility.Dispose(source);
+                                if (length > 0) body.Write(source);
                             }
+                            catch { }
+                            KernelUtility.Dispose(source);
                         }
-                        break;
-                    case ApiFormat.File:
-                        {
-                            var type = ApiResponse.FileType ?? "application/octet-stream";
-                            var name = TextUtility.EncodeUrl(ApiResponse.FileName);
-                            var disposition = TextUtility.Merge("attachment; filename=", name);
+                    }
+                    break;
+                case ApiFormat.File:
+                    {
+                        var type = ApiResponse.FileType ?? "application/octet-stream";
+                        var name = TextUtility.EncodeUrl(ApiResponse.FileName);
+                        var disposition = TextUtility.Merge("attachment; filename=", name);
 
-                            response.ContentType = type;
-                            ApiInternals.AddHeader(response, "Content-Disposition", disposition);
+                        response.ContentType = type;
+                        ApiInternals.AddHeader(response, "Content-Disposition", disposition);
 
-                            var source = ApiResponse.FileStream;
-                            try
+                        var source = ApiResponse.FileStream;
+                        try
+                        {
+                            if (source != null && source.CanRead)
                             {
-                                if (source != null && source.CanRead)
-                                {
-                                    var length = source.Length - source.Position;
-                                    ApiInternals.SetContentLength(response, length);
-                                    if (length > 0) body.Write(source);
-                                }
+                                var length = source.Length - source.Position;
+                                ApiInternals.SetContentLength(response, length);
+                                if (length > 0) body.Write(source);
                             }
-                            catch { }
-                            KernelUtility.Dispose(source);
-                        }
-                        break;
-                    case ApiFormat.Redirect:
-                        {
-                            WebUtility.RedirectResponse(response, ApiResponse.RedirectUrl);
                         }
-                        break;
-                }
-                // Context.Response.Flush();
-                // Context.Response.End();
+                        catch { }
+                        KernelUtility.Dispose(source);
+                    }
+                    break;
+                case ApiFormat.Redirect:
+                    {
+                        WebUtility.RedirectResponse(response, ApiResponse.RedirectUrl);
+                    }
+                    break;
+            }
+            // Context.Response.Flush();
+            // Context.Response.End();
+        }
+
+        private string Output()
+        {
+            if (Context == null) return "Invoker 的 Context 无效。";
+            if (ApiResponse == null) return "Invoker 的 ApiResponse 无效。";
+#if DEBUG
+            OutputMain();
+#else
+            try
+            {
+                OutputMain();
             }
             catch (Exception ex) { return ex.Message; }
+#endif
             return null;
         }
 
@@ -406,6 +428,14 @@ namespace Apewer.Web
             invoker.Run();
         }
 
+        /// <summary>获取用于执行 API 的程序集。</summary>
+        public static Assembly[] GetAssemblies()
+        {
+            var list = ApiInternals.Assemblies;
+            if (list == null) return new Assembly[0];
+            return list.ToArray();
+        }
+
 #if NETFX
 
         /// <summary>解析请求,获取指定程序集中定义的入口,并执行。指定程序集为 NULL 值时,将自动从应用程序域获取所有 WebAPI 入口。</summary>
@@ -467,8 +497,7 @@ namespace Apewer.Web
         /// <summary>设置 Kestrel 的 API 入口。</summary>
         public static void SetKestrelEntries(Assembly assembly, bool sort = true)
         {
-            var assemblies = new Assembly[] { assembly };
-            if (assemblies[0] == null) assemblies[0] = Assembly.GetCallingAssembly();
+            var assemblies = new Assembly[] { assembly ?? Assembly.GetCallingAssembly() };
             KestrelEntries = ApiInternals.GetEntries(assemblies, sort);
         }
 
diff --git a/Apewer/Web/ApiRequest.cs b/Apewer/Web/ApiRequest.cs
index 9132d23..0a10fa8 100644
--- a/Apewer/Web/ApiRequest.cs
+++ b/Apewer/Web/ApiRequest.cs
@@ -46,6 +46,9 @@ namespace Apewer.Web
         /// <summary>HTTP 头。</summary>
         public StringPairs Headers { get; set; } = new StringPairs();
 
+        /// <summary>Cookies。</summary>
+        public StringPairs Cookies { get; set; } = new StringPairs();
+
         #endregion
 
         #region API 请求。
diff --git a/Apewer/Web/ApiResponse.cs b/Apewer/Web/ApiResponse.cs
index d3fc5df..5cd5f9e 100644
--- a/Apewer/Web/ApiResponse.cs
+++ b/Apewer/Web/ApiResponse.cs
@@ -15,9 +15,12 @@ namespace Apewer.Web
 
         private Json _data = Json.NewObject();
 
-        /// <summary></summary>
+        /// <summary>HTTP 头。</summary>
         public StringPairs Headers { get; set; } = new StringPairs();
 
+        /// <summary>Cookies。</summary>
+        public StringPairs Cookies { get; set; } = new StringPairs();
+
 #if NETFX
         internal System.Web.HttpContext Context { get; set; }
 #endif
@@ -59,6 +62,10 @@ namespace Apewer.Web
         internal ApiFormat Type = ApiFormat.Json;
         internal Exception Exception { get; set; }
 
+        /// <summary>设置缓存过期时间,单位为秒。默认值:0,立即过期,不缓存。</summary>
+        /// <remarks>在 .NET Framework 中,此设置可能无效。</remarks>
+        public int Expires { get; set; }
+
         #endregion
 
         #region 输出纯文本。
diff --git a/Apewer/Web/WebUtility.cs b/Apewer/Web/WebUtility.cs
index ec2ef6c..24020e1 100644
--- a/Apewer/Web/WebUtility.cs
+++ b/Apewer/Web/WebUtility.cs
@@ -154,8 +154,9 @@ namespace Apewer.Web
             var d = GetDirectIP(request);
             var f = GetForwardedIP(request);
 
-            // return string.IsNullOrEmpty(f) ? "NULL" : $"{f},NULL";
-            // return string.IsNullOrEmpty(f) ? $"{d}" : $"{f},{d}";
+            // 去除端口。
+            if (d.NotEmpty()) d = d.Split(':')[0];
+            if (f.NotEmpty()) f = f.Split(':')[0];
 
             // 没有代理,返回 Direct IP。
             if (string.IsNullOrEmpty(f)) return d;
@@ -319,8 +320,13 @@ namespace Apewer.Web
                 foreach (var key in request.Headers.Keys)
 #endif
                 {
-                    var value = request.Headers[key].ToString();
-                    sp.Add(new KeyValuePair<string, string>(key, value));
+                    try
+                    {
+                        var values = request.Headers[key];
+                        var text = values.ToString();
+                        sp.Add(new KeyValuePair<string, string>(key, text));
+                    }
+                    catch { }
                 }
             }
             return sp;
@@ -377,6 +383,20 @@ namespace Apewer.Web
 #endif
         }
 
+        /// <summary>设置响应。</summary>
+        public static string Respond(ApiResponse response, Json data, bool lower = true)
+        {
+            if (response == null) return "Response 对象无效。";
+
+            if (data != null)
+            {
+                if (lower) data = Json.ToLower(data);
+                response.Data.Reset(data);
+            }
+
+            return null;
+        }
+
         /// <summary>设置响应,当发生错误时设置响应。返回错误信息。</summary>
         public static string Respond(ApiResponse response, IList list, bool lower = true, int depth = -1, bool force = false)
         {
@@ -631,6 +651,86 @@ namespace Apewer.Web
 
         #endregion
 
+        #region Cookies
+
+        /// <summary>获取 Cookies。</summary>
+        public static StringPairs GetCookies(HttpRequest request)
+        {
+            var sp = new StringPairs();
+            if (request != null && request.Cookies != null)
+            {
+#if NETFX
+                foreach (var key in request.Cookies.AllKeys)
+                {
+                    try
+                    {
+                        var cookie = request.Cookies[key];
+                        var value = cookie.Value ?? "";
+                        sp.Add(new KeyValuePair<string, string>(key, value));
+                    }
+                    catch { }
+                }
+#else
+                foreach (var key in request.Cookies.Keys)
+                {
+                    try
+                    {
+                        var value = request.Cookies[key] ?? "";
+                        sp.Add(new KeyValuePair<string, string>(key, value));
+                    }
+                    catch { }
+                }
+#endif
+            }
+            return sp;
+        }
+
+        /// <summary>设置响应的 Cookies。</summary>
+        public static string SetCookies(HttpResponse response, IEnumerable<KeyValuePair<string, string>> cookies)
+        {
+            // experimentation_subject_id
+            if (cookies == null) return "参数 Response 无效。";
+            var count = 0;
+            foreach (var kvp in cookies)
+            {
+                count++;
+                var error = SetCookie(response, kvp.Key, kvp.Value);
+                if (error.NotEmpty()) return error;
+            }
+            return null;
+        }
+
+        /// <summary>设置响应的 Cookie。</summary>
+        public static string SetCookie(HttpResponse response, string key, string value)
+        {
+            if (response == null) return null;
+            var k = key.SafeTrim();
+            var v = value.SafeTrim();
+            if (k.IsEmpty()) return "参数 Key 无效。";
+
+            try
+            {
+#if NETFX
+                var cookie = new HttpCookie(k, v);
+                var now = DateTime.Now;
+                cookie.Expires = v.IsEmpty() ? now.AddDays(-1) : now.AddYears(1);
+                response.SetCookie(cookie);
+
+#else
+                // var options = new CookieOptions();
+                // response.Cookies.Append(key, value, options);
+                response.Cookies.Append(key, value);
+#endif
+                return null;
+            }
+            catch (Exception ex)
+            {
+                return ex.Message;
+            }
+        }
+
+        #endregion
+
 #endif
 
     }
diff --git a/Apewer/_ChangeLog.md b/Apewer/_ChangeLog.md
index 3dd4857..2a441de 100644
--- a/Apewer/_ChangeLog.md
+++ b/Apewer/_ChangeLog.md
@@ -5,6 +5,20 @@
 
 ### 最新提交
 
+### 6.1.0
+- DateTime:修正了超出范围值的问题;
+- TextUtility:新增 RenderMarkdown 方法,支持将 Markdown 文本转换为 HTML 文本;
+- TextUtility:放宽了 SafeKey 限制十六进制字符的限制,现在允许所有字母和数字;
+- Source:解析 Record 结构加入缓存,以提高性能;
+- MySql:新增 Escape 方法;
+- MySql:新增 QueryRecords 重载,支持分页查询;
+- MySql:新增 AddColumn 方法,支持增加单列;
+- MySql:新增 SafeTable 方法和 SafeColumn 方法,提供对表明和列名的检查;
+- WebAPI:支持设置 Response 的缓存过期时间(在 .NET Framework 中可能无效);
+- WebAPI:添加 Response.Respond(Json) 方法;
+- WebAPI:支持读取或设置 Cookies,且 ApiRequest 从 Cookies 中读取 Ticket;
+- WebAPI:Response.Text 现在默认在 Content-Type 中指定 UTF-8。
+
 ### 6.0.14
 - 修正了 Json.Parse 的 NULL 引用问题;
 - 修正了生成 MySQL 的 Insert 语句字段报错问题。
diff --git a/Apewer/_Class.cs b/Apewer/_Class.cs
index bf89df0..49940e4 100644
--- a/Apewer/_Class.cs
+++ b/Apewer/_Class.cs
@@ -8,16 +8,16 @@ public class Class<T> : IComparable, IComparable<T>, IComparable<Class<T>>
     private bool _equals = false;
 
     /// <summary>装箱对象。</summary>
-    public T Value { get; set; }
+    public virtual T Value { get; set; }
 
     /// <summary></summary>
-    public bool IsNull
+    public virtual bool IsNull
     {
         get { return Value == null; }
     }
 
     /// <summary></summary>
-    public bool HasValue
+    public virtual bool HasValue
     {
         get { return Value != null; }
     }
@@ -63,7 +63,7 @@ public class Class<T> : IComparable, IComparable<T>, IComparable<Class<T>>
     /// <summary></summary>
     /// <exception cref="MissingMemberException"></exception>
     /// <exception cref="NotSupportedException"></exception>
-    public int CompareTo(object obj)
+    public virtual int CompareTo(object obj)
     {
         if (obj != null && obj is T) return CompareTo((T)obj);
         if (obj != null && obj is Class<T>) return CompareTo(obj as Class<T>);
@@ -76,7 +76,7 @@ public class Class<T> : IComparable, IComparable<T>, IComparable<Class<T>>
     /// <summary></summary>
     /// <exception cref="MissingMemberException"></exception>
     /// <exception cref="NotSupportedException"></exception>
-    public int CompareTo(T other)
+    public virtual int CompareTo(T other)
     {
         if (Value == null) throw new MissingMemberException(typeof(T).FullName, nameof(Value));
         if (!(Value is IComparable)) throw new NotSupportedException();
@@ -86,7 +86,7 @@ public class Class<T> : IComparable, IComparable<T>, IComparable<Class<T>>
     /// <summary></summary>
     /// <exception cref="MissingMemberException"></exception>
     /// <exception cref="NotSupportedException"></exception>
-    public int CompareTo(Class<T> other)
+    public virtual int CompareTo(Class<T> other)
     {
         if (Value == null) throw new MissingMemberException(typeof(T).FullName, nameof(Value));
         if (other == null || !other.HasValue) return 1;
diff --git a/Apewer/_Extensions.cs b/Apewer/_Extensions.cs
index 9b41272..ae2f959 100644
--- a/Apewer/_Extensions.cs
+++ b/Apewer/_Extensions.cs
@@ -482,7 +482,7 @@ public static class Extensions
     #region Source
 
     /// <summary>为记录的 Key 属性设置新值。</summary>
-    public static void SetNewKey(this Record @this) => Record.SetNewKey(@this);
+    public static void ResetKey(this Record @this) => Record.ResetKey(@this);
 
     /// <summary>修补基本属性。</summary>
     public static void FixProperties(this Record @this) => Record.FixProperties(@this);
@@ -534,7 +534,7 @@ public static class Extensions
     public static void Error(this ApiResponse @this, Exception exception) => ApiInternals.RespondError(@this, exception);
 
     /// <summary>输出 UTF-8 文本。</summary>
-    public static void Text(this ApiResponse @this, string content, string type = "text/plain") => ApiInternals.RespondText(@this, content, type);
+    public static void Text(this ApiResponse @this, string content, string type = "text/plain; charset=utf-8") => ApiInternals.RespondText(@this, content, type);
 
     /// <summary>输出字节数组。</summary>
     public static void Binary(this ApiResponse @this, byte[] content, string type = "application/octet-stream") => ApiInternals.RespondBinary(@this, content, type);
@@ -571,6 +571,9 @@ public static class Extensions
     /// <summary>设置响应,当发生错误时设置响应。返回错误信息。</summary>
     public static string Respond(this ApiResponse @this, Record record, bool lower = true) => WebUtility.Respond(@this, record, lower);
 
+    /// <summary>设置响应,当发生错误时设置响应。返回错误信息。</summary>
+    public static string Respond(this ApiResponse @this, Json data, bool lower = true) => WebUtility.Respond(@this, data, lower);
+
 #endif
 
     #endregion