();
+
+ 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
+ [ ]*
+ (\S+?)>? # url = $2
+ [ ]*
+ \n? # maybe one newline
+ [ ]*
+ (?:
+ (?<=\s) # lookbehind for whitespace
+ [""(]
+ (.+?) # title = $3
+ ["")]
+ [ ]*
+ )? # title is optional
+ (?:\n+|\Z)", _tabWidth - 1), RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
+
+ ///
+ /// 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(@"
+ \2\s*> # closing nested tag
+ )
+ |
+ <(?!/\2\s*> # other tags with a different name
+ )
+ )*", _nestDepth);
+
+ string content2 = content.Replace(@"\2", @"\3");
+
+ // First, look for nested blocks, e.g.:
+ //
+ //
+ // 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
+ \2> # the matching end tag
+ [ ]* # trailing spaces
+ (?=\n+|\Z) # followed by a newline or end of document
+
+ | # Special version for tags of group a.
+
+ <($block_tags_a_re) # start tag = $3
+ $attr>[ ]*\n # attributes followed by >
+ $content2 # content, support nesting
+ \3> # the matching end tag
+ [ ]* # trailing spaces
+ (?=\n+|\Z) # followed by a newline or end of document
+
+ | # Special case just for
. 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 ( and <%)
+
+ [ ]{0,$less_than_tab}
+ (?s:
+ <([?%]) # $4
+ .*?
+ \4>
+ )
+ [ ]*
+ (?=\n{2,}|\Z) # followed by a blank line or end of document
+
+ )
+ )";
+
+ pattern = pattern.Replace("$less_than_tab", (_tabWidth - 1).ToString());
+ pattern = pattern.Replace("$block_tags_b_re", blockTagsB);
+ pattern = pattern.Replace("$block_tags_a_re", blockTagsA);
+ pattern = pattern.Replace("$attr", attr);
+ pattern = pattern.Replace("$content2", content2);
+ return pattern.Replace("$content", content);
+ }
+
+ ///
+ /// 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 = "" + linkText + "";
+ }
+ 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 = "" + linkText + "";
+ }
+ 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]
+ /// 
+ ///
+ 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: 
+ // 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("
+ /// 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}{0}>\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("{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