// "Therefore those skilled at the unorthodox
// are infinite as heaven and earth,
// inexhaustible as the great rivers.
// When they come to an end,
// they begin again,
// like the days and months;
// they die and are reborn,
// like the four seasons."
//
// - Sun Tsu,
// "The Art of War"
using System;
using System.Collections.Generic;
using TheArtOfDev.HtmlRenderer.Adapters;
using TheArtOfDev.HtmlRenderer.Core.Entities;
using TheArtOfDev.HtmlRenderer.Core.Parse;
using TheArtOfDev.HtmlRenderer.Core.Utils;
namespace TheArtOfDev.HtmlRenderer.Core
{
///
/// Holds parsed stylesheet css blocks arranged by media and classes.
///
///
///
/// To learn more about CSS blocks visit CSS spec: http://www.w3.org/TR/CSS21/syndata.html#block
///
public sealed class CssData
{
#region Fields and Consts
///
/// used to return empty array
///
private static readonly List _emptyArray = new List();
///
/// dictionary of media type to dictionary of css class name to the cssBlocks collection with all the data.
///
private readonly Dictionary>> _mediaBlocks = new Dictionary>>(StringComparer.InvariantCultureIgnoreCase);
#endregion
///
/// Init.
///
internal CssData()
{
_mediaBlocks.Add("all", new Dictionary>(StringComparer.InvariantCultureIgnoreCase));
}
///
/// Parse the given stylesheet to object.
/// If is true the parsed css blocks are added to the
/// default css data (as defined by W3), merged if class name already exists. If false only the data in the given stylesheet is returned.
///
/// Platform adapter
/// the stylesheet source to parse
/// true - combine the parsed css data with default css data, false - return only the parsed css data
/// the parsed css data
public static CssData Parse(RAdapter adapter, string stylesheet, bool combineWithDefault = true)
{
CssParser parser = new CssParser(adapter);
return parser.ParseStyleSheet(stylesheet, combineWithDefault);
}
///
/// dictionary of media type to dictionary of css class name to the cssBlocks collection with all the data
///
internal IDictionary>> MediaBlocks
{
get { return _mediaBlocks; }
}
///
/// Check if there are css blocks for the given class selector.
///
/// the class selector to check for css blocks by
/// optional: the css media type (default - all)
/// true - has css blocks for the class, false - otherwise
public bool ContainsCssBlock(string className, string media = "all")
{
Dictionary> mid;
return _mediaBlocks.TryGetValue(media, out mid) && mid.ContainsKey(className);
}
///
/// Get collection of css blocks for the requested class selector.
/// the can be: class name, html element name, html element and
/// class name (elm.class), hash tag with element id (#id).
/// returned all the blocks that word on the requested class selector, it can contain simple
/// selector or hierarchy selector.
///
/// the class selector to get css blocks by
/// optional: the css media type (default - all)
/// collection of css blocks, empty collection if no blocks exists (never null)
public IEnumerable GetCssBlock(string className, string media = "all")
{
List block = null;
Dictionary> mid;
if (_mediaBlocks.TryGetValue(media, out mid))
{
mid.TryGetValue(className, out block);
}
return block ?? _emptyArray;
}
///
/// Add the given css block to the css data, merging to existing block if required.
///
///
/// If there is no css blocks for the same class it will be added to data collection.
/// If there is already css blocks for the same class it will check for each existing block
/// if the hierarchical selectors match (or not exists). if do the two css blocks will be merged into
/// one where the new block properties overwrite existing if needed. if the new block doesn't mach any
/// existing it will be added either to the beginning of the list if it has no hierarchical selectors or at the end.
/// Css block without hierarchical selectors must be added to the beginning of the list so more specific block
/// can overwrite it when the style is applied.
///
/// the media type to add the CSS to
/// the css block to add
public void AddCssBlock(string media, CssBlock cssBlock)
{
Dictionary> mid;
if (!_mediaBlocks.TryGetValue(media, out mid))
{
mid = new Dictionary>(StringComparer.InvariantCultureIgnoreCase);
_mediaBlocks.Add(media, mid);
}
if (!mid.ContainsKey(cssBlock.Class))
{
var list = new List();
list.Add(cssBlock);
mid[cssBlock.Class] = list;
}
else
{
bool merged = false;
var list = mid[cssBlock.Class];
foreach (var block in list)
{
if (block.EqualsSelector(cssBlock))
{
merged = true;
block.Merge(cssBlock);
break;
}
}
if (!merged)
{
// general block must be first
if (cssBlock.Selectors == null)
list.Insert(0, cssBlock);
else
list.Add(cssBlock);
}
}
}
///
/// Combine this CSS data blocks with CSS blocks for each media.
/// Merge blocks if exists in both.
///
/// the CSS data to combine with
public void Combine(CssData other)
{
ArgChecker.AssertArgNotNull(other, "other");
// for each media block
foreach (var mediaBlock in other.MediaBlocks)
{
// for each css class in the media block
foreach (var bla in mediaBlock.Value)
{
// for each css block of the css class
foreach (var cssBlock in bla.Value)
{
// combine with this
AddCssBlock(mediaBlock.Key, cssBlock);
}
}
}
}
///
/// Create deep copy of the css data with cloned css blocks.
///
/// cloned object
public CssData Clone()
{
var clone = new CssData();
foreach (var mid in _mediaBlocks)
{
var cloneMid = new Dictionary>(StringComparer.InvariantCultureIgnoreCase);
foreach (var blocks in mid.Value)
{
var cloneList = new List();
foreach (var cssBlock in blocks.Value)
{
cloneList.Add(cssBlock.Clone());
}
cloneMid[blocks.Key] = cloneList;
}
clone._mediaBlocks[mid.Key] = cloneMid;
}
return clone;
}
}
}