using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.UI;
using System.Text.RegularExpressions;
namespace CssSpriteGenerator
{
public class HtmlUtils
{
/// <summary>
/// Analyses a string with html, finds all image tags, and returns
/// a collection of ImageTags, one for each image in the html.
/// </summary>
/// <param name="html"></param>
/// <returns></returns>
public static IList<ImageTag> ImagesInHtml(string html)
{
List<ImageTag> result = new List<ImageTag>();
string regexpImgGroup =
RegexTagWithAttributes("img"); // just an img tag
Regex r = new Regex(regexpImgGroup, RegexOptions.IgnoreCase);
Match m = r.Match(html);
while (m.Success)
{
string tagText = m.Value;
ImageAttributeDictionary imgAttributes = TagAttributes("img", m);
result.Add(new ImageTag { ImgAttributes = imgAttributes, TagText = tagText });
m = m.NextMatch();
}
return result;
}
/// <summary>
/// Generates the regular expression that matches an html tag and its attributes.
/// </summary>
/// <param name="tag"></param>
/// <returns></returns>
private static string RegexTagWithAttributes(string tag)
{
const string regexpTagGroup =
@"<{0}" +
@"(?:" + // start attributes definition
@"\s*" + // starts with zero more more white spaces
@"(?<{0}attrname>\w*)" + // attribute name, must consist of letter, digits or underscore
@"\s*=\s*" + // followed by zero or more space, then an equals sign, then zero or more spaces
@"(?<{0}quote>""|')" + // followed by quote (" or ')
@"(?<{0}attrvalue>.*?)" + // followed by value. Using a non greedy match on zero or more characters .*?
@"\k<{0}quote>" + // followed by the same quote as we saw before. The \k<{0}quote> refers to the first quote.
@")*" + // zero or more attributes of the form attr="value"
@"(?:[^>]*)" + // followed by zero or more non-> characters
@">"; // followed by closing >
string result = string.Format(regexpTagGroup, tag);
return result;
}
/// <summary>
/// Generates the regular expression that matches an html end tag and its attributes.
/// </summary>
/// <param name="tag"></param>
/// <returns></returns>
private static string RegexEndTag(string tag)
{
const string regexpEndTagGroup =
@"</{0}>";
string result = string.Format(regexpEndTagGroup, tag);
return result;
}
/// <summary>
/// After the regex generated by RegexTagWithAttributes has been matched,
/// use this method to retrieve the attributes of the tag and return them in a
/// ImageAttributeDictionary.
/// </summary>
/// <param name="tag">
/// Tag that was passed to RegexTagWithAttributes to generate the regular expression
/// that has now been matched
/// </param>
/// <param name="m">
/// Match of the regular expression.
/// </param>
/// <returns></returns>
private static ImageAttributeDictionary TagAttributes(string tag, Match m)
{
CaptureCollection attrNames = m.Groups[tag + "attrname"].Captures;
CaptureCollection attrValues = m.Groups[tag + "attrvalue"].Captures;
int nbrNames = attrNames.Count;
int nbrValues = attrValues.Count;
if (nbrNames != nbrValues)
{
throw new Exception(
string.Format("Image tag {0} in {1} has {2} attribute names, but {3} attribute values", tag, m.Value, nbrNames, nbrValues));
}
// All attributes will be stored in this dictionary (attribute name = key, attribute value = value)
ImageAttributeDictionary attributes = new ImageAttributeDictionary();
// If an image tag has the same attribute multiple times, the browser uses the first occurrance (tested on IE, Firefox, Chrome).
// So, go backwards through the list of attributes. That way, if there are duplicates, the first one will
// remain in the dictionary.
for (int i = nbrNames - 1; i >= 0; i--)
{
attributes[attrNames[i].Value.ToLower()] = attrValues[i].Value;
}
return attributes;
}
/// <summary>
/// Produces the CSS to be used with a div tag, in order to make that div tag show a sprite.
/// </summary>
/// <param name="spriteUrl">
/// Url of the sprite image.
/// </param>
/// <param name="xOffset">
/// X offset within the sprite where the original image starts.
/// </param>
/// <param name="yOffset">
/// Y offset within the sprite where the original image starts.
/// </param>
/// <param name="imageWidth">
/// Width in px of the original image.
/// </param>
/// <param name="imageHeight">
/// Height in px of the original image.
/// </param>
/// <returns></returns>
public static string PageSpriteCss(
string spriteUrl, int xOffset, int yOffset, int imageWidth, int imageHeight)
{
string css =
string.Format(
"width: {0}px; height: {1}px; background: url({2}) -{3}px -{4}px;",
imageWidth, imageHeight, UrlUtils.EscapedUrl(spriteUrl), xOffset, yOffset);
return css;
}
/// <summary>
/// Generates the html for a sprite that lives on the page (rather than a sprite used with the css).
/// Essentially, this will be a div tag that is styled via a class or via inline style.
/// However, if the original img was enclosed in an anchor, you get a span enclosed in an anchor.
///
/// Example for simple img translation:
///
/// <div style="width: 50px; height: 50px; background: url(___spritegen/2-0-0-B3-45-8A-FF-3E-C3-78-19-4B-89-7E-E0-91-04-8B-70.png) -50px -0px; display:inline-block;
/// text-indent:-9999px;">the alt text</div>
///
/// Example for img enclosed with anchor:
///
/// <a href="http://google.com" target="_blank"
/// style="text-decoration: none;
/// width: 50px; height: 50px;background: url(___spritegen/2-0-0-B3-45-8A-FF-3E-C3-78-19-4B-89-7E-E0-91-04-8B-70.png) -50px -0px;display:inline-block;
/// >
/// <span style="display:inline-block;text-indent:-9999px; ">the alt text</span>
/// </a>
///
/// </summary>
/// <param name="pageSpriteCss">
/// CSS to be used with the div tag.
/// </param>
/// <param name="imageAttributes">
/// Attributes of the original image.
/// </param>
/// <param name="inlineSpriteStyles">
/// True: add the css to the div tag as inline style.
/// False: add the css via a css class.
/// </param>
/// <param name="altCopyOptions">
/// Determines how the alt attribute of the img is treated. See description of altCopyOptions in ConfigSection.cs.
/// </param>
/// <param name="classPostfix">
/// If you need to create a new css class, give its name this postfix.
/// </param>
/// <param name="additionalCss">
/// If you need to add CSS to a stylesheet, return it here.
/// The caller is reponsible for creating a stylesheet that contains this CSS.
/// </param>
/// <returns>
/// Complete html representing the sprite.
/// </returns>
public static string PageSpriteHtml(
string spriteUrl, int xOffset, int yOffset, int imageWidth, int imageHeight,
ImageAttributeDictionary imageAttributes,
bool inlineSpriteStyles,
Stylesheet additionalCss)
{
string spriteCss = PageSpriteCss(spriteUrl, xOffset, yOffset, imageWidth, imageHeight);
// -----------------
// create opening tag
List<string> imgExcludes = new List<string>
{
"src",
"width",
"height",
"class",
"style"
};
// -----------------
// copy over the attributes from the original img tag
string tagAttributesString = imageAttributes.ToString(imgExcludes, null);
// ----------------
string styleAttributeValue = imageAttributes.AttributeValue("style");
string classAttributeValue = imageAttributes.AttributeValue("class");
if (inlineSpriteStyles)
{
styleAttributeValue = CombinedCss(styleAttributeValue, spriteCss);
}
else
{
string additionalClass = additionalCss.AddDeclaration(spriteCss);
classAttributeValue = CombinedClasses(classAttributeValue, additionalClass);
}
// ---------------
// assemble final html
string html =
"<img src=\"" + UrlUtils.UrlTransparent1x1Png() + "\"" +
tagAttributesString +
NonEmptyHtmlAttribute("style", styleAttributeValue) +
NonEmptyHtmlAttribute("class", classAttributeValue) +
@" />";
return html;
}
private static string NonEmptyHtmlAttribute(string attributeName, string attributeValue)
{
string htmlAttribute = "";
if (!string.IsNullOrWhiteSpace(attributeValue))
{
htmlAttribute = string.Format(@" {0}=""{1}""", attributeName, attributeValue);
}
return htmlAttribute;
}
/// <summary>
/// Creates an image tag out of the passed in imageAttributes.
/// If anchorAttributes contains attributes, than an anchor tag is created around the img.
/// </summary>
/// <param name="imageUrl">
/// Overrides the src attribute
/// </param>
/// <param name="imageAttributes">
/// </param>
/// <param name="imageWidth">
/// Overrides the width attribute
/// </param>
/// <param name="imageHeight">
/// Overrides the height attribute
/// </param>
/// <param name="overrideImgTagDimensionProperties">
/// If this is true, than any width and height properties of the img tag will be
/// removed, and new width and height properties created based on the imageWidth and imageHeight.
/// </param>
/// <returns></returns>
public static string ImgHtml(
string imageUrl, ImageAttributeDictionary imageAttributes,
int imageWidth, int imageHeight, bool overrideImgTagDimensionProperties)
{
// ------------
// Create the html for the image.
List<string> imgExcludes = new List<string>();
imgExcludes.Add("src");
if (overrideImgTagDimensionProperties)
{
imgExcludes.Add("width");
imgExcludes.Add("height");
}
string imgAttributesString = imageAttributes.ToString(imgExcludes, null);
string imgHtml = "<img";
if (overrideImgTagDimensionProperties)
{
// Make sure width and height are added
imgHtml += string.Format(@" width=""{0}""", imageWidth);
imgHtml += string.Format(@" height=""{0}""", imageHeight);
}
imgHtml +=
string.Format(@" src=""{0}""", UrlUtils.EscapedUrl(imageUrl)) +
imgAttributesString +
@" />";
return imgHtml;
}
private static string CombinedClasses(string class1, string class2)
{
if (string.IsNullOrWhiteSpace(class1))
{
return class2;
}
string trimmedClass1 = class1.Trim(new char[] { ' ' });
return trimmedClass1 + " " + class2;
}
private static string CombinedCss(string css1, string css2)
{
if (string.IsNullOrWhiteSpace(css1))
{
return css2;
}
string trimmedCss1 = css1.Trim(new char[] { ' ', ';' });
return trimmedCss1 + ";" + css2;
}
/// <summary>
/// Produces the CSS to be used with a sprite that will be used for CSS background images.
/// </summary>
/// <param name="spriteUrl">
/// Url of the sprite image.
/// </param>
/// <param name="xOffset">
/// X offset within the sprite where the original image starts.
/// </param>
/// <param name="yOffset">
/// Y offset within the sprite where the original image starts.
/// </param>
/// <param name="alignment">
/// Required alignment of the background image.
/// </param>
/// <returns></returns>
public static string CssSpriteCss(
string spriteUrl, int xOffset, int yOffset, Alignment alignment)
{
string xPos = posString(xOffset, "xOffset", alignment, Alignment.Left, Alignment.Right);
string yPos = posString(yOffset, "yOffset", alignment, Alignment.Top, Alignment.Bottom);
string result =
string.Format(
"background-image: url({0}); background-position: {1} {2};",
UrlUtils.EscapedUrl(spriteUrl), xPos, yPos);
return result;
}
private static string posString(int offset, string offsetName, Alignment alignment, Alignment checkAlignment1, Alignment checkAlignment2)
{
string result = null;
if ((alignment == checkAlignment1) || (alignment == checkAlignment2))
{
if (offset != 0)
{
throw new Exception(
string.Format("Alignment is {0} but {1} is {2} while it should be 0", alignment, offsetName, offset));
}
result = alignment.ToString();
}
else
{
result = (-1 * offset).ToString() + "px";
}
return result;
}
}
}