I have snippets of Html stored in a table. Not entire pages, no tags or the like, just basic formatting.
I would like to be able to display that Html as text only, no formatting, on a given page (actually just the first 30 – 50 characters but that’s the easy bit).
How do I place the “text” within that Html into a string as straight text?
So this piece of code.
<b>Hello World.</b><br/><p><i>Is there anyone out there?</i><p>
Becomes:
Hello World. Is there anyone out there?
Answers:
Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.
Method 1
The MIT licensed HtmlAgilityPack has in one of its samples a method that converts from HTML to plain text.
var plainText = HtmlUtilities.ConvertToPlainText(string html);
Feed it an HTML string like
<b>hello, <i>world!</i></b>
And you’ll get a plain text result like:
hello world!
Method 2
I could not use HtmlAgilityPack, so I wrote a second best solution for myself
private static string HtmlToPlainText(string html) { const string tagWhiteSpace = @"(>|$)(W|n|r)+<";//matches one or more (white space or line breaks) between '>' and '<' const string stripFormatting = @"<[^>]*(>|$)";//match any character between '<' and '>', even when end tag is missing const string lineBreak = @"<(br|BR)s{0,1}/{0,1}>";//matches: <br>,<br/>,<br />,<BR>,<BR/>,<BR /> var lineBreakRegex = new Regex(lineBreak, RegexOptions.Multiline); var stripFormattingRegex = new Regex(stripFormatting, RegexOptions.Multiline); var tagWhiteSpaceRegex = new Regex(tagWhiteSpace, RegexOptions.Multiline); var text = html; //Decode html specific characters text = System.Net.WebUtility.HtmlDecode(text); //Remove tag whitespace/line breaks text = tagWhiteSpaceRegex.Replace(text, "><"); //Replace <br /> with line breaks text = lineBreakRegex.Replace(text, Environment.NewLine); //Strip formatting text = stripFormattingRegex.Replace(text, string.Empty); return text; }
Method 3
If you are talking about tag stripping, it is relatively straight forward if you don’t have to worry about things like <script>
tags. If all you need to do is display the text without the tags you can accomplish that with a regular expression:
<[^>]*>
If you do have to worry about <script>
tags and the like then you’ll need something a bit more powerful then regular expressions because you need to track state, omething more like a Context Free Grammar (CFG). Althought you might be able to accomplish it with ‘Left To Right’ or non-greedy matching.
If you can use regular expressions there are many web pages out there with good info:
- http://weblogs.asp.net/rosherove/archive/2003/05/13/6963.aspx
- http://www.google.com/search?hl=en&q=html+tag+stripping+&btnG=Search
If you need the more complex behaviour of a CFG I would suggest using a third party tool, unfortunately I don’t know of a good one to recommend.
Method 4
HTTPUtility.HTMLEncode()
is meant to handle encoding HTML tags as strings. It takes care of all the heavy lifting for you. From the MSDN Documentation:
If characters such as blanks and punctuation are passed in an HTTP stream, they might be misinterpreted at the receiving end. HTML encoding converts characters that are not allowed in HTML into character-entity equivalents; HTML decoding reverses the encoding. For example, when embedded in a block of text, the characters
<
and>
, are encoded as<
and>
for HTTP transmission.
HTTPUtility.HTMLEncode()
method, detailed here:
public static void HtmlEncode( string s, TextWriter output )
Usage:
String TestString = "This is a <Test String>."; StringWriter writer = new StringWriter(); Server.HtmlEncode(TestString, writer); String EncodedString = writer.ToString();
Method 5
Three Step Process for converting HTML into Plain Text
First You need to Install Nuget Package For HtmlAgilityPack
Second Create This class
public class HtmlToText { public HtmlToText() { } public string Convert(string path) { HtmlDocument doc = new HtmlDocument(); doc.Load(path); StringWriter sw = new StringWriter(); ConvertTo(doc.DocumentNode, sw); sw.Flush(); return sw.ToString(); } public string ConvertHtml(string html) { HtmlDocument doc = new HtmlDocument(); doc.LoadHtml(html); StringWriter sw = new StringWriter(); ConvertTo(doc.DocumentNode, sw); sw.Flush(); return sw.ToString(); } private void ConvertContentTo(HtmlNode node, TextWriter outText) { foreach(HtmlNode subnode in node.ChildNodes) { ConvertTo(subnode, outText); } } public void ConvertTo(HtmlNode node, TextWriter outText) { string html; switch(node.NodeType) { case HtmlNodeType.Comment: // don't output comments break; case HtmlNodeType.Document: ConvertContentTo(node, outText); break; case HtmlNodeType.Text: // script and style must not be output string parentName = node.ParentNode.Name; if ((parentName == "script") || (parentName == "style")) break; // get text html = ((HtmlTextNode)node).Text; // is it in fact a special closing node output as text? if (HtmlNode.IsOverlappedClosingElement(html)) break; // check the text is meaningful and not a bunch of whitespaces if (html.Trim().Length > 0) { outText.Write(HtmlEntity.DeEntitize(html)); } break; case HtmlNodeType.Element: switch(node.Name) { case "p": // treat paragraphs as crlf outText.Write("rn"); break; } if (node.HasChildNodes) { ConvertContentTo(node, outText); } break; } } }
By using above class with reference to Judah Himango’s answer
Third you need to create the Object of above class and Use ConvertHtml(HTMLContent)
Method for converting HTML into Plain Text rather than ConvertToPlainText(string html);
HtmlToText htt=new HtmlToText(); var plainText = htt.ConvertHtml(HTMLContent);
Method 6
To add to vfilby’s answer, you can just perform a RegEx replace within your code; no new classes are necessary. In case other newbies like myself stumple upon this question.
using System.Text.RegularExpressions;
Then…
private string StripHtml(string source) { string output; //get rid of HTML tags output = Regex.Replace(source, "<[^>]*>", string.Empty); //get rid of multiple blank lines output = Regex.Replace(output, @"^s*$n", string.Empty, RegexOptions.Multiline); return output; }
Method 7
It has limitation that not collapsing long inline whitespace, but it is definitely portable and respects layout like webbrowser.
static string HtmlToPlainText(string html) { string buf; string block = "address|article|aside|blockquote|canvas|dd|div|dl|dt|" + "fieldset|figcaption|figure|footer|form|h\d|header|hr|li|main|nav|" + "noscript|ol|output|p|pre|section|table|tfoot|ul|video"; string patNestedBlock = $"(\s*?</?({block})[^>]*?>)+\s*"; buf = Regex.Replace(html, patNestedBlock, "n", RegexOptions.IgnoreCase); // Replace br tag to newline. buf = Regex.Replace(buf, @"<(br)[^>]*>", "n", RegexOptions.IgnoreCase); // (Optional) remove styles and scripts. buf = Regex.Replace(buf, @"<(script|style)[^>]*?>.*?</1>", "", RegexOptions.Singleline); // Remove all tags. buf = Regex.Replace(buf, @"<[^>]*(>|$)", "", RegexOptions.Multiline); // Replace HTML entities. buf = WebUtility.HtmlDecode(buf); return buf; }
Method 8
I think the easiest way is to make a ‘string’ extension method (based on what user Richard have suggested):
using System; using System.Text.RegularExpressions; public static class StringHelpers { public static string StripHTML(this string HTMLText) { var reg = new Regex("<[^>]+>", RegexOptions.IgnoreCase); return reg.Replace(HTMLText, ""); } }
Then just use this extension method on any ‘string’ variable in your program:
var yourHtmlString = "<div class="someclass"><h2>yourHtmlText</h2></span>"; var yourTextString = yourHtmlString.StripHTML();
I use this extension method to convert html formated comments to plain text so it will be displayed correctly on a crystal report, and it works perfect!
Method 9
The simplest way I found:
HtmlFilter.ConvertToPlainText(html);
The HtmlFilter class is located in Microsoft.TeamFoundation.WorkItemTracking.Controls.dll
The dll can be found in folder like this:
%ProgramFiles%Common Filesmicrosoft sharedTeam Foundation Server14.0
In VS 2015, the dll also requires reference to Microsoft.TeamFoundation.WorkItemTracking.Common.dll, located in the same folder.
Method 10
There not a method with the name ‘ConvertToPlainText’ in the HtmlAgilityPack but you can convert a html string to CLEAR string with :
HtmlDocument doc = new HtmlDocument(); doc.LoadHtml(htmlString); var textString = doc.DocumentNode.InnerText; Regex.Replace(textString , @"<(.|n)*?>", string.Empty).Replace(" ", "");
Thats works for me. BUT I DONT FIND A METHOD WITH NAME ‘ConvertToPlainText’ IN ‘HtmlAgilityPack’.
Method 11
I had the same question, just my html had a simple pre-known layout, like:
<DIV><P>abc</P><P>def</P></DIV>
So I ended up using such simple code:
string.Join (Environment.NewLine, XDocument.Parse (html).Root.Elements ().Select (el => el.Value))
Which outputs:
abc def
Method 12
I have faced similar problem and found best solution . Below code works perfect for me.
private string ConvertHtml_Totext(string source) { try { string result; // Remove HTML Development formatting // Replace line breaks with space // because browsers inserts space result = source.Replace("r", " "); // Replace line breaks with space // because browsers inserts space result = result.Replace("n", " "); // Remove step-formatting result = result.Replace("t", string.Empty); // Remove repeating spaces because browsers ignore them result = System.Text.RegularExpressions.Regex.Replace(result, @"( )+", " "); // Remove the header (prepare first by clearing attributes) result = System.Text.RegularExpressions.Regex.Replace(result, @"<( )*head([^>])*>","<head>", System.Text.RegularExpressions.RegexOptions.IgnoreCase); result = System.Text.RegularExpressions.Regex.Replace(result, @"(<( )*(/)( )*head( )*>)","</head>", System.Text.RegularExpressions.RegexOptions.IgnoreCase); result = System.Text.RegularExpressions.Regex.Replace(result, "(<head>).*(</head>)",string.Empty, System.Text.RegularExpressions.RegexOptions.IgnoreCase); // remove all scripts (prepare first by clearing attributes) result = System.Text.RegularExpressions.Regex.Replace(result, @"<( )*script([^>])*>","<script>", System.Text.RegularExpressions.RegexOptions.IgnoreCase); result = System.Text.RegularExpressions.Regex.Replace(result, @"(<( )*(/)( )*script( )*>)","</script>", System.Text.RegularExpressions.RegexOptions.IgnoreCase); //result = System.Text.RegularExpressions.Regex.Replace(result, // @"(<script>)([^(<script>.</script>)])*(</script>)", // string.Empty, // System.Text.RegularExpressions.RegexOptions.IgnoreCase); result = System.Text.RegularExpressions.Regex.Replace(result, @"(<script>).*(</script>)",string.Empty, System.Text.RegularExpressions.RegexOptions.IgnoreCase); // remove all styles (prepare first by clearing attributes) result = System.Text.RegularExpressions.Regex.Replace(result, @"<( )*style([^>])*>","<style>", System.Text.RegularExpressions.RegexOptions.IgnoreCase); result = System.Text.RegularExpressions.Regex.Replace(result, @"(<( )*(/)( )*style( )*>)","</style>", System.Text.RegularExpressions.RegexOptions.IgnoreCase); result = System.Text.RegularExpressions.Regex.Replace(result, "(<style>).*(</style>)",string.Empty, System.Text.RegularExpressions.RegexOptions.IgnoreCase); // insert tabs in spaces of <td> tags result = System.Text.RegularExpressions.Regex.Replace(result, @"<( )*td([^>])*>","t", System.Text.RegularExpressions.RegexOptions.IgnoreCase); // insert line breaks in places of <BR> and <LI> tags result = System.Text.RegularExpressions.Regex.Replace(result, @"<( )*br( )*>","r", System.Text.RegularExpressions.RegexOptions.IgnoreCase); result = System.Text.RegularExpressions.Regex.Replace(result, @"<( )*li( )*>","r", System.Text.RegularExpressions.RegexOptions.IgnoreCase); // insert line paragraphs (double line breaks) in place // if <P>, <DIV> and <TR> tags result = System.Text.RegularExpressions.Regex.Replace(result, @"<( )*div([^>])*>","rr", System.Text.RegularExpressions.RegexOptions.IgnoreCase); result = System.Text.RegularExpressions.Regex.Replace(result, @"<( )*tr([^>])*>","rr", System.Text.RegularExpressions.RegexOptions.IgnoreCase); result = System.Text.RegularExpressions.Regex.Replace(result, @"<( )*p([^>])*>","rr", System.Text.RegularExpressions.RegexOptions.IgnoreCase); // Remove remaining tags like <a>, links, images, // comments etc - anything that's enclosed inside < > result = System.Text.RegularExpressions.Regex.Replace(result, @"<[^>]*>",string.Empty, System.Text.RegularExpressions.RegexOptions.IgnoreCase); // replace special characters: result = System.Text.RegularExpressions.Regex.Replace(result, @" "," ", System.Text.RegularExpressions.RegexOptions.IgnoreCase); result = System.Text.RegularExpressions.Regex.Replace(result, @"•"," * ", System.Text.RegularExpressions.RegexOptions.IgnoreCase); result = System.Text.RegularExpressions.Regex.Replace(result, @"‹","<", System.Text.RegularExpressions.RegexOptions.IgnoreCase); result = System.Text.RegularExpressions.Regex.Replace(result, @"›",">", System.Text.RegularExpressions.RegexOptions.IgnoreCase); result = System.Text.RegularExpressions.Regex.Replace(result, @"™","(tm)", System.Text.RegularExpressions.RegexOptions.IgnoreCase); result = System.Text.RegularExpressions.Regex.Replace(result, @"⁄","/", System.Text.RegularExpressions.RegexOptions.IgnoreCase); result = System.Text.RegularExpressions.Regex.Replace(result, @"<","<", System.Text.RegularExpressions.RegexOptions.IgnoreCase); result = System.Text.RegularExpressions.Regex.Replace(result, @">",">", System.Text.RegularExpressions.RegexOptions.IgnoreCase); result = System.Text.RegularExpressions.Regex.Replace(result, @"©","(c)", System.Text.RegularExpressions.RegexOptions.IgnoreCase); result = System.Text.RegularExpressions.Regex.Replace(result, @"®","(r)", System.Text.RegularExpressions.RegexOptions.IgnoreCase); // Remove all others. More can be added, see // http://hotwired.lycos.com/webmonkey/reference/special_characters/ result = System.Text.RegularExpressions.Regex.Replace(result, @"&(.{2,6});", string.Empty, System.Text.RegularExpressions.RegexOptions.IgnoreCase); // for testing //System.Text.RegularExpressions.Regex.Replace(result, // this.txtRegex.Text,string.Empty, // System.Text.RegularExpressions.RegexOptions.IgnoreCase); // make line breaking consistent result = result.Replace("n", "r"); // Remove extra line breaks and tabs: // replace over 2 breaks with 2 and over 4 tabs with 4. // Prepare first to remove any whitespaces in between // the escaped characters and remove redundant tabs in between line breaks result = System.Text.RegularExpressions.Regex.Replace(result, "(r)( )+(r)","rr", System.Text.RegularExpressions.RegexOptions.IgnoreCase); result = System.Text.RegularExpressions.Regex.Replace(result, "(t)( )+(t)","tt", System.Text.RegularExpressions.RegexOptions.IgnoreCase); result = System.Text.RegularExpressions.Regex.Replace(result, "(t)( )+(r)","tr", System.Text.RegularExpressions.RegexOptions.IgnoreCase); result = System.Text.RegularExpressions.Regex.Replace(result, "(r)( )+(t)","rt", System.Text.RegularExpressions.RegexOptions.IgnoreCase); // Remove redundant tabs result = System.Text.RegularExpressions.Regex.Replace(result, "(r)(t)+(r)","rr", System.Text.RegularExpressions.RegexOptions.IgnoreCase); // Remove multiple tabs following a line break with just one tab result = System.Text.RegularExpressions.Regex.Replace(result, "(r)(t)+","rt", System.Text.RegularExpressions.RegexOptions.IgnoreCase); // Initial replacement target string for line breaks string breaks = "rrr"; // Initial replacement target string for tabs string tabs = "ttttt"; for (int index=0; index<result.Length; index++) { result = result.Replace(breaks, "rr"); result = result.Replace(tabs, "tttt"); breaks = breaks + "r"; tabs = tabs + "t"; } // That's it. return result; } catch { MessageBox.Show("Error"); return source; }
}
Escape characters such as n and r had to be removed first because they cause regexes to cease working as expected.
Moreover, to make the result string display correctly in the textbox, one might need to split it up and set textbox’s Lines property instead of assigning to Text property.
this.txtResult.Lines =
StripHTML(this.txtSource.Text).Split(“r”.ToCharArray());
Source : https://www.codeproject.com/Articles/11902/Convert-HTML-to-Plain-Text-2
Method 13
If you have data that has HTML tags and you want to display it so that a person can SEE the tags, use HttpServerUtility::HtmlEncode.
If you have data that has HTML tags in it and you want the user to see the tags rendered, then display the text as is. If the text represents an entire web page, use an IFRAME for it.
If you have data that has HTML tags and you want to strip out the tags and just display the unformatted text, use a regular expression.
Method 14
Depends on what you mean by “html.” The most complex case would be complete web pages. That’s also the easiest to handle, since you can use a text-mode web browser. See the Wikipedia article listing web browsers, including text mode browsers. Lynx is probably the best known, but one of the others may be better for your needs.
Method 15
Did not write but an using:
using HtmlAgilityPack; using System; using System.IO; using System.Text.RegularExpressions; namespace foo { //small but important modification to class https://github.com/zzzprojects/html-agility-pack/blob/master/src/Samples/Html2Txt/HtmlConvert.cs public static class HtmlToText { public static string Convert(string path) { HtmlDocument doc = new HtmlDocument(); doc.Load(path); return ConvertDoc(doc); } public static string ConvertHtml(string html) { HtmlDocument doc = new HtmlDocument(); doc.LoadHtml(html); return ConvertDoc(doc); } public static string ConvertDoc(HtmlDocument doc) { using (StringWriter sw = new StringWriter()) { ConvertTo(doc.DocumentNode, sw); sw.Flush(); return sw.ToString(); } } internal static void ConvertContentTo(HtmlNode node, TextWriter outText, PreceedingDomTextInfo textInfo) { foreach (HtmlNode subnode in node.ChildNodes) { ConvertTo(subnode, outText, textInfo); } } public static void ConvertTo(HtmlNode node, TextWriter outText) { ConvertTo(node, outText, new PreceedingDomTextInfo(false)); } internal static void ConvertTo(HtmlNode node, TextWriter outText, PreceedingDomTextInfo textInfo) { string html; switch (node.NodeType) { case HtmlNodeType.Comment: // don't output comments break; case HtmlNodeType.Document: ConvertContentTo(node, outText, textInfo); break; case HtmlNodeType.Text: // script and style must not be output string parentName = node.ParentNode.Name; if ((parentName == "script") || (parentName == "style")) { break; } // get text html = ((HtmlTextNode)node).Text; // is it in fact a special closing node output as text? if (HtmlNode.IsOverlappedClosingElement(html)) { break; } // check the text is meaningful and not a bunch of whitespaces if (html.Length == 0) { break; } if (!textInfo.WritePrecedingWhiteSpace || textInfo.LastCharWasSpace) { html = html.TrimStart(); if (html.Length == 0) { break; } textInfo.IsFirstTextOfDocWritten.Value = textInfo.WritePrecedingWhiteSpace = true; } outText.Write(HtmlEntity.DeEntitize(Regex.Replace(html.TrimEnd(), @"s{2,}", " "))); if (textInfo.LastCharWasSpace = char.IsWhiteSpace(html[html.Length - 1])) { outText.Write(' '); } break; case HtmlNodeType.Element: string endElementString = null; bool isInline; bool skip = false; int listIndex = 0; switch (node.Name) { case "nav": skip = true; isInline = false; break; case "body": case "section": case "article": case "aside": case "h1": case "h2": case "header": case "footer": case "address": case "main": case "div": case "p": // stylistic - adjust as you tend to use if (textInfo.IsFirstTextOfDocWritten) { outText.Write("rn"); } endElementString = "rn"; isInline = false; break; case "br": outText.Write("rn"); skip = true; textInfo.WritePrecedingWhiteSpace = false; isInline = true; break; case "a": if (node.Attributes.Contains("href")) { string href = node.Attributes["href"].Value.Trim(); if (node.InnerText.IndexOf(href, StringComparison.InvariantCultureIgnoreCase) == -1) { endElementString = "<" + href + ">"; } } isInline = true; break; case "li": if (textInfo.ListIndex > 0) { outText.Write("rn{0}.t", textInfo.ListIndex++); } else { outText.Write("rn*t"); //using '*' as bullet char, with tab after, but whatever you want eg "t->", if utf-8 0x2022 } isInline = false; break; case "ol": listIndex = 1; goto case "ul"; case "ul": //not handling nested lists any differently at this stage - that is getting close to rendering problems endElementString = "rn"; isInline = false; break; case "img": //inline-block in reality if (node.Attributes.Contains("alt")) { outText.Write('[' + node.Attributes["alt"].Value); endElementString = "]"; } if (node.Attributes.Contains("src")) { outText.Write('<' + node.Attributes["src"].Value + '>'); } isInline = true; break; default: isInline = true; break; } if (!skip && node.HasChildNodes) { ConvertContentTo(node, outText, isInline ? textInfo : new PreceedingDomTextInfo(textInfo.IsFirstTextOfDocWritten) { ListIndex = listIndex }); } if (endElementString != null) { outText.Write(endElementString); } break; } } } internal class PreceedingDomTextInfo { public PreceedingDomTextInfo(BoolWrapper isFirstTextOfDocWritten) { IsFirstTextOfDocWritten = isFirstTextOfDocWritten; } public bool WritePrecedingWhiteSpace { get; set; } public bool LastCharWasSpace { get; set; } public readonly BoolWrapper IsFirstTextOfDocWritten; public int ListIndex { get; set; } } internal class BoolWrapper { public BoolWrapper() { } public bool Value { get; set; } public static implicit operator bool(BoolWrapper boolWrapper) { return boolWrapper.Value; } public static implicit operator BoolWrapper(bool boolWrapper) { return new BoolWrapper { Value = boolWrapper }; } } }
Method 16
I think it has a simple answer:
public string RemoveHTMLTags(string HTMLCode) { string str=System.Text.RegularExpressions.Regex.Replace(HTMLCode, "<[^>]*>", ""); return str; }
Method 17
For anyone looking for an exact solution to the OP question for a textual abbreviation of a given html document, without newlines and HTML tags, please find the solution below.
Like with every proposed solution, there are some assumptions with the code below:
- script or style tags should not contain script and style tags as a
part of script - only major inline elements will be inlined without
space, i.e.he<span>ll</span>o
should outputhello
. List of inline
tags: https://www.w3schools.com/htmL/html_blocks.asp
Considering the above, the following string extension with compiled regular expressions will output expected plain text with regard to html escaped characters and null on null input.
public static class StringExtensions { public static string ConvertToPlain(this string html) { if (html == null) { return html; } html = scriptRegex.Replace(html, string.Empty); html = inlineTagRegex.Replace(html, string.Empty); html = tagRegex.Replace(html, " "); html = HttpUtility.HtmlDecode(html); html = multiWhitespaceRegex.Replace(html, " "); return html.Trim(); } private static readonly Regex inlineTagRegex = new Regex("<\/?(a|span|sub|sup|b|i|strong|small|big|em|label|q)[^>]*>", RegexOptions.Compiled | RegexOptions.Singleline); private static readonly Regex scriptRegex = new Regex("<(script|style)[^>]*?>.*?</\1>", RegexOptions.Compiled | RegexOptions.Singleline); private static readonly Regex tagRegex = new Regex("<[^>]+>", RegexOptions.Compiled | RegexOptions.Singleline); private static readonly Regex multiWhitespaceRegex = new Regex("\s+", RegexOptions.Compiled | RegexOptions.Singleline); }
Method 18
Here is my solution:
public string StripHTML(string html) { if (string.IsNullOrWhiteSpace(html)) return ""; // could be stored in static variable var regex = new Regex("<[^>]+>|\s{2}", RegexOptions.IgnoreCase); return System.Web.HttpUtility.HtmlDecode(regex.Replace(html, "")); }
Example:
StripHTML("<p class='test' style='color:red;'>Here is my solution:</p>"); // output -> Here is my solution:
Method 19
public static string StripTags2(string html)
{
return html.Replace(“<“, “<“).Replace(“>”, “>”);
}
By this you escape all “<” and “>” in a string. Is this what you want?
All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0