RaUI/Source/ryControls/HtmlRenderer/Core/Parse/CssValueParser.cs
zilinsoft 3262955f2f ### 2023-11-07更新
------
#### RaUIV4    V4.0.2311.0701
- *.[全新]整合了MyDb、ryControls、MyDb_MySQL等dll文件到RaUI一个项目。
- *.[新增]新增ApkOp类,可以轻松获取APK信息。
- *.[新增]新增JsonExt扩展类,让Json操作更简单。
- *.[新增]新增WebP类,可以支持webp格式的图片。
- *.[改进]ryQuickSQL中的AddField方法改为自动替换已存在的同名值。
- *.[修复]ryQuickSQL中的AddFieldCalc方法无法正常计算的BUG。
2023-11-07 16:37:53 +08:00

543 lines
19 KiB
C#

// "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.Globalization;
using TheArtOfDev.HtmlRenderer.Adapters;
using TheArtOfDev.HtmlRenderer.Adapters.Entities;
using TheArtOfDev.HtmlRenderer.Core.Dom;
using TheArtOfDev.HtmlRenderer.Core.Utils;
namespace TheArtOfDev.HtmlRenderer.Core.Parse
{
/// <summary>
/// Parse CSS properties values like numbers, Urls, etc.
/// </summary>
internal sealed class CssValueParser
{
#region Fields and Consts
/// <summary>
///
/// </summary>
private readonly RAdapter _adapter;
#endregion
/// <summary>
/// Init.
/// </summary>
public CssValueParser(RAdapter adapter)
{
ArgChecker.AssertArgNotNull(adapter, "global");
_adapter = adapter;
}
/// <summary>
/// Check if the given substring is a valid double number.
/// Assume given substring is not empty and all indexes are valid!<br/>
/// </summary>
/// <returns>true - valid double number, false - otherwise</returns>
public static bool IsFloat(string str, int idx, int length)
{
if (length < 1)
return false;
bool sawDot = false;
for (int i = 0; i < length; i++)
{
if (str[idx + i] == '.')
{
if (sawDot)
return false;
sawDot = true;
}
else if (!char.IsDigit(str[idx + i]))
{
return false;
}
}
return true;
}
/// <summary>
/// Check if the given substring is a valid double number.
/// Assume given substring is not empty and all indexes are valid!<br/>
/// </summary>
/// <returns>true - valid int number, false - otherwise</returns>
public static bool IsInt(string str, int idx, int length)
{
if (length < 1)
return false;
for (int i = 0; i < length; i++)
{
if (!char.IsDigit(str[idx + i]))
return false;
}
return true;
}
/// <summary>
/// Check if the given string is a valid length value.
/// </summary>
/// <param name="value">the string value to check</param>
/// <returns>true - valid, false - invalid</returns>
public static bool IsValidLength(string value)
{
if (value.Length > 1)
{
string number = string.Empty;
if (value.EndsWith("%"))
{
number = value.Substring(0, value.Length - 1);
}
else if (value.Length > 2)
{
number = value.Substring(0, value.Length - 2);
}
double stub;
return double.TryParse(number, out stub);
}
return false;
}
/// <summary>
/// Evals a number and returns it. If number is a percentage, it will be multiplied by hundredPercent
/// </summary>
/// <param name="number">Number to be parsed</param>
/// <param name="hundredPercent">Number that represents the 100% if parsed number is a percentage</param>
/// <returns>Parsed number. Zero if error while parsing.</returns>
public static double ParseNumber(string number, double hundredPercent)
{
if (string.IsNullOrEmpty(number))
{
return 0f;
}
string toParse = number;
bool isPercent = number.EndsWith("%");
double result;
if (isPercent)
toParse = number.Substring(0, number.Length - 1);
if (!double.TryParse(toParse, NumberStyles.Number, NumberFormatInfo.InvariantInfo, out result))
{
return 0f;
}
if (isPercent)
{
result = (result / 100f) * hundredPercent;
}
return result;
}
/// <summary>
/// Parses a length. Lengths are followed by an unit identifier (e.g. 10px, 3.1em)
/// </summary>
/// <param name="length">Specified length</param>
/// <param name="hundredPercent">Equivalent to 100 percent when length is percentage</param>
/// <param name="fontAdjust">if the length is in pixels and the length is font related it needs to use 72/96 factor</param>
/// <param name="box"></param>
/// <returns>the parsed length value with adjustments</returns>
public static double ParseLength(string length, double hundredPercent, CssBoxProperties box, bool fontAdjust = false)
{
return ParseLength(length, hundredPercent, box.GetEmHeight(), null, fontAdjust, false);
}
/// <summary>
/// Parses a length. Lengths are followed by an unit identifier (e.g. 10px, 3.1em)
/// </summary>
/// <param name="length">Specified length</param>
/// <param name="hundredPercent">Equivalent to 100 percent when length is percentage</param>
/// <param name="box"></param>
/// <param name="defaultUnit"></param>
/// <returns>the parsed length value with adjustments</returns>
public static double ParseLength(string length, double hundredPercent, CssBoxProperties box, string defaultUnit)
{
return ParseLength(length, hundredPercent, box.GetEmHeight(), defaultUnit, false, false);
}
/// <summary>
/// Parses a length. Lengths are followed by an unit identifier (e.g. 10px, 3.1em)
/// </summary>
/// <param name="length">Specified length</param>
/// <param name="hundredPercent">Equivalent to 100 percent when length is percentage</param>
/// <param name="emFactor"></param>
/// <param name="defaultUnit"></param>
/// <param name="fontAdjust">if the length is in pixels and the length is font related it needs to use 72/96 factor</param>
/// <param name="returnPoints">Allows the return double to be in points. If false, result will be pixels</param>
/// <returns>the parsed length value with adjustments</returns>
public static double ParseLength(string length, double hundredPercent, double emFactor, string defaultUnit, bool fontAdjust, bool returnPoints)
{
//Return zero if no length specified, zero specified
if (string.IsNullOrEmpty(length) || length == "0")
return 0f;
//If percentage, use ParseNumber
if (length.EndsWith("%"))
return ParseNumber(length, hundredPercent);
//Get units of the length
bool hasUnit;
string unit = GetUnit(length, defaultUnit, out hasUnit);
//Factor will depend on the unit
double factor;
//Number of the length
string number = hasUnit ? length.Substring(0, length.Length - 2) : length;
//TODO: Units behave different in paper and in screen!
switch (unit)
{
case CssConstants.Em:
factor = emFactor;
break;
case CssConstants.Ex:
factor = emFactor / 2;
break;
case CssConstants.Px:
factor = fontAdjust ? 72f / 96f : 1f; //TODO:a check support for hi dpi
break;
case CssConstants.Mm:
factor = 3.779527559f; //3 pixels per millimeter
break;
case CssConstants.Cm:
factor = 37.795275591f; //37 pixels per centimeter
break;
case CssConstants.In:
factor = 96f; //96 pixels per inch
break;
case CssConstants.Pt:
factor = 96f / 72f; // 1 point = 1/72 of inch
if (returnPoints)
{
return ParseNumber(number, hundredPercent);
}
break;
case CssConstants.Pc:
factor = 16f; // 1 pica = 12 points
break;
default:
factor = 0f;
break;
}
return factor * ParseNumber(number, hundredPercent);
}
/// <summary>
/// Get the unit to use for the length, use default if no unit found in length string.
/// </summary>
private static string GetUnit(string length, string defaultUnit, out bool hasUnit)
{
var unit = length.Length >= 3 ? length.Substring(length.Length - 2, 2) : string.Empty;
switch (unit)
{
case CssConstants.Em:
case CssConstants.Ex:
case CssConstants.Px:
case CssConstants.Mm:
case CssConstants.Cm:
case CssConstants.In:
case CssConstants.Pt:
case CssConstants.Pc:
hasUnit = true;
break;
default:
hasUnit = false;
unit = defaultUnit ?? String.Empty;
break;
}
return unit;
}
/// <summary>
/// Check if the given color string value is valid.
/// </summary>
/// <param name="colorValue">color string value to parse</param>
/// <returns>true - valid, false - invalid</returns>
public bool IsColorValid(string colorValue)
{
RColor color;
return TryGetColor(colorValue, 0, colorValue.Length, out color);
}
/// <summary>
/// Parses a color value in CSS style; e.g. #ff0000, red, rgb(255,0,0), rgb(100%, 0, 0)
/// </summary>
/// <param name="colorValue">color string value to parse</param>
/// <returns>Color value</returns>
public RColor GetActualColor(string colorValue)
{
RColor color;
TryGetColor(colorValue, 0, colorValue.Length, out color);
return color;
}
/// <summary>
/// Parses a color value in CSS style; e.g. #ff0000, RED, RGB(255,0,0), RGB(100%, 0, 0)
/// </summary>
/// <param name="str">color substring value to parse</param>
/// <param name="idx">substring start idx </param>
/// <param name="length">substring length</param>
/// <param name="color">return the parsed color</param>
/// <returns>true - valid color, false - otherwise</returns>
public bool TryGetColor(string str, int idx, int length, out RColor color)
{
try
{
if (!string.IsNullOrEmpty(str))
{
if (length > 1 && str[idx] == '#')
{
return GetColorByHex(str, idx, length, out color);
}
else if (length > 10 && CommonUtils.SubStringEquals(str, idx, 4, "rgb(") && str[length - 1] == ')')
{
return GetColorByRgb(str, idx, length, out color);
}
else if (length > 13 && CommonUtils.SubStringEquals(str, idx, 5, "rgba(") && str[length - 1] == ')')
{
return GetColorByRgba(str, idx, length, out color);
}
else
{
return GetColorByName(str, idx, length, out color);
}
}
}
catch
{ }
color = RColor.Black;
return false;
}
/// <summary>
/// Parses a border value in CSS style; e.g. 1px, 1, thin, thick, medium
/// </summary>
/// <param name="borderValue"></param>
/// <param name="b"></param>
/// <returns></returns>
public static double GetActualBorderWidth(string borderValue, CssBoxProperties b)
{
if (string.IsNullOrEmpty(borderValue))
{
return GetActualBorderWidth(CssConstants.Medium, b);
}
switch (borderValue)
{
case CssConstants.Thin:
return 1f;
case CssConstants.Medium:
return 2f;
case CssConstants.Thick:
return 4f;
default:
return Math.Abs(ParseLength(borderValue, 1, b));
}
}
#region Private methods
/// <summary>
/// Get color by parsing given hex value color string (#A28B34).
/// </summary>
/// <returns>true - valid color, false - otherwise</returns>
private static bool GetColorByHex(string str, int idx, int length, out RColor color)
{
int r = -1;
int g = -1;
int b = -1;
if (length == 7)
{
r = ParseHexInt(str, idx + 1, 2);
g = ParseHexInt(str, idx + 3, 2);
b = ParseHexInt(str, idx + 5, 2);
}
else if (length == 4)
{
r = ParseHexInt(str, idx + 1, 1);
r = r * 16 + r;
g = ParseHexInt(str, idx + 2, 1);
g = g * 16 + g;
b = ParseHexInt(str, idx + 3, 1);
b = b * 16 + b;
}
if (r > -1 && g > -1 && b > -1)
{
color = RColor.FromArgb(r, g, b);
return true;
}
color = RColor.Empty;
return false;
}
/// <summary>
/// Get color by parsing given RGB value color string (RGB(255,180,90))
/// </summary>
/// <returns>true - valid color, false - otherwise</returns>
private static bool GetColorByRgb(string str, int idx, int length, out RColor color)
{
int r = -1;
int g = -1;
int b = -1;
if (length > 10)
{
int s = idx + 4;
r = ParseIntAtIndex(str, ref s);
if (s < idx + length)
{
g = ParseIntAtIndex(str, ref s);
}
if (s < idx + length)
{
b = ParseIntAtIndex(str, ref s);
}
}
if (r > -1 && g > -1 && b > -1)
{
color = RColor.FromArgb(r, g, b);
return true;
}
color = RColor.Empty;
return false;
}
/// <summary>
/// Get color by parsing given RGBA value color string (RGBA(255,180,90,180))
/// </summary>
/// <returns>true - valid color, false - otherwise</returns>
private static bool GetColorByRgba(string str, int idx, int length, out RColor color)
{
int r = -1;
int g = -1;
int b = -1;
int a = -1;
if (length > 13)
{
int s = idx + 5;
r = ParseIntAtIndex(str, ref s);
if (s < idx + length)
{
g = ParseIntAtIndex(str, ref s);
}
if (s < idx + length)
{
b = ParseIntAtIndex(str, ref s);
}
if (s < idx + length)
{
a = ParseIntAtIndex(str, ref s);
}
}
if (r > -1 && g > -1 && b > -1 && a > -1)
{
color = RColor.FromArgb(a, r, g, b);
return true;
}
color = RColor.Empty;
return false;
}
/// <summary>
/// Get color by given name, including .NET name.
/// </summary>
/// <returns>true - valid color, false - otherwise</returns>
private bool GetColorByName(string str, int idx, int length, out RColor color)
{
color = _adapter.GetColor(str.Substring(idx, length));
return color.A > 0;
}
/// <summary>
/// Parse the given decimal number string to positive int value.<br/>
/// Start at given <paramref name="startIdx"/>, ignore whitespaces and take
/// as many digits as possible to parse to int.
/// </summary>
/// <param name="str">the string to parse</param>
/// <param name="startIdx">the index to start parsing at</param>
/// <returns>parsed int or 0</returns>
private static int ParseIntAtIndex(string str, ref int startIdx)
{
int len = 0;
while (char.IsWhiteSpace(str, startIdx))
startIdx++;
while (char.IsDigit(str, startIdx + len))
len++;
var val = ParseInt(str, startIdx, len);
startIdx = startIdx + len + 1;
return val;
}
/// <summary>
/// Parse the given decimal number string to positive int value.
/// Assume given substring is not empty and all indexes are valid!<br/>
/// </summary>
/// <returns>int value, -1 if not valid</returns>
private static int ParseInt(string str, int idx, int length)
{
if (length < 1)
return -1;
int num = 0;
for (int i = 0; i < length; i++)
{
int c = str[idx + i];
if (!(c >= 48 && c <= 57))
return -1;
num = num * 10 + c - 48;
}
return num;
}
/// <summary>
/// Parse the given hex number string to positive int value.
/// Assume given substring is not empty and all indexes are valid!<br/>
/// </summary>
/// <returns>int value, -1 if not valid</returns>
private static int ParseHexInt(string str, int idx, int length)
{
if (length < 1)
return -1;
int num = 0;
for (int i = 0; i < length; i++)
{
int c = str[idx + i];
if (!(c >= 48 && c <= 57) && !(c >= 65 && c <= 70) && !(c >= 97 && c <= 102))
return -1;
num = num * 16 + (c <= 57 ? c - 48 : (10 + c - (c <= 70 ? 65 : 97)));
}
return num;
}
#endregion
}
}