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 @@ - + Apewer Apewer Apewer - 6.0.14 + 6.1.0 - + Elivo Apewer Lab @@ -21,7 +21,7 @@ Apewer Libraries - + true bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml @@ -30,19 +30,19 @@ Library - + true true - + $(AssemblyName) - .NET Standard 2.1 NETSTD;HAVE_ASYNC;HAVE_BIG_INTEGER;$(AdditionalConstants) - + $(AssemblyName) - .NET Framework 2.0 NETFX;NET20;$(AdditionalConstants) @@ -50,7 +50,6 @@ - @@ -59,7 +58,7 @@ - + $(AssemblyName) - .NET Framework 4.0 NETFX;NET40;HAVE_BIG_INTEGER;$(AdditionalConstants) @@ -70,7 +69,6 @@ - @@ -83,7 +81,7 @@ - + $(AssemblyName) - .NET Framework 4.6.1 NETFX;NET461;NET45;HAVE_ASYNC;HAVE_BIG_INTEGER;$(AdditionalConstants) @@ -94,7 +92,6 @@ - @@ -107,7 +104,7 @@ - + $(AssemblyName) - .NET Core 3.1 NETCORE;HAVE_ASYNC;HAVE_BIG_INTEGER;$(AdditionalConstants) @@ -115,9 +112,6 @@ - - - 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(entity); } + /// 创建新的零值 DateTime 对象。 + 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 + +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 +{ + + /// + /// 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). + /// + internal class Markdown + { + private const string _version = "1.13"; + + #region Constructors and Options + + /// + /// Create a new Markdown instance using default options + /// + public Markdown() + { + } + +#if NETFX || NETCORE + + /// + /// 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) + /// + 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 + + /// + /// Create a new Markdown instance and set the options from the MarkdownOptions object. + /// + 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; + } + + /// + /// use ">" for HTML output, or " />" for XHTML output + /// + public string EmptyElementSuffix { get; set; } = " />"; + + /// + /// when false, email addresses will never be auto-linked + /// WARNING: this is a significant deviation from the markdown spec + /// + public bool LinkEmails { get; set; } = true; + + /// + /// when true, bold and italic require non-word characters on either side + /// WARNING: this is a significant deviation from the markdown spec + /// + public bool StrictBoldItalic { get; set; } = false; + + /// + /// when true, asterisks may be used for intraword emphasis + /// this does nothing if StrictBoldItalic is false + /// + public bool AsteriskIntraWordEmphasis { get; set; } = false; + + /// + /// when true, RETURN becomes a literal newline + /// WARNING: this is a significant deviation from the markdown spec + /// + public bool AutoNewLines { get; set; } = false; + + /// + /// when true, (most) bare plain URLs are auto-hyperlinked + /// WARNING: this is a significant deviation from the markdown spec + /// + public bool AutoHyperlink { get; set; } = false; + + #endregion + + /// + /// maximum nested depth of [] and () supported by the transform; implementation detail + /// + private const int _nestDepth = 6; + + /// + /// Tabs are automatically converted to spaces as part of the transform + /// this constant determines how "wide" those tabs become in spaces + /// + private const int _tabWidth = 4; + + private const string _markerUL = "[*+-]"; + private const string _markerOL = @"\d+[.]"; + + private static readonly Dictionary _escapeTable; + private static readonly Dictionary _invertedEscapeTable; + private static readonly Dictionary _backslashEscapeTable; + + private readonly Dictionary _urls = new Dictionary(); + private readonly Dictionary _titles = new Dictionary(); + private readonly Dictionary _htmlBlocks = new Dictionary(); + + private int _listLevel; + private const string AutoLinkPreventionMarker = "\x1AP"; // temporarily replaces "://" where auto-linking shouldn't happen + + /// + /// In the static constuctor we'll initialize what stays the same across all transforms. + /// + static Markdown() + { + // Table of hash values for escaped characters: + _escapeTable = new Dictionary(); + _invertedEscapeTable = new Dictionary(); + // Table of hash value for backslash escaped characters: + _backslashEscapeTable = new Dictionary(); + + 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); + } + + /// + /// current version of MarkdownSharp; + /// see http://code.google.com/p/markdownsharp/ for the latest code or to contribute + /// + public string Version + { + get { return _version; } + } + + /// + /// Transforms the provided Markdown-formatted text to HTML; + /// see http://en.wikipedia.org/wiki/Markdown + /// + /// + /// 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. + /// + 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"; + } + + /// + /// Perform transformations that form block-level tags like paragraphs, headers, and list items. + /// + 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 + //

tags around block-level tags. + text = HashHTMLBlocks(text); + + return FormParagraphs(text, unhash: unhash, createParagraphs: createParagraphs); + } + + ///

+ /// Perform transformations that occur *within* block-level tags like paragraphs, headers, and list items. + /// + 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](). + 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); + + /// + /// 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 + /// + 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

tags + grafs[i] = _leadingWhitespace.Replace(RunSpanGamut(grafs[i]), createParagraphs ? "

" : "") + (createParagraphs ? "

" : ""); + } + } + + 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; + + /// + /// Reusable pattern to match balanced [brackets]. See Friedl's + /// "Mastering Regular Expressions", 2nd Ed., pp. 328-331. + /// + 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; + + /// + /// Reusable pattern to match balanced (parens). See Friedl's + /// "Mastering Regular Expressions", 2nd Ed., pp. 328-331. + /// + 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 + [ ]* + ? # 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); + + /// + /// Strips link definitions from text, stores the URLs and titles in hash references. + /// + /// + /// ^[id]: url "optional title" + /// + 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("\"", """); + + return ""; + } + + // compiling this monster regex results in worse performance. trust me. + private static readonly Regex _blocksHtml = new Regex(GetBlockPattern(), RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace); + + /// + /// derived pretty much verbatim from PHP Markdown + /// + 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

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(@" + # 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.: + //

+ //
+ // tags for inner block must be indented. + //
+ //
+ // + // 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 `
` and stop at the first `
`. + string pattern = @" + (?> + (?> + (?<=\n) # Starting at the beginning of a line + | # or + \A\n? # the beginning of the doc + ) + ( # save in $1 + + # Match from `\n` to `\n`, handling nested tags + # in between. + + <($block_tags_b_re) # start tag = $2 + $attr> # attributes followed by > and \n + $content # content, support nesting + # 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 + # the matching end tag + [ ]* # trailing spaces + (?=\n+|\Z) # followed by a newline or end of document + + | # Special case just for
. It was easier to make a special + # case than to make the other regex more complicated. + + [ ]{0,$less_than_tab} +
# 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 ( + ) + [ ]* + (?=\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); + } + + /// + /// replaces any block-level HTML blocks with hash entries + /// + 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 + (<\?.*?\?>)| # match " + + RepeatString(@" + (<[A-Za-z\/!$](?:[^<>]|", _nestDepth - 1) + @" + (<[A-Za-z\/!$](?:[^<>]" + + RepeatString(")*>)", _nestDepth) + + " # match and ", + RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.ExplicitCapture | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); + + /// + /// returns an array of HTML tokens comprising the input string. Each token is + /// either a tag (possibly with nested, tags contained therein, such + /// as <a href="<MTFoo>">, 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. + /// + private List TokenizeHTML(string text) + { + int pos = 0; + int tagStart = 0; + var tokens = new List(); + + // 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); + + /// + /// Turn Markdown link shortcuts into HTML anchor tags + /// + /// + /// [link text](url "title") + /// [link text][id] + /// [id] + /// + 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 = ""; + } + 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 = ""; + } + 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("{0}", 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); + + /// + /// Turn Markdown image shortcuts into HTML img tags. + /// + /// + /// ![alt text][id] + /// ![alt text](url "optional title") + /// + 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("\"{1}\"", + /// Turn Markdown headers into HTML header tags + /// + /// + /// + /// Header 1 + /// ======== + /// + /// + /// Header 2 + /// -------- + /// + /// + /// # Header 1 + /// ## Header 2 + /// ## Header 2 with closing hashes ## + /// ... + /// ###### Header 6 + /// + /// + 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("{0}\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("{0}\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); + + /// + /// Turn Markdown horizontal rules into HTML hr tags + /// + /// + /// *** + /// * * * + /// --- + /// - - - + /// + private string DoHorizontalRules(string text) + { + return _horizontalRules.Replace(text, " + /// Turn Markdown lists into HTML ul and ol and li tags + /// + 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}\n", listType, start, result); + } + + /// + /// Process the contents of a single ordered or unordered list, splitting it + /// into individual list items. + /// + 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("
  • {0}
  • \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); + + /// + /// /// Turn Markdown 4-space indented code into HTML pre code blocks + /// + 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
    ", codeBlock, "\n
    \n\n"); + } + + private static readonly Regex _codeSpan = new Regex(@" + (? + /// Turn Markdown `code spans` into HTML code tags + /// + 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: + // + //

    Just type foo `bar` baz at the prompt.

    + // + // 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 `bar` ... + // + + 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("", span, ""); + } + + 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); + + /// + /// Turn Markdown *italics* and **bold** into HTML strong and em tags + /// + private string DoItalicsAndBold(string text) + { + if (!(text.Contains("*") || text.Contains("_"))) + return text; + // must go first, then + if (StrictBoldItalic) + { + if (AsteriskIntraWordEmphasis) + { + text = _semiStrictBold.Replace(text, "$1$3"); + text = _semiStrictItalic.Replace(text, "$1$3"); + } + else + { + text = _strictBold.Replace(text, "$1$3"); + text = _strictItalic.Replace(text, "$1$3"); + } + } + else + { + text = _bold.Replace(text, "$2"); + text = _italic.Replace(text, "$2"); + } + return text; + } + + /// + /// Turn markdown line breaks (two space at end of line) into HTML break tags + /// + private string DoHardBreaks(string text) + { + if (AutoNewLines) + { + return Regex.Replace(text, @"\n", string.Format("[ ]? # '>' at the start of a line + .+\n # rest of the first line + (.+\n)* # subsequent consecutive lines + \n* # blanks + )+ + )", RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline | RegexOptions.Compiled); + + /// + /// Turn Markdown > quoted blocks into HTML blockquote blocks + /// + 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
     content, so we need to fix that:
    +            bq = Regex.Replace(bq, @"(\s*
    .+?
    )", new MatchEvaluator(BlockQuoteEvaluator2), RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline); + + bq = string.Format("
    \n{0}\n
    ", 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 + // 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); + + /// + /// Turn angle-delimited URLs into HTML anchor tags + /// + /// + /// <http://www.example.com> + /// + 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 + // *except* for the case + text = _autolinkBare.Replace(text, HandleTrailingParens); + } + + // Hyperlinks: + text = Regex.Replace(text, "<((https?|ftp):[^'\">\\s]+)>", new MatchEvaluator(HyperlinkEvaluator)); + + if (LinkEmails) + { + // Email addresses: or + // 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("{1}", 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.: + // + // foo + // @example.com + // + // Based by a filter by Matthew Wickline, posted to the BBEdit-Talk + // mailing list: + // + email = "mailto:" + email; + + // leave ':' alone (to spot mailto: later) + email = EncodeEmailAddress(email); + + email = string.Format("{0}", 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); + + /// + /// Remove one level of line-leading spaces + /// + private string Outdent(string block) + { + return _outDent.Replace(block, ""); + } + + #region Encoding and Normalization + + /// + /// encodes email address randomly + /// roughly 10% raw, 45% hex, 45% dec + /// note that @ is always encoded and : never is + /// + 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); // m + else + sb.AppendFormat("&#{0};", (int)c); // m + } + return sb.ToString(); + } + + private static readonly Regex _codeEncoder = new Regex(@"&|<|>|\\|\*|_|\{|\}|\[|\]", RegexOptions.Compiled); + + /// + /// Encode/escape certain Markdown characters inside code blocks and spans where they are literals + /// + 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 "&"; + // Do the angle bracket song and dance + case "<": + return "<"; + case ">": + return ">"; + // 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); + + /// + /// Encode any ampersands (that aren't part of an HTML entity) and left or right angle brackets + /// + private string EncodeAmpsAndAngles(string s) + { + s = _amps.Replace(s, "&"); + return _angles.Replace(s, "<"); + } + + private static readonly Regex _backslashEscapes; + + /// + /// Encodes any escaped characters such as \`, \*, \[ etc + /// + 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. + + /// + /// swap back in all the special characters we've hidden + /// + private string Unescape(string s) => _unescapes.Replace(s, new MatchEvaluator(UnescapeEvaluator)); + + private string UnescapeEvaluator(Match match) => _invertedEscapeTable[match.Value]; + + /// + /// escapes Bold [ * ] and Italic [ _ ] characters + /// + private string EscapeBoldItalic(string s) + { + s = s.Replace("*", _escapeTable["*"]); + return s.Replace("_", _escapeTable["_"]); + } + + private static string AttributeEncode(string s) + { + return s.Replace(">", ">").Replace("<", "<").Replace("\"", """).Replace("'", "'"); + } + + private static string AttributeSafeUrl(string s) + { + s = AttributeEncode(s); + foreach (var c in "*_:()[]") + s = s.Replace(c.ToString(), _escapeTable[c.ToString()]); + return s; + } + + /// + /// Within tags -- meaning between < and > -- 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. + /// + 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("(?=.)", _escapeTable["`"]); + value = EscapeBoldItalic(value); + } + + sb.Append(value); + } + + return sb.ToString(); + } + + /// + /// 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 + /// + 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 + + /// + /// this is to emulate what's evailable in PHP + /// + 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 +{ + + /// + /// Options for configuring MarkdownSharp. + /// + internal class MarkdownOptions + { + /// + /// when true, (most) bare plain URLs are auto-hyperlinked + /// WARNING: this is a significant deviation from the markdown spec + /// + public bool AutoHyperlink { get; set; } + + /// + /// when true, RETURN becomes a literal newline + /// WARNING: this is a significant deviation from the markdown spec + /// + public bool AutoNewlines { get; set; } + + /// + /// use ">" for HTML output, or " />" for XHTML output + /// + public string EmptyElementSuffix { get; set; } + + /// + /// when false, email addresses will never be auto-linked + /// WARNING: this is a significant deviation from the markdown spec + /// + public bool LinkEmails { get; set; } + + /// + /// when true, bold and italic require non-word characters on either side + /// WARNING: this is a significant deviation from the markdown spec + /// + public bool StrictBoldItalic { get; set; } + + /// + /// when true, asterisks may be used for intraword emphasis + /// this does nothing if StrictBoldItalic is false + /// + 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 /// 所有 GUID 中的字符,包含字母、数字和连字符。 public const string GuidCollection = "0123456789ABCDEFabcdef-"; + /// 所有主键字符。 + public const string KeyCollection = "0123456789abcdefghijklmnopqrstuvwxyz"; + /// 所有十六进制字符。 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 /// Json。 [Serializable] public class Json +#if !NET20 + : DynamicObject +#endif { #region 配置。 @@ -1698,6 +1701,48 @@ namespace Apewer #endregion + #region Dynamic + +#if !NET20 + + /// + 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; + } + + /// + 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(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(); } /// 从 NTP 服务器获取 UTC 时间。 @@ -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; + /// 使用自动的列名称。当类型为 VarChar 或 NVarChar 时必须指定长度。 /// public ColumnAttribute(ColumnType type = ColumnType.NVarChar255, int length = 0) @@ -41,19 +43,55 @@ namespace Apewer.Source } /// 属性。 - public PropertyInfo Property { get { return _property; } internal set { _property = value; } } + public PropertyInfo Property + { + get { return _property; } + internal set + { + if (_locked) return; + _property = value; + } + } /// 字段名。 - public string Field { get { return _field; } set { _field = value; } } + public string Field + { + get { return _field; } + set + { + if (_locked) return; + _field = value; + } + } /// 指定字段的最大长度。 - public int Length { get { return _length; } set { _length = value; } } + public int Length + { + get { return _length; } + set + { + if (_locked) return; + _length = value; + } + } /// 字段类型。 - public ColumnType Type { get { return _type; } set { _type = value; } } + public ColumnType Type + { + get { return _type; } + set + { + if (_locked) return; + _type = value; + } + } /// Independent 特性。 - public bool Independent { get { return _independent; } internal set { _independent = value; } } + public bool Independent + { + get { return _independent; } + internal set { _independent = value; } + } /// private void New(string field, ColumnType type, int length, bool underline) @@ -77,6 +115,12 @@ namespace Apewer.Source } } + /// 锁定属性,阻止修改。 + 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()); } - /// /// 插入记录。成功时候返回空字符串,发生异常时返回异常信息。 - 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 } } - /// + /// 获取记录。 public Result QueryRecord(string key, long flag = 0) where T : Record { var k = TextUtility.RestrictGuid(key); @@ -515,6 +513,25 @@ namespace Apewer.Source } } + /// 获取记录。 + /// 要跳过的记录数,可用最小值为 0。 + /// 要获取的记录数,可用最小值为 1。 + public Result> QueryRecords(int skip, int count) where T : Record + { + try + { + if (skip < 0) return new Result>(new ArgumentOutOfRangeException(nameof(skip))); + if (count < 1) return new Result>(new ArgumentOutOfRangeException(nameof(count))); + var tableName = TableStructure.ParseTable(typeof(T)).Name; + var sql = $"select * from `{tableName}` where _flag = 1 limit {skip}, {count}; "; + return QueryRecords(sql); + } + catch (Exception exception) + { + return new Result>(exception); + } + } + /// 查询有效的 Key 值。 public Result> QueryKeys(Type model, long flag = 0) { @@ -562,10 +579,111 @@ namespace Apewer.Source } } + /// 对表添加列,返回错误信息。 + /// 记录类型。 + /// 列名称。 + /// 字段类型。 + /// 字段长度,仅对 VarChar 和 NVarChar 类型有效。 + /// + public string AddColumn(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 + /// 对文本转义,符合 SQL 安全性。可根据字段类型限制 UTF-8 字节数,默认为 0 时不限制字节数。 + 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; + } + + /// + 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; + } + + /// + public static string SafeColumn(string column) => SafeTable(column); + /// /// /// 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) + /// 重置主键。 + internal static void ResetKey(Record record) { if (record == null) return; record.Key = GenerateKey(); } - internal static string GenerateKey() => Guid.NewGuid().ToString().ToLower().Replace("-", ""); + /// 生成新主键。 + 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; + /// public TableAttribute(string name = null) { @@ -27,18 +29,26 @@ namespace Apewer.Source _name = TableStructure.RestrictName(name, underline); } - /// + /// 表名。 public string Name { get { return _name; } - set { _name = TableStructure.RestrictName(value, false); } + set + { + if (_locked) return; + _name = TableStructure.RestrictName(value, false); + } } /// public bool Independent { get { return _independent; } - internal set { _independent = value; } + internal set + { + if (_locked) return; + _independent = value; + } } /// @@ -47,6 +57,12 @@ namespace Apewer.Source return _name.GetHashCode(); } + /// 锁定属性,阻止修改。 + 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() { } - /// - public bool Independent { get { return _independent; } private set { _independent = value; } } + /// 不依赖 Record 公共属性。 + public bool Independent + { + get { return _independent; } + private set { _independent = value; } + } - /// - public string Table { get { return _tablename; } set { _tablename = value ?? ""; } } + /// 表名称。 + public string Table + { + get { return _tablename; } + private set { _tablename = value ?? ""; } + } - /// - public Dictionary Columns { get { return _columns; } set { _columns = value; } } + /// 列信息。 + public Dictionary Columns + { + get + { + if (_locked) + { + var copy = new Dictionary(_columns.Count); + foreach (var c in _columns) copy.Add(c.Key, c.Value); + return copy; + } + return _columns; + } + private set { _columns = value; } + } + + /// 锁定属性,阻止修改。 + public void Lock() + { + _locked = true; + foreach (var c in _columns) c.Value.Lock(); + } + + #region cache + + private static Dictionary _tsc = new Dictionary(); + + private static Dictionary _tac = new Dictionary(); + + #endregion #region static /// /// /// - 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; } /// /// /// - public static TableStructure ParseModel(Type model) + public static TableStructure ParseModel(bool useCache = true) where T : IRecord + { + return ParseModel(typeof(T), useCache); + } + + /// + /// + /// + 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; + } + + /// + /// + /// " + public static TableAttribute ParseTable(bool useCache = true) where T : IRecord + { + return ParseTable(typeof(T), useCache); } /// /// /// " - 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(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; } + /// 渲染 Markdown 文本为 HTML 文本。 + public static string RenderMarkdown(string markdown) => MarkdownSharp.Demo.ToHtml(markdown); + /// 合并用于启动进程的参数。 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(); 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 } /// 输出 UTF-8 文本。 - 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"; } /// 输出字节数组。 @@ -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) + /// 设置缓存时间,单位为秒,最大为 2592000 秒(30 天)。 + 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 + } + + /// 设置响应的 Cookies。 + public static void SetCookies(HttpResponse response, IEnumerable> 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 + + // 可以是除了控制字符 (CTLs)、空格 (spaces) 或制表符 (tab)之外的任何 US-ASCII 字符。 + // 同时不能包含以下分隔字符: ( ) < > @ , ; : \ " / [ ] ? = { }. + + // 是可选的,如果存在的话,那么需要包含在双引号里面。 + // 支持除了控制字符(CTLs)、空格(whitespace)、双引号(double quotes)、逗号(comma)、分号(semicolon)以及反斜线(backslash)之外的任意 US-ASCII 字符。 + + // 关于编码:许多应用会对 cookie 值按照URL编码(URL encoding)规则进行编码,但是按照 RFC 规范,这不是必须的。不过满足规范中对于 所允许使用的字符的要求是有用的。 + + if (cookies == null) return; + + var ps = new List(); + 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(); } + /// 获取用于执行 API 的程序集。 + public static Assembly[] GetAssemblies() + { + var list = ApiInternals.Assemblies; + if (list == null) return new Assembly[0]; + return list.ToArray(); + } + #if NETFX /// 解析请求,获取指定程序集中定义的入口,并执行。指定程序集为 NULL 值时,将自动从应用程序域获取所有 WebAPI 入口。 @@ -467,8 +497,7 @@ namespace Apewer.Web /// 设置 Kestrel 的 API 入口。 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 /// HTTP 头。 public StringPairs Headers { get; set; } = new StringPairs(); + /// Cookies。 + 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(); - /// + /// HTTP 头。 public StringPairs Headers { get; set; } = new StringPairs(); + /// Cookies。 + 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; } + /// 设置缓存过期时间,单位为秒。默认值:0,立即过期,不缓存。 + /// 在 .NET Framework 中,此设置可能无效。 + 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(key, value)); + try + { + var values = request.Headers[key]; + var text = values.ToString(); + sp.Add(new KeyValuePair(key, text)); + } + catch { } } } return sp; @@ -377,6 +383,20 @@ namespace Apewer.Web #endif } + /// 设置响应。 + 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; + } + /// 设置响应,当发生错误时设置响应。返回错误信息。 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 + + /// 获取 Cookies。 + 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(key, value)); + } + catch { } + } +#else + foreach (var key in request.Cookies.Keys) + { + try + { + var value = request.Cookies[key] ?? ""; + sp.Add(new KeyValuePair(key, value)); + } + catch { } + } +#endif + } + return sp; + } + + /// 设置响应的 Cookies。 + public static string SetCookies(HttpResponse response, IEnumerable> 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; + } + + /// 设置响应的 Cookie。 + 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 : IComparable, IComparable, IComparable> private bool _equals = false; /// 装箱对象。 - public T Value { get; set; } + public virtual T Value { get; set; } /// - public bool IsNull + public virtual bool IsNull { get { return Value == null; } } /// - public bool HasValue + public virtual bool HasValue { get { return Value != null; } } @@ -63,7 +63,7 @@ public class Class : IComparable, IComparable, IComparable> /// /// /// - 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) return CompareTo(obj as Class); @@ -76,7 +76,7 @@ public class Class : IComparable, IComparable, IComparable> /// /// /// - 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 : IComparable, IComparable, IComparable> /// /// /// - public int CompareTo(Class other) + public virtual int CompareTo(Class 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 /// 为记录的 Key 属性设置新值。 - public static void SetNewKey(this Record @this) => Record.SetNewKey(@this); + public static void ResetKey(this Record @this) => Record.ResetKey(@this); /// 修补基本属性。 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); /// 输出 UTF-8 文本。 - 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); /// 输出字节数组。 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 /// 设置响应,当发生错误时设置响应。返回错误信息。 public static string Respond(this ApiResponse @this, Record record, bool lower = true) => WebUtility.Respond(@this, record, lower); + /// 设置响应,当发生错误时设置响应。返回错误信息。 + public static string Respond(this ApiResponse @this, Json data, bool lower = true) => WebUtility.Respond(@this, data, lower); + #endif #endregion