mirror of https://github.com/buggins/dlangui.git
parent
8893efc8e5
commit
d065ca4fe1
|
@ -1,662 +0,0 @@
|
||||||
// Written in the D programming language.
|
|
||||||
|
|
||||||
/**
|
|
||||||
This module contains implementation of CSS support - Cascading Style Sheets.
|
|
||||||
|
|
||||||
Port of CoolReader Engine written in C++.
|
|
||||||
|
|
||||||
Supports subset of CSS standards.
|
|
||||||
|
|
||||||
|
|
||||||
Synopsis:
|
|
||||||
|
|
||||||
----
|
|
||||||
import dlangui.core.css;
|
|
||||||
|
|
||||||
----
|
|
||||||
|
|
||||||
Copyright: Vadim Lopatin, 2015
|
|
||||||
License: Boost License 1.0
|
|
||||||
Authors: Vadim Lopatin, coolreader.org@gmail.com
|
|
||||||
*/
|
|
||||||
module dlangui.core.css;
|
|
||||||
|
|
||||||
import std.traits;
|
|
||||||
import std.conv : to;
|
|
||||||
import std.string;
|
|
||||||
import std.array : empty;
|
|
||||||
import std.algorithm : equal;
|
|
||||||
import std.ascii : isAlpha;
|
|
||||||
|
|
||||||
import dlangui.core.dom;
|
|
||||||
|
|
||||||
/// display property values
|
|
||||||
enum CssDisplay : ubyte {
|
|
||||||
inherit,
|
|
||||||
inline,
|
|
||||||
block,
|
|
||||||
list_item,
|
|
||||||
run_in,
|
|
||||||
compact,
|
|
||||||
marker,
|
|
||||||
table,
|
|
||||||
inline_table,
|
|
||||||
table_row_group,
|
|
||||||
table_header_group,
|
|
||||||
table_footer_group,
|
|
||||||
table_row,
|
|
||||||
table_column_group,
|
|
||||||
table_column,
|
|
||||||
table_cell,
|
|
||||||
table_caption,
|
|
||||||
none
|
|
||||||
}
|
|
||||||
|
|
||||||
/// white-space property values
|
|
||||||
enum CssWhiteSpace : ubyte {
|
|
||||||
inherit,
|
|
||||||
normal,
|
|
||||||
pre,
|
|
||||||
nowrap
|
|
||||||
}
|
|
||||||
|
|
||||||
/// text-align property values
|
|
||||||
enum CssTextAlign : ubyte {
|
|
||||||
inherit,
|
|
||||||
left,
|
|
||||||
right,
|
|
||||||
center,
|
|
||||||
justify
|
|
||||||
}
|
|
||||||
|
|
||||||
/// vertical-align property values
|
|
||||||
enum CssVerticalAlign : ubyte {
|
|
||||||
inherit,
|
|
||||||
baseline,
|
|
||||||
sub,
|
|
||||||
super_,
|
|
||||||
top,
|
|
||||||
text_top,
|
|
||||||
middle,
|
|
||||||
bottom,
|
|
||||||
text_bottom
|
|
||||||
}
|
|
||||||
|
|
||||||
/// text-decoration property values
|
|
||||||
enum CssTextDecoration : ubyte {
|
|
||||||
// TODO: support multiple flags
|
|
||||||
inherit = 0,
|
|
||||||
none = 1,
|
|
||||||
underline = 2,
|
|
||||||
overline = 3,
|
|
||||||
line_through = 4,
|
|
||||||
blink = 5
|
|
||||||
}
|
|
||||||
|
|
||||||
/// hyphenate property values
|
|
||||||
enum CssHyphenate : ubyte {
|
|
||||||
inherit = 0,
|
|
||||||
none = 1,
|
|
||||||
auto_ = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
/// font-style property values
|
|
||||||
enum CssFontStyle : ubyte {
|
|
||||||
inherit,
|
|
||||||
normal,
|
|
||||||
italic,
|
|
||||||
oblique
|
|
||||||
}
|
|
||||||
|
|
||||||
/// font-weight property values
|
|
||||||
enum CssFontWeight : ubyte {
|
|
||||||
inherit,
|
|
||||||
normal,
|
|
||||||
bold,
|
|
||||||
bolder,
|
|
||||||
lighter,
|
|
||||||
fw_100,
|
|
||||||
fw_200,
|
|
||||||
fw_300,
|
|
||||||
fw_400,
|
|
||||||
fw_500,
|
|
||||||
fw_600,
|
|
||||||
fw_700,
|
|
||||||
fw_800,
|
|
||||||
fw_900
|
|
||||||
}
|
|
||||||
|
|
||||||
/// font-family property values
|
|
||||||
enum CssFontFamily : ubyte {
|
|
||||||
inherit,
|
|
||||||
serif,
|
|
||||||
sans_serif,
|
|
||||||
cursive,
|
|
||||||
fantasy,
|
|
||||||
monospace
|
|
||||||
}
|
|
||||||
|
|
||||||
/// page split property values
|
|
||||||
enum CssPageBreak : ubyte {
|
|
||||||
inherit,
|
|
||||||
auto_,
|
|
||||||
always,
|
|
||||||
avoid,
|
|
||||||
left,
|
|
||||||
right
|
|
||||||
}
|
|
||||||
|
|
||||||
/// list-style-type property values
|
|
||||||
enum CssListStyleType : ubyte {
|
|
||||||
inherit,
|
|
||||||
disc,
|
|
||||||
circle,
|
|
||||||
square,
|
|
||||||
decimal,
|
|
||||||
lower_roman,
|
|
||||||
upper_roman,
|
|
||||||
lower_alpha,
|
|
||||||
upper_alpha,
|
|
||||||
none
|
|
||||||
}
|
|
||||||
|
|
||||||
/// list-style-position property values
|
|
||||||
enum CssListStylePosition : ubyte {
|
|
||||||
inherit,
|
|
||||||
inside,
|
|
||||||
outside
|
|
||||||
}
|
|
||||||
|
|
||||||
/// css length value types
|
|
||||||
enum CssValueType : ubyte {
|
|
||||||
inherited,
|
|
||||||
unspecified,
|
|
||||||
px,
|
|
||||||
em,
|
|
||||||
ex,
|
|
||||||
in_, // 2.54 cm
|
|
||||||
cm,
|
|
||||||
mm,
|
|
||||||
pt, // 1/72 in
|
|
||||||
pc, // 12 pt
|
|
||||||
percent,
|
|
||||||
color
|
|
||||||
}
|
|
||||||
|
|
||||||
/// css length value
|
|
||||||
struct CssValue {
|
|
||||||
|
|
||||||
int value = 0; ///< value (*256 for all types except % and px)
|
|
||||||
CssValueType type = CssValueType.px; ///< type of value
|
|
||||||
|
|
||||||
this(int px_value ) {
|
|
||||||
value = px_value;
|
|
||||||
}
|
|
||||||
this(CssValueType n_type, int n_value) {
|
|
||||||
type = n_type;
|
|
||||||
value = n_value;
|
|
||||||
}
|
|
||||||
bool opEqual(CssValue v) const
|
|
||||||
{
|
|
||||||
return type == v.type
|
|
||||||
&& value == v.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
static const CssValue inherited = CssValue(CssValueType.inherited, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
enum CssDeclType : ubyte {
|
|
||||||
unknown,
|
|
||||||
display,
|
|
||||||
white_space,
|
|
||||||
text_align,
|
|
||||||
text_align_last,
|
|
||||||
text_decoration,
|
|
||||||
hyphenate, // hyphenate
|
|
||||||
_webkit_hyphens, // -webkit-hyphens
|
|
||||||
adobe_hyphenate, // adobe-hyphenate
|
|
||||||
adobe_text_layout, // adobe-text-layout
|
|
||||||
color,
|
|
||||||
background_color,
|
|
||||||
vertical_align,
|
|
||||||
font_family, // id families like serif, sans-serif
|
|
||||||
//font_names, // string font name like Arial, Courier
|
|
||||||
font_size,
|
|
||||||
font_style,
|
|
||||||
font_weight,
|
|
||||||
text_indent,
|
|
||||||
line_height,
|
|
||||||
letter_spacing,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
margin_left,
|
|
||||||
margin_right,
|
|
||||||
margin_top,
|
|
||||||
margin_bottom,
|
|
||||||
margin,
|
|
||||||
padding_left,
|
|
||||||
padding_right,
|
|
||||||
padding_top,
|
|
||||||
padding_bottom,
|
|
||||||
padding,
|
|
||||||
page_break_before,
|
|
||||||
page_break_after,
|
|
||||||
page_break_inside,
|
|
||||||
list_style,
|
|
||||||
list_style_type,
|
|
||||||
list_style_position,
|
|
||||||
list_style_image
|
|
||||||
}
|
|
||||||
|
|
||||||
class CssStyle {
|
|
||||||
CssDisplay display = CssDisplay.block;
|
|
||||||
CssWhiteSpace whiteSpace = CssWhiteSpace.inherit;
|
|
||||||
CssTextAlign textAlign = CssTextAlign.inherit;
|
|
||||||
CssTextAlign textAlignLast = CssTextAlign.inherit;
|
|
||||||
CssTextDecoration textDecoration = CssTextDecoration.inherit;
|
|
||||||
CssHyphenate hyphenate = CssHyphenate.inherit;
|
|
||||||
CssVerticalAlign verticalAlign = CssVerticalAlign.inherit;
|
|
||||||
CssFontFamily fontFamily = CssFontFamily.inherit;
|
|
||||||
CssFontStyle fontStyle = CssFontStyle.inherit;
|
|
||||||
CssPageBreak pageBreakBefore = CssPageBreak.inherit;
|
|
||||||
CssPageBreak pageBreakInside = CssPageBreak.inherit;
|
|
||||||
CssPageBreak pageBreakAfter = CssPageBreak.inherit;
|
|
||||||
CssListStyleType listStyleType = CssListStyleType.inherit;
|
|
||||||
CssListStylePosition listStylePosition = CssListStylePosition.inherit;
|
|
||||||
CssFontWeight fontWeight = CssFontWeight.inherit;
|
|
||||||
string fontFaces;
|
|
||||||
CssValue color = CssValue.inherited;
|
|
||||||
CssValue backgroundColor = CssValue.inherited;
|
|
||||||
CssValue lineHeight = CssValue.inherited;
|
|
||||||
CssValue letterSpacing = CssValue.inherited;
|
|
||||||
CssValue width = CssValue.inherited;
|
|
||||||
CssValue height = CssValue.inherited;
|
|
||||||
CssValue marginLeft = CssValue.inherited;
|
|
||||||
CssValue marginRight = CssValue.inherited;
|
|
||||||
CssValue marginTop = CssValue.inherited;
|
|
||||||
CssValue marginBottom = CssValue.inherited;
|
|
||||||
CssValue paddingLeft = CssValue.inherited;
|
|
||||||
CssValue paddingRight = CssValue.inherited;
|
|
||||||
CssValue paddingTop = CssValue.inherited;
|
|
||||||
CssValue paddingBottom = CssValue.inherited;
|
|
||||||
CssValue fontSize = CssValue.inherited;
|
|
||||||
CssValue textIndent = CssValue.inherited;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// selector rule type
|
|
||||||
enum CssSelectorRuleType : ubyte {
|
|
||||||
universal, // *
|
|
||||||
parent, // E > F
|
|
||||||
ancessor, // E F
|
|
||||||
predecessor, // E + F
|
|
||||||
attrset, // E[foo]
|
|
||||||
attreq, // E[foo="value"]
|
|
||||||
attrhas, // E[foo~="value"]
|
|
||||||
attrstarts, // E[foo|="value"]
|
|
||||||
id, // E#id
|
|
||||||
class_ // E.class
|
|
||||||
}
|
|
||||||
|
|
||||||
class CssSelectorRule
|
|
||||||
{
|
|
||||||
private:
|
|
||||||
CssSelectorRuleType _type;
|
|
||||||
elem_id _id;
|
|
||||||
attr_id _attrid;
|
|
||||||
CssSelectorRule _next;
|
|
||||||
string _value;
|
|
||||||
public:
|
|
||||||
this(CssSelectorRuleType type) {
|
|
||||||
_type = type;
|
|
||||||
}
|
|
||||||
this(const CssSelectorRule v) {
|
|
||||||
_type = v._type;
|
|
||||||
_id = v._id;
|
|
||||||
_attrid = v._attrid;
|
|
||||||
_value = v._value;
|
|
||||||
}
|
|
||||||
~this() {
|
|
||||||
//if (_next)
|
|
||||||
// destroy(_next);
|
|
||||||
}
|
|
||||||
|
|
||||||
@property elem_id id() { return _id; }
|
|
||||||
@property void id(elem_id newid) { _id = newid; }
|
|
||||||
@property attr_id attrid() { return _attrid; }
|
|
||||||
@property void setAttr(attr_id newid, string value) { _attrid = newid; _value = value; }
|
|
||||||
@property CssSelectorRule next() { return _next; }
|
|
||||||
@property void next(CssSelectorRule v) { _next = v; }
|
|
||||||
/// check condition for node
|
|
||||||
bool check(ref Node node) const {
|
|
||||||
if (!node || !node.parent)
|
|
||||||
return false;
|
|
||||||
switch (_type) with (CssSelectorRuleType) {
|
|
||||||
case parent: // E > F
|
|
||||||
node = node.parent;
|
|
||||||
if (!node)
|
|
||||||
return false;
|
|
||||||
return node.id == _id;
|
|
||||||
|
|
||||||
case ancessor: // E F
|
|
||||||
for (;;) {
|
|
||||||
node = node.parent;
|
|
||||||
if (!node)
|
|
||||||
return false;
|
|
||||||
if (node.id == _id)
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
case predecessor: // E + F
|
|
||||||
int index = node.index;
|
|
||||||
// while
|
|
||||||
if (index > 0) {
|
|
||||||
Node elem = node.parent.childElement(index-1, _id);
|
|
||||||
if ( elem ) {
|
|
||||||
node = elem;
|
|
||||||
//CRLog::trace("+ selector: found pred element");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
//index--;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
|
|
||||||
case attrset: // E[foo]
|
|
||||||
return node.hasAttr(_attrid);
|
|
||||||
|
|
||||||
case attreq: // E[foo="value"]
|
|
||||||
string val = node.attrValue(Ns.any, _attrid);
|
|
||||||
return (val == _value);
|
|
||||||
|
|
||||||
case attrhas: // E[foo~="value"]
|
|
||||||
// one of space separated values
|
|
||||||
string val = node.attrValue(Ns.any, _attrid);
|
|
||||||
int p = cast(int)val.indexOf(_value);
|
|
||||||
if (p < 0)
|
|
||||||
return false;
|
|
||||||
if ( (p > 0 && val[p - 1] != ' ')
|
|
||||||
|| ( p + _value.length < val.length && val[p + _value.length] != ' '))
|
|
||||||
return false;
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case attrstarts: // E[foo|="value"]
|
|
||||||
string val = node.attrValue(Ns.any, _attrid);
|
|
||||||
if (_value.length > val.length)
|
|
||||||
return false;
|
|
||||||
return val[0 .. _value.length] == _value;
|
|
||||||
|
|
||||||
case id: // E#id
|
|
||||||
string val = node.attrValue(Ns.any, Attr.id);
|
|
||||||
return val == _value;
|
|
||||||
|
|
||||||
case class_: // E.class
|
|
||||||
string val = node.attrValue(Ns.any, Attr.class_);
|
|
||||||
return !val.icmp(_value);
|
|
||||||
|
|
||||||
case universal: // *
|
|
||||||
return true;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
import dlangui.core.cssparser;
|
|
||||||
|
|
||||||
/** simple CSS selector
|
|
||||||
|
|
||||||
Currently supports only element name and universal selector.
|
|
||||||
|
|
||||||
- * { } - universal selector
|
|
||||||
- element-name { } - selector by element name
|
|
||||||
- element1, element2 { } - several selectors delimited by comma
|
|
||||||
*/
|
|
||||||
class CssSelector {
|
|
||||||
private:
|
|
||||||
uint _id;
|
|
||||||
CssDeclaration _decl;
|
|
||||||
int _specificity;
|
|
||||||
CssSelector _next;
|
|
||||||
CssSelectorRule _rules;
|
|
||||||
public:
|
|
||||||
/// get element tag id (0 - any tag)
|
|
||||||
@property elem_id id() { return _id; }
|
|
||||||
/// set element tag id (0 - any tag)
|
|
||||||
@property void id(elem_id id) { _id = id; }
|
|
||||||
|
|
||||||
this() { }
|
|
||||||
|
|
||||||
~this() {
|
|
||||||
//if (_next)
|
|
||||||
// destroy(_next);
|
|
||||||
}
|
|
||||||
|
|
||||||
void insertRuleStart(CssSelectorRule rule) {
|
|
||||||
rule.next = _rules;
|
|
||||||
_rules = rule;
|
|
||||||
}
|
|
||||||
|
|
||||||
void insertRuleAfterStart(CssSelectorRule rule) {
|
|
||||||
if (!_rules) {
|
|
||||||
_rules = rule;
|
|
||||||
} else {
|
|
||||||
rule.next = _rules.next;
|
|
||||||
_rules.next = rule;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// check if selector rules match this node
|
|
||||||
bool check(Node node) const {
|
|
||||||
CssSelectorRule rule = cast(CssSelectorRule)_rules;
|
|
||||||
while (rule && node) {
|
|
||||||
if (!rule.check(node))
|
|
||||||
return false;
|
|
||||||
rule = rule.next;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// apply to style if selector matches
|
|
||||||
void apply(Node node, CssStyle style) const {
|
|
||||||
if (check(node))
|
|
||||||
_decl.apply(style);
|
|
||||||
}
|
|
||||||
|
|
||||||
void setDeclaration(CssDeclaration decl) {
|
|
||||||
_decl = decl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CssDeclItem {
|
|
||||||
CssDeclType type = CssDeclType.unknown;
|
|
||||||
union {
|
|
||||||
int value;
|
|
||||||
CssValue length;
|
|
||||||
}
|
|
||||||
string str;
|
|
||||||
|
|
||||||
void apply(CssStyle style) const {
|
|
||||||
switch (type) with (CssDeclType) {
|
|
||||||
case display: style.display = cast(CssDisplay)value; break;
|
|
||||||
case white_space: style.whiteSpace = cast(CssWhiteSpace)value; break;
|
|
||||||
case text_align: style.textAlign = cast(CssTextAlign)value; break;
|
|
||||||
case text_align_last: style.textAlignLast = cast(CssTextAlign)value; break;
|
|
||||||
case text_decoration: style.textDecoration = cast(CssTextDecoration)value; break;
|
|
||||||
|
|
||||||
case _webkit_hyphens: // -webkit-hyphens
|
|
||||||
case adobe_hyphenate: // adobe-hyphenate
|
|
||||||
case adobe_text_layout: // adobe-text-layout
|
|
||||||
case hyphenate:
|
|
||||||
style.hyphenate = cast(CssHyphenate)value;
|
|
||||||
break; // hyphenate
|
|
||||||
|
|
||||||
case color: style.color = length; break;
|
|
||||||
case background_color: style.backgroundColor = length; break;
|
|
||||||
case vertical_align: style.verticalAlign = cast(CssVerticalAlign)value; break;
|
|
||||||
case font_family:
|
|
||||||
if (value >= 0)
|
|
||||||
style.fontFamily = cast(CssFontFamily)value;
|
|
||||||
if (!str.empty)
|
|
||||||
style.fontFaces = str;
|
|
||||||
break; // id families like serif, sans-serif
|
|
||||||
//case font_names: break; // string font name like Arial, Courier
|
|
||||||
case font_style: style.fontStyle = cast(CssFontStyle)value; break;
|
|
||||||
case font_weight: style.fontWeight = cast(CssFontWeight)value; break;
|
|
||||||
case text_indent: style.textIndent = length; break;
|
|
||||||
case font_size: style.fontSize = length; break;
|
|
||||||
case line_height: style.lineHeight = length; break;
|
|
||||||
case letter_spacing: style.letterSpacing = length; break;
|
|
||||||
case width: style.width = length; break;
|
|
||||||
case height: style.height = length; break;
|
|
||||||
case margin_left: style.marginLeft = length; break;
|
|
||||||
case margin_right: style.marginRight = length; break;
|
|
||||||
case margin_top: style.marginTop = length; break;
|
|
||||||
case margin_bottom: style.marginBottom = length; break;
|
|
||||||
case padding_left: style.paddingLeft = length; break;
|
|
||||||
case padding_right: style.paddingRight = length; break;
|
|
||||||
case padding_top: style.paddingTop = length; break;
|
|
||||||
case padding_bottom: style.paddingBottom = length; break;
|
|
||||||
case page_break_before: style.pageBreakBefore = cast(CssPageBreak)value; break;
|
|
||||||
case page_break_after: style.pageBreakAfter = cast(CssPageBreak)value; break;
|
|
||||||
case page_break_inside: style.pageBreakInside = cast(CssPageBreak)value; break;
|
|
||||||
case list_style: break; // TODO
|
|
||||||
case list_style_type: style.listStyleType = cast(CssListStyleType)value; break;
|
|
||||||
case list_style_position: style.listStylePosition = cast(CssListStylePosition)value; break;
|
|
||||||
case list_style_image: break; // TODO
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// css declaration like { display: block; margin-top: 10px }
|
|
||||||
class CssDeclaration {
|
|
||||||
private CssDeclItem[] _list;
|
|
||||||
|
|
||||||
@property bool empty() {
|
|
||||||
return _list.length == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void addLengthDecl(CssDeclType type, CssValue len) {
|
|
||||||
CssDeclItem item;
|
|
||||||
item.type = type;
|
|
||||||
item.length = len;
|
|
||||||
_list ~= item;
|
|
||||||
}
|
|
||||||
|
|
||||||
void addDecl(CssDeclType type, int value, string str) {
|
|
||||||
CssDeclItem item;
|
|
||||||
item.type = type;
|
|
||||||
item.value = value;
|
|
||||||
item.str = str;
|
|
||||||
_list ~= item;
|
|
||||||
}
|
|
||||||
|
|
||||||
void apply(CssStyle style) const {
|
|
||||||
foreach(item; _list)
|
|
||||||
item.apply(style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// CSS Style Sheet
|
|
||||||
class StyleSheet {
|
|
||||||
private:
|
|
||||||
CssSelector[elem_id] _selectorMap;
|
|
||||||
int _len;
|
|
||||||
public:
|
|
||||||
/// clears stylesheet
|
|
||||||
void clear() {
|
|
||||||
_selectorMap = null;
|
|
||||||
_len = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// count of selectors in stylesheet
|
|
||||||
@property int length() { return _len; }
|
|
||||||
|
|
||||||
/// add selector to stylesheet
|
|
||||||
void add(CssSelector selector) {
|
|
||||||
elem_id id = selector.id;
|
|
||||||
if (auto p = id in _selectorMap) {
|
|
||||||
for (;;) {
|
|
||||||
if (!(*p) || (*p)._specificity < selector._specificity) {
|
|
||||||
selector._next = (*p);
|
|
||||||
(*p) = selector;
|
|
||||||
_len++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
p = &((*p)._next);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// first selector for this id
|
|
||||||
_selectorMap[id] = selector;
|
|
||||||
_len++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// apply stylesheet to node style
|
|
||||||
void apply(Node node, CssStyle style) {
|
|
||||||
elem_id id = node.id;
|
|
||||||
CssSelector selector_0, selector_id;
|
|
||||||
if (auto p = 0 in _selectorMap)
|
|
||||||
selector_0 = *p;
|
|
||||||
if (id) {
|
|
||||||
if (auto p = id in _selectorMap)
|
|
||||||
selector_id = *p;
|
|
||||||
}
|
|
||||||
for (;;) {
|
|
||||||
if (selector_0) {
|
|
||||||
if (!selector_id || selector_id._specificity < selector_0._specificity) {
|
|
||||||
selector_0.apply(node, style);
|
|
||||||
selector_0 = selector_0._next;
|
|
||||||
} else {
|
|
||||||
selector_id.apply(node, style);
|
|
||||||
selector_id = selector_id._next;
|
|
||||||
}
|
|
||||||
} else if (selector_id) {
|
|
||||||
selector_id.apply(node, style);
|
|
||||||
selector_id = selector_id._next;
|
|
||||||
} else {
|
|
||||||
// end of lists
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unittest {
|
|
||||||
CssStyle style = new CssStyle();
|
|
||||||
CssWhiteSpace whiteSpace = CssWhiteSpace.inherit;
|
|
||||||
CssTextAlign textAlign = CssTextAlign.inherit;
|
|
||||||
CssTextAlign textAlignLast = CssTextAlign.inherit;
|
|
||||||
CssTextDecoration textDecoration = CssTextDecoration.inherit;
|
|
||||||
CssHyphenate hyphenate = CssHyphenate.inherit;
|
|
||||||
string src = "{ display: inline; text-decoration: underline; white-space: pre; text-align: right; text-align-last: left; " ~
|
|
||||||
"hyphenate: auto; width: 70%; height: 1.5pt; margin-left: 2.0em; " ~
|
|
||||||
"font-family: Arial, 'Times New Roman', sans-serif; font-size: 18pt; line-height: 120%; letter-spacing: 2px; font-weight: 300; " ~
|
|
||||||
" }tail";
|
|
||||||
CssDeclaration decl = parseCssDeclaration(src, true);
|
|
||||||
assert(decl !is null);
|
|
||||||
assert(!src.empty && src[0] == 't');
|
|
||||||
assert(style.display == CssDisplay.block);
|
|
||||||
assert(style.textDecoration == CssTextDecoration.inherit);
|
|
||||||
assert(style.whiteSpace == CssWhiteSpace.inherit);
|
|
||||||
assert(style.textAlign == CssTextAlign.inherit);
|
|
||||||
assert(style.textAlignLast == CssTextAlign.inherit);
|
|
||||||
assert(style.hyphenate == CssHyphenate.inherit);
|
|
||||||
assert(style.width == CssValue.inherited);
|
|
||||||
decl.apply(style);
|
|
||||||
assert(style.display == CssDisplay.inline);
|
|
||||||
assert(style.textDecoration == CssTextDecoration.underline);
|
|
||||||
assert(style.whiteSpace == CssWhiteSpace.pre);
|
|
||||||
assert(style.textAlign == CssTextAlign.right);
|
|
||||||
assert(style.textAlignLast == CssTextAlign.left);
|
|
||||||
assert(style.hyphenate == CssHyphenate.auto_);
|
|
||||||
assert(style.width == CssValue(CssValueType.percent, 70));
|
|
||||||
assert(style.height == CssValue(CssValueType.pt, 1*256 + 5*256/10)); // 1.5
|
|
||||||
assert(style.marginLeft == CssValue(CssValueType.em, 2*256 + 0*256/10)); // 2.0
|
|
||||||
assert(style.lineHeight == CssValue(CssValueType.percent, 120)); // 120%
|
|
||||||
assert(style.letterSpacing == CssValue(CssValueType.px, 2)); // 2px
|
|
||||||
assert(style.fontFamily == CssFontFamily.sans_serif);
|
|
||||||
assert(style.fontFaces == "\"Arial\", \"Times New Roman\"");
|
|
||||||
assert(style.fontWeight == CssFontWeight.fw_300);
|
|
||||||
}
|
|
|
@ -1,858 +0,0 @@
|
||||||
module dlangui.core.cssparser;
|
|
||||||
|
|
||||||
import std.traits;
|
|
||||||
import std.conv : to;
|
|
||||||
import std.string;
|
|
||||||
import std.array : empty;
|
|
||||||
import std.algorithm : equal;
|
|
||||||
import std.ascii : isAlpha, isWhite;
|
|
||||||
|
|
||||||
import dlangui.core.dom;
|
|
||||||
import dlangui.core.css;
|
|
||||||
import dlangui.core.types : parseHexDigit;
|
|
||||||
|
|
||||||
/// skip specified count of chars of string, returns next available character, or 0 if end of string reached
|
|
||||||
private char skip(ref string src, int count = 1) {
|
|
||||||
if (count >= src.length) {
|
|
||||||
src = null;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
src = src[count .. $];
|
|
||||||
return src[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// returns char of string at specified position (first by default) or 0 if end of string reached
|
|
||||||
private char peek(string str, int offset = 0) {
|
|
||||||
return offset >= str.length ? 0 : str[offset];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// skip spaces, move to new location, return first character in string, 0 if end of string reached
|
|
||||||
private char skipSpaces(ref string str)
|
|
||||||
{
|
|
||||||
string oldpos = str;
|
|
||||||
for (;;) {
|
|
||||||
char ch = str.peek;
|
|
||||||
if (!ch)
|
|
||||||
return 0;
|
|
||||||
while (isWhite(ch))
|
|
||||||
ch = str.skip;
|
|
||||||
if (str.peek == '/' && str.peek(1) == '*') {
|
|
||||||
// comment found
|
|
||||||
str.skip(2);
|
|
||||||
while (str.peek && (str.peek(0) != '*' || str.peek(1) != '/'))
|
|
||||||
str.skip;
|
|
||||||
if (str.peek == '*' && str.peek(1) == '/' )
|
|
||||||
str.skip(2);
|
|
||||||
}
|
|
||||||
ch = str.peek;
|
|
||||||
while (isWhite(ch))
|
|
||||||
ch = str.skip;
|
|
||||||
if (oldpos.ptr is str.ptr)
|
|
||||||
break;
|
|
||||||
if (str.empty)
|
|
||||||
return 0;
|
|
||||||
oldpos = str;
|
|
||||||
}
|
|
||||||
return str.peek;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private bool isIdentChar(char ch) {
|
|
||||||
return isAlpha(ch) || (ch == '-') || (ch == '_');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// parse css identifier
|
|
||||||
private string parseIdent(ref string src) {
|
|
||||||
int pos = 0;
|
|
||||||
for ( ; pos < src.length; pos++) {
|
|
||||||
if (!src[pos].isIdentChar)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (!pos)
|
|
||||||
return null;
|
|
||||||
string res = src[0 .. pos];
|
|
||||||
src.skip(pos);
|
|
||||||
src.skipSpaces;
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool skipChar(ref string src, char ch) {
|
|
||||||
src.skipSpaces;
|
|
||||||
if (src.peek == ch) {
|
|
||||||
src.skip;
|
|
||||||
src.skipSpaces;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string replaceChar(string s, char from, char to) {
|
|
||||||
foreach(ch; s) {
|
|
||||||
if (ch == from) {
|
|
||||||
char[] buf;
|
|
||||||
foreach(c; s)
|
|
||||||
if (c == from)
|
|
||||||
buf ~= to;
|
|
||||||
else
|
|
||||||
buf ~= c;
|
|
||||||
return buf.dup;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// remove trailing _ from string, e.g. "body_" -> "body"
|
|
||||||
private string removeTrailingUnderscore(string s) {
|
|
||||||
if (s.endsWith("_"))
|
|
||||||
return s[0..$-1];
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int parseEnumItem(E)(ref string src, int defValue = -1) if (is(E == enum)) {
|
|
||||||
string ident = replaceChar(parseIdent(src), '-', '_');
|
|
||||||
foreach(member; EnumMembers!E) {
|
|
||||||
if (ident == removeTrailingUnderscore(member.to!string)) {
|
|
||||||
return member.to!int;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return defValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
private CssDeclType parseCssDeclType(ref string src) {
|
|
||||||
int n = parseEnumItem!CssDeclType(src, -1);
|
|
||||||
if (n < 0)
|
|
||||||
return CssDeclType.unknown;
|
|
||||||
if (!skipChar(src, ':')) // no : after identifier
|
|
||||||
return CssDeclType.unknown;
|
|
||||||
return cast(CssDeclType)n;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool nextProperty(ref string str) {
|
|
||||||
int pos = 0;
|
|
||||||
for (; pos < str.length; pos++) {
|
|
||||||
char ch = str[pos];
|
|
||||||
if (ch == '}')
|
|
||||||
break;
|
|
||||||
if (ch == ';') {
|
|
||||||
pos++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
str.skip(pos);
|
|
||||||
str.skipSpaces;
|
|
||||||
return !str.empty && str[0] != '}';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private int parseStandardColor(string ident) {
|
|
||||||
switch(ident) {
|
|
||||||
case "black": return 0x000000;
|
|
||||||
case "green": return 0x008000;
|
|
||||||
case "silver": return 0xC0C0C0;
|
|
||||||
case "lime": return 0x00FF00;
|
|
||||||
case "gray": return 0x808080;
|
|
||||||
case "olive": return 0x808000;
|
|
||||||
case "white": return 0xFFFFFF;
|
|
||||||
case "yellow": return 0xFFFF00;
|
|
||||||
case "maroon": return 0x800000;
|
|
||||||
case "navy": return 0x000080;
|
|
||||||
case "red": return 0xFF0000;
|
|
||||||
case "blue": return 0x0000FF;
|
|
||||||
case "purple": return 0x800080;
|
|
||||||
case "teal": return 0x008080;
|
|
||||||
case "fuchsia": return 0xFF00FF;
|
|
||||||
case "aqua": return 0x00FFFF;
|
|
||||||
default: return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool parseColor(ref string src, ref CssValue value)
|
|
||||||
{
|
|
||||||
value.type = CssValueType.unspecified;
|
|
||||||
value.value = 0;
|
|
||||||
if (!src.skipSpaces)
|
|
||||||
return false;
|
|
||||||
string ident = parseIdent(src);
|
|
||||||
if (!ident.empty) {
|
|
||||||
switch(ident) {
|
|
||||||
case "inherited":
|
|
||||||
value.type = CssValueType.inherited;
|
|
||||||
return true;
|
|
||||||
case "none":
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
int v = parseStandardColor(ident);
|
|
||||||
if (v >= 0) {
|
|
||||||
value.value = v;
|
|
||||||
value.type = CssValueType.color;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
char ch = src[0];
|
|
||||||
if (ch == '#') {
|
|
||||||
// #rgb or #rrggbb colors
|
|
||||||
ch = src.skip;
|
|
||||||
int nDigits = 0;
|
|
||||||
for ( ; nDigits < src.length && parseHexDigit(src[nDigits]) != uint.max; nDigits++ ) {
|
|
||||||
}
|
|
||||||
if ( nDigits==3 ) {
|
|
||||||
int r = parseHexDigit( src[0] );
|
|
||||||
int g = parseHexDigit( src[1] );
|
|
||||||
int b = parseHexDigit( src[2] );
|
|
||||||
value.type = CssValueType.color;
|
|
||||||
value.value = (((r + r*16) * 256) | (g + g*16)) * 256 | (b + b*16);
|
|
||||||
src.skip(3);
|
|
||||||
return true;
|
|
||||||
} else if ( nDigits==6 ) {
|
|
||||||
int r = parseHexDigit( src[0] ) * 16;
|
|
||||||
r += parseHexDigit( src[1] );
|
|
||||||
int g = parseHexDigit( src[2] ) * 16;
|
|
||||||
g += parseHexDigit( src[3] );
|
|
||||||
int b = parseHexDigit( src[4] ) * 16;
|
|
||||||
b += parseHexDigit( src[5] );
|
|
||||||
value.type = CssValueType.color;
|
|
||||||
value.value = ((r * 256) | g) * 256 | b;
|
|
||||||
src.skip(6);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool parseLength(ref string src, ref CssValue value)
|
|
||||||
{
|
|
||||||
value.type = CssValueType.unspecified;
|
|
||||||
value.value = 0;
|
|
||||||
src.skipSpaces;
|
|
||||||
string ident = parseIdent(src);
|
|
||||||
if (!ident.empty) {
|
|
||||||
switch(ident) {
|
|
||||||
case "inherited":
|
|
||||||
value.type = CssValueType.inherited;
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (src.empty)
|
|
||||||
return false;
|
|
||||||
int n = 0;
|
|
||||||
char ch = src[0];
|
|
||||||
if (ch != '.') {
|
|
||||||
if (ch < '0' || ch > '9') {
|
|
||||||
return false; // not a number
|
|
||||||
}
|
|
||||||
while (ch >= '0' && ch <= '9') {
|
|
||||||
n = n*10 + (ch - '0');
|
|
||||||
ch = src.skip;
|
|
||||||
if (!ch)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
int frac = 0;
|
|
||||||
int frac_div = 1;
|
|
||||||
if (ch == '.') {
|
|
||||||
src.skip;
|
|
||||||
if (!src.empty) {
|
|
||||||
ch = src[0];
|
|
||||||
while (ch >= '0' && ch <= '9') {
|
|
||||||
frac = frac*10 + (ch - '0');
|
|
||||||
frac_div *= 10;
|
|
||||||
ch = src.skip;
|
|
||||||
if (!ch)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (ch == '%') {
|
|
||||||
value.type = CssValueType.percent;
|
|
||||||
src.skip;
|
|
||||||
} else {
|
|
||||||
ident = parseIdent(src);
|
|
||||||
if (!ident.empty) {
|
|
||||||
switch(ident) {
|
|
||||||
case "em":
|
|
||||||
case "m": // for DML - cannot add suffix which starts from 'e'
|
|
||||||
value.type = CssValueType.em; break;
|
|
||||||
case "pt": value.type = CssValueType.pt; break;
|
|
||||||
case "ex": value.type = CssValueType.ex; break;
|
|
||||||
case "px": value.type = CssValueType.px; break;
|
|
||||||
case "in": value.type = CssValueType.in_; break;
|
|
||||||
case "cm": value.type = CssValueType.cm; break;
|
|
||||||
case "mm": value.type = CssValueType.mm; break;
|
|
||||||
case "pc": value.type = CssValueType.pc; break;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
value.type = CssValueType.px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ( value.type == CssValueType.px || value.type == CssValueType.percent )
|
|
||||||
value.value = n; // normal
|
|
||||||
else
|
|
||||||
value.value = n * 256 + 256 * frac / frac_div; // *256
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void appendItem(ref string[] list, ref char[] item) {
|
|
||||||
if (!item.empty) {
|
|
||||||
list ~= item.dup;
|
|
||||||
item.length = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// splits string like "Arial", Times New Roman, Courier; into list, stops on ; and }
|
|
||||||
/// returns true if at least one item added to list; moves str to new position
|
|
||||||
bool splitPropertyValueList(ref string str, ref string[] list)
|
|
||||||
{
|
|
||||||
int i=0;
|
|
||||||
char quote_char = 0;
|
|
||||||
char[] name;
|
|
||||||
bool last_space = false;
|
|
||||||
for (i=0; i < str.length; i++) {
|
|
||||||
char ch = str[i];
|
|
||||||
switch(ch) {
|
|
||||||
case '\'':
|
|
||||||
case '\"':
|
|
||||||
if (quote_char == 0) {
|
|
||||||
if (!name.empty)
|
|
||||||
appendItem(list, name);
|
|
||||||
quote_char = ch;
|
|
||||||
} else if (quote_char == ch) {
|
|
||||||
if (!name.empty)
|
|
||||||
appendItem(list, name);
|
|
||||||
quote_char = 0;
|
|
||||||
} else {
|
|
||||||
// append char
|
|
||||||
name ~= ch;
|
|
||||||
}
|
|
||||||
last_space = false;
|
|
||||||
break;
|
|
||||||
case ',':
|
|
||||||
{
|
|
||||||
if (quote_char==0) {
|
|
||||||
if (!name.empty)
|
|
||||||
appendItem(list, name);
|
|
||||||
} else {
|
|
||||||
// inside quotation: append char
|
|
||||||
name ~= ch;
|
|
||||||
}
|
|
||||||
last_space = false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case '\t':
|
|
||||||
case ' ':
|
|
||||||
{
|
|
||||||
if (quote_char != 0)
|
|
||||||
name ~= ch;
|
|
||||||
last_space = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ';':
|
|
||||||
case '}':
|
|
||||||
if (quote_char==0) {
|
|
||||||
if (!name.empty)
|
|
||||||
appendItem(list, name);
|
|
||||||
str = i < str.length ? str[i .. $] : null;
|
|
||||||
return list.length > 0;
|
|
||||||
} else {
|
|
||||||
// inside quotation: append char
|
|
||||||
name ~= ch;
|
|
||||||
last_space = false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (last_space && !name.empty && quote_char == 0)
|
|
||||||
name ~= ' ';
|
|
||||||
name ~= ch;
|
|
||||||
last_space = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!name.empty)
|
|
||||||
appendItem(list, name);
|
|
||||||
str = i < str.length ? str[i .. $] : null;
|
|
||||||
return list.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
unittest {
|
|
||||||
string src = "Arial, 'Times New Roman', \"Arial Black\", sans-serif; next-property: }";
|
|
||||||
string[] list;
|
|
||||||
assert(splitPropertyValueList(src, list));
|
|
||||||
assert(list.length == 4);
|
|
||||||
assert(list[0] == "Arial");
|
|
||||||
assert(list[1] == "Times New Roman");
|
|
||||||
assert(list[2] == "Arial Black");
|
|
||||||
assert(list[3] == "sans-serif");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// joins list of items into comma separated string, each item in quotation marks
|
|
||||||
string joinPropertyValueList(string[] list) {
|
|
||||||
if (list.empty)
|
|
||||||
return null;
|
|
||||||
char[] res;
|
|
||||||
|
|
||||||
for (int i = 0; i < list.length; i++) {
|
|
||||||
if (i > 0)
|
|
||||||
res ~= ", ";
|
|
||||||
res ~= "\"";
|
|
||||||
res ~= list[i];
|
|
||||||
res ~= "\"";
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.dup;
|
|
||||||
}
|
|
||||||
|
|
||||||
unittest {
|
|
||||||
assert(joinPropertyValueList(["item1", "item 2"]) == "\"item1\", \"item 2\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private bool parseAttrValue(ref string str, ref string attrvalue)
|
|
||||||
{
|
|
||||||
char[] buf;
|
|
||||||
int pos = 0;
|
|
||||||
if (!str.skipSpaces)
|
|
||||||
return false;
|
|
||||||
char ch = str[0];
|
|
||||||
if (ch == '\"') {
|
|
||||||
str.skip;
|
|
||||||
for ( ; pos < str.length && str[pos] != '\"'; pos++) {
|
|
||||||
if (pos >= 1000)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (pos >= str.length || str[pos] != '\"')
|
|
||||||
return false;
|
|
||||||
buf ~= str[0 .. pos];
|
|
||||||
str.skip(pos + 1);
|
|
||||||
if (!str.skipSpaces)
|
|
||||||
return false;
|
|
||||||
if (str[0] != ']')
|
|
||||||
return false;
|
|
||||||
str.skip;
|
|
||||||
attrvalue = buf.dup;
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
for ( ; pos < str.length && str[pos] != ' ' && str[pos] != '\t' && str[pos] != ']'; pos++) {
|
|
||||||
if (pos >= 1000)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (pos >= str.length || str[pos] != ']')
|
|
||||||
return false;
|
|
||||||
buf ~= str[0 .. pos];
|
|
||||||
str.skip(pos + 1);
|
|
||||||
attrvalue = buf.dup;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private CssSelectorRule parseAttr(ref string str, Document doc)
|
|
||||||
{
|
|
||||||
CssSelectorRuleType st = CssSelectorRuleType.universal;
|
|
||||||
char ch = str[0];
|
|
||||||
if (ch == '.') {
|
|
||||||
// E.class
|
|
||||||
str.skip;
|
|
||||||
str.skipSpaces;
|
|
||||||
string attrvalue = parseIdent(str);
|
|
||||||
if (attrvalue.empty)
|
|
||||||
return null;
|
|
||||||
CssSelectorRule rule = new CssSelectorRule(CssSelectorRuleType.class_);
|
|
||||||
rule.setAttr(Attr.class_, attrvalue.toLower);
|
|
||||||
return rule;
|
|
||||||
} else if (ch == '#') {
|
|
||||||
// E#id
|
|
||||||
str.skip;
|
|
||||||
str.skipSpaces;
|
|
||||||
string attrvalue = parseIdent(str);
|
|
||||||
if (attrvalue.empty)
|
|
||||||
return null;
|
|
||||||
CssSelectorRule rule = new CssSelectorRule(CssSelectorRuleType.id);
|
|
||||||
rule.setAttr(Attr.id, attrvalue.toLower);
|
|
||||||
return rule;
|
|
||||||
} else if (ch != '[')
|
|
||||||
return null;
|
|
||||||
// [.....] rule
|
|
||||||
str.skip; // skip [
|
|
||||||
str.skipSpaces;
|
|
||||||
string attrname = parseIdent(str);
|
|
||||||
if (attrname.empty)
|
|
||||||
return null;
|
|
||||||
if (!str.skipSpaces)
|
|
||||||
return null;
|
|
||||||
string attrvalue = null;
|
|
||||||
ch = str[0];
|
|
||||||
if (ch == ']') {
|
|
||||||
// empty []
|
|
||||||
st = CssSelectorRuleType.attrset;
|
|
||||||
str.skip; // skip ]
|
|
||||||
} else if (ch == '=') {
|
|
||||||
str.skip; // skip =
|
|
||||||
if (!parseAttrValue(str, attrvalue))
|
|
||||||
return null;
|
|
||||||
st = CssSelectorRuleType.attreq;
|
|
||||||
} else if (ch == '~' && str.length > 1 && str[1] == '=') {
|
|
||||||
str.skip(2); // skip ~=
|
|
||||||
if (!parseAttrValue(str, attrvalue))
|
|
||||||
return null;
|
|
||||||
st = CssSelectorRuleType.attrhas;
|
|
||||||
} else if (ch == '|' && str.length > 1 && str[1] == '=') {
|
|
||||||
str.skip(2); // skip |=
|
|
||||||
if (!parseAttrValue(str, attrvalue))
|
|
||||||
return null;
|
|
||||||
st = CssSelectorRuleType.attrstarts;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
CssSelectorRule rule = new CssSelectorRule(st);
|
|
||||||
attr_id id = doc.attrId(attrname);
|
|
||||||
rule.setAttr(id, attrvalue);
|
|
||||||
return rule;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse css properties declaration either in {} or w/o {} - e.g. { width: 40%; margin-top: 3px } -- returns null if parse error occured or property list is empty
|
|
||||||
CssDeclaration parseCssDeclaration(ref string src, bool mustBeInBrackets = true) {
|
|
||||||
if (!src.skipSpaces)
|
|
||||||
return null;
|
|
||||||
if (mustBeInBrackets && !skipChar(src, '{'))
|
|
||||||
return null; // decl must start with {
|
|
||||||
CssDeclaration res = new CssDeclaration();
|
|
||||||
for (;;) {
|
|
||||||
CssDeclType propId = parseCssDeclType(src);
|
|
||||||
if (src.empty)
|
|
||||||
break;
|
|
||||||
if (propId != CssDeclType.unknown) {
|
|
||||||
int n = -1;
|
|
||||||
string s = null;
|
|
||||||
switch(propId) with(CssDeclType) {
|
|
||||||
case display: n = parseEnumItem!CssDisplay(src, -1); break;
|
|
||||||
case white_space: n = parseEnumItem!CssWhiteSpace(src, -1); break;
|
|
||||||
case text_align: n = parseEnumItem!CssTextAlign(src, -1); break;
|
|
||||||
case text_align_last: n = parseEnumItem!CssTextAlign(src, -1); break;
|
|
||||||
case text_decoration: n = parseEnumItem!CssTextDecoration(src, -1); break;
|
|
||||||
case hyphenate:
|
|
||||||
case _webkit_hyphens: // -webkit-hyphens
|
|
||||||
case adobe_hyphenate: // adobe-hyphenate
|
|
||||||
case adobe_text_layout: // adobe-text-layout
|
|
||||||
n = parseEnumItem!CssHyphenate(src, -1);
|
|
||||||
break; // hyphenate
|
|
||||||
case color:
|
|
||||||
case background_color:
|
|
||||||
CssValue v;
|
|
||||||
if (parseColor(src, v)) {
|
|
||||||
res.addLengthDecl(propId, v);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case vertical_align: n = parseEnumItem!CssVerticalAlign(src, -1); break;
|
|
||||||
case font_family: // id families like serif, sans-serif
|
|
||||||
string[] list;
|
|
||||||
string[] faceList;
|
|
||||||
if (splitPropertyValueList(src, list)) {
|
|
||||||
foreach(item; list) {
|
|
||||||
string name = item;
|
|
||||||
int family = parseEnumItem!CssFontFamily(name, -1);
|
|
||||||
if (family != -1) {
|
|
||||||
// family name, e.g. sans-serif
|
|
||||||
n = family;
|
|
||||||
} else {
|
|
||||||
faceList ~= item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s = joinPropertyValueList(faceList);
|
|
||||||
break;
|
|
||||||
case font_style: n = parseEnumItem!CssFontStyle(src, -1); break;
|
|
||||||
case font_weight:
|
|
||||||
n = parseEnumItem!CssFontWeight(src, -1);
|
|
||||||
if (n < 0) {
|
|
||||||
CssValue value;
|
|
||||||
if (parseLength(src, value)) {
|
|
||||||
if (value.type == CssValueType.px) {
|
|
||||||
if (value.value < 150)
|
|
||||||
n = CssFontWeight.fw_100;
|
|
||||||
else if (value.value < 250)
|
|
||||||
n = CssFontWeight.fw_200;
|
|
||||||
else if (value.value < 350)
|
|
||||||
n = CssFontWeight.fw_300;
|
|
||||||
else if (value.value < 450)
|
|
||||||
n = CssFontWeight.fw_400;
|
|
||||||
else if (value.value < 550)
|
|
||||||
n = CssFontWeight.fw_500;
|
|
||||||
else if (value.value < 650)
|
|
||||||
n = CssFontWeight.fw_600;
|
|
||||||
else if (value.value < 750)
|
|
||||||
n = CssFontWeight.fw_700;
|
|
||||||
else if (value.value < 850)
|
|
||||||
n = CssFontWeight.fw_800;
|
|
||||||
else
|
|
||||||
n = CssFontWeight.fw_900;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//n = parseEnumItem!Css(src, -1);
|
|
||||||
break;
|
|
||||||
case text_indent:
|
|
||||||
{
|
|
||||||
// read length
|
|
||||||
CssValue len;
|
|
||||||
bool negative = false;
|
|
||||||
if (src[0] == '-') {
|
|
||||||
src.skip;
|
|
||||||
negative = true;
|
|
||||||
}
|
|
||||||
if (parseLength(src, len)) {
|
|
||||||
// read optional "hanging" flag
|
|
||||||
src.skipSpaces;
|
|
||||||
string attr = parseIdent(src);
|
|
||||||
if (attr == "hanging")
|
|
||||||
len.value = -len.value;
|
|
||||||
res.addLengthDecl(propId, len);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case line_height:
|
|
||||||
case letter_spacing:
|
|
||||||
case font_size:
|
|
||||||
case width:
|
|
||||||
case height:
|
|
||||||
case margin_left:
|
|
||||||
case margin_right:
|
|
||||||
case margin_top:
|
|
||||||
case margin_bottom:
|
|
||||||
case padding_left:
|
|
||||||
case padding_right:
|
|
||||||
case padding_top:
|
|
||||||
case padding_bottom:
|
|
||||||
// parse length
|
|
||||||
CssValue value;
|
|
||||||
if (parseLength(src, value))
|
|
||||||
res.addLengthDecl(propId, value);
|
|
||||||
break;
|
|
||||||
case margin:
|
|
||||||
case padding:
|
|
||||||
//n = parseEnumItem!Css(src, -1);
|
|
||||||
CssValue[4] len;
|
|
||||||
int i;
|
|
||||||
for (i = 0; i < 4; ++i)
|
|
||||||
if (!parseLength(src, len[i]))
|
|
||||||
break;
|
|
||||||
if (i) {
|
|
||||||
switch (i) {
|
|
||||||
case 1:
|
|
||||||
len[1] = len[0];
|
|
||||||
goto case; /* fall through */
|
|
||||||
case 2:
|
|
||||||
len[2] = len[0];
|
|
||||||
goto case; /* fall through */
|
|
||||||
case 3:
|
|
||||||
len[3] = len[1];
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (propId == margin) {
|
|
||||||
res.addLengthDecl(margin_left, len[0]);
|
|
||||||
res.addLengthDecl(margin_top, len[1]);
|
|
||||||
res.addLengthDecl(margin_right, len[2]);
|
|
||||||
res.addLengthDecl(margin_bottom, len[3]);
|
|
||||||
} else {
|
|
||||||
res.addLengthDecl(padding_left, len[0]);
|
|
||||||
res.addLengthDecl(padding_top, len[1]);
|
|
||||||
res.addLengthDecl(padding_right, len[2]);
|
|
||||||
res.addLengthDecl(padding_bottom, len[3]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case page_break_before:
|
|
||||||
case page_break_inside:
|
|
||||||
case page_break_after:
|
|
||||||
n = parseEnumItem!CssPageBreak(src, -1);
|
|
||||||
break;
|
|
||||||
case list_style:
|
|
||||||
//n = parseEnumItem!Css(src, -1);
|
|
||||||
break;
|
|
||||||
case list_style_type: n = parseEnumItem!CssListStyleType(src, -1); break;
|
|
||||||
case list_style_position: n = parseEnumItem!CssListStylePosition(src, -1); break;
|
|
||||||
case list_style_image:
|
|
||||||
//n = parseEnumItem!CssListStyleImage(src, -1);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (n >= 0 || !s.empty)
|
|
||||||
res.addDecl(propId, n, s);
|
|
||||||
}
|
|
||||||
if (!nextProperty(src))
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (mustBeInBrackets && !skipChar(src, '}'))
|
|
||||||
return null;
|
|
||||||
if (res.empty)
|
|
||||||
return null;
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// parse Css selector, return selector object if parsed ok, null if error occured
|
|
||||||
CssSelector parseCssSelector(ref string str, Document doc) {
|
|
||||||
if (str.empty)
|
|
||||||
return null;
|
|
||||||
CssSelector res = new CssSelector();
|
|
||||||
for (;;) {
|
|
||||||
if (!str.skipSpaces)
|
|
||||||
return null;
|
|
||||||
char ch = str[0];
|
|
||||||
string ident = parseIdent(str);
|
|
||||||
if (ch == '*') { // universal selector
|
|
||||||
str.skip;
|
|
||||||
str.skipSpaces;
|
|
||||||
res.id = 0;
|
|
||||||
} else if (ch == '.') { // classname follows
|
|
||||||
res.id = 0;
|
|
||||||
// will be parsed as attribute
|
|
||||||
} else if (!ident.empty) {
|
|
||||||
// ident
|
|
||||||
res.id = doc.tagId(ident);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!str.skipSpaces)
|
|
||||||
return null;
|
|
||||||
ch = str[0];
|
|
||||||
if (ch == ',' || ch == '{')
|
|
||||||
return res;
|
|
||||||
// one or more attribute rules
|
|
||||||
bool attr_rule = false;
|
|
||||||
while (ch == '[' || ch == '.' || ch == '#') {
|
|
||||||
CssSelectorRule rule = parseAttr(str, doc);
|
|
||||||
if (!rule)
|
|
||||||
return null;
|
|
||||||
res.insertRuleStart(rule); //insertRuleAfterStart
|
|
||||||
ch = str.skipSpaces;
|
|
||||||
attr_rule = true;
|
|
||||||
//continue;
|
|
||||||
}
|
|
||||||
// element relation
|
|
||||||
if (ch == '>') {
|
|
||||||
str.skip;
|
|
||||||
CssSelectorRule rule = new CssSelectorRule(CssSelectorRuleType.parent);
|
|
||||||
rule.id = res.id;
|
|
||||||
res.insertRuleStart(rule);
|
|
||||||
res.id = 0;
|
|
||||||
continue;
|
|
||||||
} else if (ch == '+') {
|
|
||||||
str.skip;
|
|
||||||
CssSelectorRule rule = new CssSelectorRule(CssSelectorRuleType.predecessor);
|
|
||||||
rule.id = res.id;
|
|
||||||
res.insertRuleStart(rule);
|
|
||||||
res.id = 0;
|
|
||||||
continue;
|
|
||||||
} else if (ch.isAlpha) {
|
|
||||||
CssSelectorRule rule = new CssSelectorRule(CssSelectorRuleType.ancessor);
|
|
||||||
rule.id = res.id;
|
|
||||||
res.insertRuleStart(rule);
|
|
||||||
res.id = 0;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!attr_rule)
|
|
||||||
return null;
|
|
||||||
else if (str.length > 0 && (str[0] == ',' || str[0] == '{'))
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// skips until } or end of string, returns true if some characters left in string
|
|
||||||
private bool skipUntilEndOfRule(ref string str)
|
|
||||||
{
|
|
||||||
while (str.length && str[0] != '}')
|
|
||||||
str.skip;
|
|
||||||
if (str.peek == '}')
|
|
||||||
str.skip;
|
|
||||||
return !str.empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
unittest {
|
|
||||||
Document doc = new Document();
|
|
||||||
string str;
|
|
||||||
str = "body { width: 50% }";
|
|
||||||
assert(parseCssSelector(str, doc) !is null);
|
|
||||||
assert(parseCssDeclaration(str, true) !is null);
|
|
||||||
str = "body > p { font-family: sans-serif }";
|
|
||||||
assert(parseCssSelector(str, doc) !is null);
|
|
||||||
assert(parseCssDeclaration(str, true) !is null);
|
|
||||||
str = ".myclass + div { }";
|
|
||||||
assert(parseCssSelector(str, doc) !is null);
|
|
||||||
assert(parseCssDeclaration(str, true) is null); // empty property decl
|
|
||||||
destroy(doc);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// parse stylesheet text
|
|
||||||
bool parseStyleSheet(StyleSheet sheet, Document doc, string str) {
|
|
||||||
bool res = false;
|
|
||||||
for(;;) {
|
|
||||||
if (!str.skipSpaces)
|
|
||||||
break;
|
|
||||||
CssSelector[] selectors;
|
|
||||||
for(;;) {
|
|
||||||
CssSelector selector = parseCssSelector(str, doc);
|
|
||||||
if (!selector)
|
|
||||||
break;
|
|
||||||
selectors ~= selector;
|
|
||||||
str.skipChar(',');
|
|
||||||
}
|
|
||||||
if (selectors.length) {
|
|
||||||
if (CssDeclaration decl = parseCssDeclaration(str, true)) {
|
|
||||||
foreach(item; selectors) {
|
|
||||||
item.setDeclaration(decl);
|
|
||||||
sheet.add(item);
|
|
||||||
res = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!skipUntilEndOfRule(str))
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
unittest {
|
|
||||||
string src = q{
|
|
||||||
body { width: 50%; color: blue }
|
|
||||||
body > div, body > section {
|
|
||||||
/* some comment
|
|
||||||
goes here */
|
|
||||||
font-family: serif;
|
|
||||||
background-color: yellow;
|
|
||||||
}
|
|
||||||
section {
|
|
||||||
margin-top: 5px
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Document doc = new Document();
|
|
||||||
StyleSheet sheet = new StyleSheet();
|
|
||||||
assert(parseStyleSheet(sheet, doc, src));
|
|
||||||
assert(sheet.length == 2);
|
|
||||||
// check appending of additional source text
|
|
||||||
assert(parseStyleSheet(sheet, doc, "pre { white-space: pre }"));
|
|
||||||
assert(sheet.length == 3);
|
|
||||||
destroy(doc);
|
|
||||||
}
|
|
||||||
|
|
||||||
unittest {
|
|
||||||
Document doc = new Document();
|
|
||||||
StyleSheet sheet = new StyleSheet();
|
|
||||||
assert(parseStyleSheet(sheet, doc, "* { color: #aaa }"));
|
|
||||||
assert(sheet.length == 1);
|
|
||||||
assert(parseStyleSheet(sheet, doc, "div, p { display: block }"));
|
|
||||||
assert(sheet.length == 3);
|
|
||||||
// check appending of additional source text
|
|
||||||
assert(parseStyleSheet(sheet, doc, "pre { white-space: pre }"));
|
|
||||||
assert(sheet.length == 4);
|
|
||||||
assert(parseStyleSheet(sheet, doc, "pre { font-size: 120% }"));
|
|
||||||
assert(sheet.length == 5);
|
|
||||||
destroy(doc);
|
|
||||||
}
|
|
|
@ -1,456 +0,0 @@
|
||||||
// Written in the D programming language.
|
|
||||||
|
|
||||||
/**
|
|
||||||
This module contains implementation DOM - document object model.
|
|
||||||
|
|
||||||
Port of CoolReader Engine written in C++.
|
|
||||||
|
|
||||||
Synopsis:
|
|
||||||
|
|
||||||
----
|
|
||||||
import dlangui.core.dom;
|
|
||||||
|
|
||||||
----
|
|
||||||
|
|
||||||
Copyright: Vadim Lopatin, 2015
|
|
||||||
License: Boost License 1.0
|
|
||||||
Authors: Vadim Lopatin, coolreader.org@gmail.com
|
|
||||||
*/
|
|
||||||
module dlangui.core.dom;
|
|
||||||
|
|
||||||
import dlangui.core.collections;
|
|
||||||
|
|
||||||
import std.traits;
|
|
||||||
import std.conv : to;
|
|
||||||
import std.string : startsWith, endsWith;
|
|
||||||
import std.array : empty;
|
|
||||||
import std.algorithm : equal;
|
|
||||||
|
|
||||||
// Namespace, element tag and attribute names are stored as numeric ids for better performance and lesser memory consumption.
|
|
||||||
|
|
||||||
/// id type for interning namespaces
|
|
||||||
alias ns_id = short;
|
|
||||||
/// id type for interning element names
|
|
||||||
alias elem_id = int;
|
|
||||||
/// id type for interning attribute names
|
|
||||||
alias attr_id = short;
|
|
||||||
|
|
||||||
|
|
||||||
/// Base class for DOM nodes
|
|
||||||
class Node {
|
|
||||||
private:
|
|
||||||
Node _parent;
|
|
||||||
Document _document;
|
|
||||||
public:
|
|
||||||
/// returns parent node
|
|
||||||
@property Node parent() { return _parent; }
|
|
||||||
/// returns document node
|
|
||||||
@property Document document() { return _document; }
|
|
||||||
|
|
||||||
/// return element tag id
|
|
||||||
@property elem_id id() { return 0; }
|
|
||||||
/// return element namespace id
|
|
||||||
@property ns_id nsid() { return 0; }
|
|
||||||
/// return element tag name
|
|
||||||
@property string name() { return document.tagName(id); }
|
|
||||||
/// return element namespace name
|
|
||||||
@property string nsname() { return document.nsName(nsid); }
|
|
||||||
|
|
||||||
// node properties
|
|
||||||
|
|
||||||
/// returns true if node is text
|
|
||||||
@property bool isText() { return false; }
|
|
||||||
/// returns true if node is element
|
|
||||||
@property bool isElement() { return false; }
|
|
||||||
/// returns true if node has child nodes
|
|
||||||
@property bool hasChildren() { return false; }
|
|
||||||
|
|
||||||
// attributes
|
|
||||||
|
|
||||||
/// returns attribute count
|
|
||||||
@property int attrCount() { return 0; }
|
|
||||||
|
|
||||||
/// get attribute by index
|
|
||||||
Attribute attr(int index) { return null; }
|
|
||||||
/// get attribute by namespace and attribute ids
|
|
||||||
Attribute attr(ns_id nsid, attr_id attrid) { return null; }
|
|
||||||
/// get attribute by namespace and attribute names
|
|
||||||
Attribute attr(string nsname, string attrname) { return attr(_document.nsId(nsname), _document.attrId(attrname)); }
|
|
||||||
|
|
||||||
/// set attribute value by namespace and attribute ids
|
|
||||||
Attribute setAttr(ns_id nsid, attr_id attrid, string value) { assert(false); }
|
|
||||||
/// set attribute value by namespace and attribute names
|
|
||||||
Attribute setAttr(string nsname, string attrname, string value) { return setAttr(_document.nsId(nsname), _document.attrId(attrname), value); }
|
|
||||||
/// get attribute value by namespace and attribute ids
|
|
||||||
string attrValue(ns_id nsid, attr_id attrid) { return null; }
|
|
||||||
/// get attribute value by namespace and attribute ids
|
|
||||||
string attrValue(string nsname, string attrname) { return attrValue(_document.nsId(nsname), _document.attrId(attrname)); }
|
|
||||||
/// returns true if node has attribute with specified name
|
|
||||||
bool hasAttr(string attrname) {
|
|
||||||
return hasAttr(document.attrId(attrname));
|
|
||||||
}
|
|
||||||
/// returns true if node has attribute with specified id
|
|
||||||
bool hasAttr(attr_id attrid) {
|
|
||||||
if (Attribute a = attr(Ns.any, attrid))
|
|
||||||
return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// child nodes
|
|
||||||
|
|
||||||
/// returns child node count
|
|
||||||
@property int childCount() { return 0; }
|
|
||||||
/// returns child node by index
|
|
||||||
@property Node child(int index) { return null; }
|
|
||||||
/// returns first child node
|
|
||||||
@property Node firstChild() { return null; }
|
|
||||||
/// returns last child node
|
|
||||||
@property Node lastChild() { return null; }
|
|
||||||
|
|
||||||
/// find child node, return its index if found, -1 if not found or not child of this node
|
|
||||||
int childIndex(Node child) { return -1; }
|
|
||||||
/// return node index in parent's child node collection, -1 if not found
|
|
||||||
@property int index() { return _parent ? _parent.childIndex(this) : -1; }
|
|
||||||
|
|
||||||
/// returns child node by index and optionally compares its tag id, returns null if child with this index is not an element or id does not match
|
|
||||||
Element childElement(int index, elem_id id = 0) {
|
|
||||||
Element res = cast(Element)child(index);
|
|
||||||
if (res && (id == 0 || res.id == id))
|
|
||||||
return res;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// append text child
|
|
||||||
Node appendText(dstring s, int index = -1) { assert(false); }
|
|
||||||
/// append element child - by namespace and tag names
|
|
||||||
Node appendElement(string ns, string tag, int index = -1) { return appendElement(_document.nsId(ns), _document.tagId(tag), index); }
|
|
||||||
/// append element child - by namespace and tag ids
|
|
||||||
Node appendElement(ns_id ns, elem_id tag, int index = -1) { assert(false); }
|
|
||||||
|
|
||||||
// Text methods
|
|
||||||
|
|
||||||
/// node text
|
|
||||||
@property dstring text() { return null; }
|
|
||||||
/// ditto
|
|
||||||
@property void text(dstring s) { }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Text node
|
|
||||||
class Text : Node {
|
|
||||||
private:
|
|
||||||
dstring _text;
|
|
||||||
this(Document doc, dstring text = null) {
|
|
||||||
_document = doc;
|
|
||||||
_text = text;
|
|
||||||
}
|
|
||||||
public:
|
|
||||||
/// node text
|
|
||||||
override @property dstring text() { return _text; }
|
|
||||||
/// ditto
|
|
||||||
override @property void text(dstring s) { _text = s; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Element node
|
|
||||||
class Element : Node {
|
|
||||||
private:
|
|
||||||
Collection!Node _children;
|
|
||||||
Collection!Attribute _attrs;
|
|
||||||
elem_id _id; // element tag id
|
|
||||||
ns_id _ns; // element namespace id
|
|
||||||
|
|
||||||
this(Document doc, ns_id ns, elem_id id) {
|
|
||||||
_document = doc;
|
|
||||||
_ns = ns;
|
|
||||||
_id = id;
|
|
||||||
}
|
|
||||||
public:
|
|
||||||
|
|
||||||
/// return element tag id
|
|
||||||
override @property elem_id id() { return _id; }
|
|
||||||
/// return element namespace id
|
|
||||||
override @property ns_id nsid() { return _ns; }
|
|
||||||
|
|
||||||
// Attributes
|
|
||||||
|
|
||||||
/// returns attribute count
|
|
||||||
override @property int attrCount() { return cast(int)_attrs.length; }
|
|
||||||
|
|
||||||
/// get attribute by index
|
|
||||||
override Attribute attr(int index) { return index >= 0 && index < _attrs.length ? _attrs[index] : null; }
|
|
||||||
/// get attribute by namespace and attribute ids
|
|
||||||
override Attribute attr(ns_id nsid, attr_id attrid) {
|
|
||||||
foreach (a; _attrs)
|
|
||||||
if ((nsid == Ns.any || nsid == a.nsid) && attrid == a.id)
|
|
||||||
return a;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
/// get attribute by namespace and attribute names
|
|
||||||
override Attribute attr(string nsname, string attrname) { return attr(_document.nsId(nsname), _document.attrId(attrname)); }
|
|
||||||
|
|
||||||
/// set attribute value by namespace and attribute ids
|
|
||||||
override Attribute setAttr(ns_id nsid, attr_id attrid, string value) {
|
|
||||||
Attribute a = attr(nsid, attrid);
|
|
||||||
if (!a) {
|
|
||||||
a = new Attribute(this, nsid, attrid, value);
|
|
||||||
_attrs.add(a);
|
|
||||||
} else {
|
|
||||||
a.value = value;
|
|
||||||
}
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
/// set attribute value by namespace and attribute names
|
|
||||||
override Attribute setAttr(string nsname, string attrname, string value) { return setAttr(_document.nsId(nsname), _document.attrId(attrname), value); }
|
|
||||||
/// get attribute value by namespace and attribute ids
|
|
||||||
override string attrValue(ns_id nsid, attr_id attrid) {
|
|
||||||
if (Attribute a = attr(nsid, attrid))
|
|
||||||
return a.value;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
/// get attribute value by namespace and attribute ids
|
|
||||||
override string attrValue(string nsname, string attrname) { return attrValue(_document.nsId(nsname), _document.attrId(attrname)); }
|
|
||||||
|
|
||||||
// child nodes
|
|
||||||
|
|
||||||
/// returns child node count
|
|
||||||
override @property int childCount() { return cast(int)_children.length; }
|
|
||||||
/// returns child node by index
|
|
||||||
override @property Node child(int index) { return index >= 0 && index < _children.length ? _children[index] : null; }
|
|
||||||
/// returns first child node
|
|
||||||
override @property Node firstChild() { return _children.length > 0 ? _children[0] : null; }
|
|
||||||
/// returns last child node
|
|
||||||
override @property Node lastChild() { return _children.length > 0 ? _children[_children.length - 1] : null; }
|
|
||||||
/// find child node, return its index if found, -1 if not found or not child of this node
|
|
||||||
override int childIndex(Node child) {
|
|
||||||
for (int i = 0; i < _children.length; i++)
|
|
||||||
if (child is _children[i])
|
|
||||||
return i;
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// append text child
|
|
||||||
override Node appendText(dstring s, int index = -1) {
|
|
||||||
Node item = document.createText(s);
|
|
||||||
_children.add(item, index >= 0 ? index : size_t.max);
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
/// append element child - by namespace and tag ids
|
|
||||||
override Node appendElement(ns_id ns, elem_id tag, int index = -1) {
|
|
||||||
Node item = document.createElement(ns, tag);
|
|
||||||
_children.add(item, index >= 0 ? index : size_t.max);
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
/// append element child - by namespace and tag names
|
|
||||||
override Node appendElement(string ns, string tag, int index = -1) { return appendElement(_document.nsId(ns), _document.tagId(tag), index); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Document node
|
|
||||||
class Document : Element {
|
|
||||||
public:
|
|
||||||
this() {
|
|
||||||
super(null, 0, 0);
|
|
||||||
_elemIds.initialize!Tag();
|
|
||||||
_attrIds.initialize!Attr();
|
|
||||||
_nsIds.initialize!Ns();
|
|
||||||
_document = this;
|
|
||||||
}
|
|
||||||
/// create text node
|
|
||||||
Text createText(dstring text) {
|
|
||||||
return new Text(this, text);
|
|
||||||
}
|
|
||||||
/// create element node by namespace and tag ids
|
|
||||||
Element createElement(ns_id ns, elem_id tag) {
|
|
||||||
return new Element(this, ns, tag);
|
|
||||||
}
|
|
||||||
/// create element node by namespace and tag names
|
|
||||||
Element createElement(string ns, string tag) {
|
|
||||||
return new Element(this, nsId(ns), tagId(tag));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ids
|
|
||||||
|
|
||||||
/// return name for element tag id
|
|
||||||
string tagName(elem_id id) {
|
|
||||||
return _elemIds[id];
|
|
||||||
}
|
|
||||||
/// return name for namespace id
|
|
||||||
string nsName(ns_id id) {
|
|
||||||
return _nsIds[id];
|
|
||||||
}
|
|
||||||
/// return name for attribute id
|
|
||||||
string attrName(ns_id id) {
|
|
||||||
return _attrIds[id];
|
|
||||||
}
|
|
||||||
/// get id for element tag name
|
|
||||||
elem_id tagId(string s) {
|
|
||||||
if (s.empty)
|
|
||||||
return 0;
|
|
||||||
return _elemIds.intern(s);
|
|
||||||
}
|
|
||||||
/// get id for namespace name
|
|
||||||
ns_id nsId(string s) {
|
|
||||||
if (s.empty)
|
|
||||||
return 0;
|
|
||||||
return _nsIds.intern(s);
|
|
||||||
}
|
|
||||||
/// get id for attribute name
|
|
||||||
attr_id attrId(string s) {
|
|
||||||
if (s.empty)
|
|
||||||
return 0;
|
|
||||||
return _attrIds.intern(s);
|
|
||||||
}
|
|
||||||
private:
|
|
||||||
IdentMap!(elem_id) _elemIds;
|
|
||||||
IdentMap!(attr_id) _attrIds;
|
|
||||||
IdentMap!(ns_id) _nsIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
class Attribute {
|
|
||||||
private:
|
|
||||||
attr_id _id;
|
|
||||||
ns_id _nsid;
|
|
||||||
string _value;
|
|
||||||
Node _parent;
|
|
||||||
this(Node parent, ns_id nsid, attr_id id, string value) {
|
|
||||||
_parent = parent;
|
|
||||||
_nsid = nsid;
|
|
||||||
_id = id;
|
|
||||||
_value = value;
|
|
||||||
}
|
|
||||||
public:
|
|
||||||
/// Parent element which owns this attribute
|
|
||||||
@property Node parent() { return _parent; }
|
|
||||||
/// Parent element document
|
|
||||||
@property Document document() { return _parent.document; }
|
|
||||||
|
|
||||||
/// get attribute id
|
|
||||||
@property attr_id id() { return _id; }
|
|
||||||
/// get attribute namespace id
|
|
||||||
@property ns_id nsid() { return _nsid; }
|
|
||||||
/// get attribute tag name
|
|
||||||
@property string name() { return document.tagName(_id); }
|
|
||||||
/// get attribute namespace name
|
|
||||||
@property string nsname() { return document.nsName(_nsid); }
|
|
||||||
|
|
||||||
/// get attribute value
|
|
||||||
@property string value() { return _value; }
|
|
||||||
/// set attribute value
|
|
||||||
@property void value(string s) { _value = s; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// remove trailing _ from string, e.g. "body_" -> "body"
|
|
||||||
private string removeTrailingUnderscore(string s) {
|
|
||||||
if (s.endsWith("_"))
|
|
||||||
return s[0..$-1];
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// String identifier to Id map - for interning strings
|
|
||||||
struct IdentMap(ident_t) {
|
|
||||||
/// initialize with elements of enum
|
|
||||||
void initialize(E)() if (is(E == enum)) {
|
|
||||||
foreach(member; EnumMembers!E) {
|
|
||||||
static if (member.to!int > 0) {
|
|
||||||
//pragma(msg, "interning string '" ~ removeTrailingUnderscore(member.to!string) ~ "' for " ~ E.stringof);
|
|
||||||
intern(removeTrailingUnderscore(member.to!string), member);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// intern string - return ID assigned for it
|
|
||||||
ident_t intern(string s, ident_t id = 0) {
|
|
||||||
if (auto p = s in _stringToId)
|
|
||||||
return *p;
|
|
||||||
ident_t res;
|
|
||||||
if (id > 0) {
|
|
||||||
if (_nextId <= id)
|
|
||||||
_nextId = cast(ident_t)(id + 1);
|
|
||||||
res = id;
|
|
||||||
} else {
|
|
||||||
res = _nextId++;
|
|
||||||
}
|
|
||||||
_idToString[res] = s;
|
|
||||||
_stringToId[s] = res;
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
/// lookup id for string, return 0 if string is not found
|
|
||||||
ident_t opIndex(string s) {
|
|
||||||
if (s.empty)
|
|
||||||
return 0;
|
|
||||||
if (auto p = s in _stringToId)
|
|
||||||
return *p;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
/// lookup name for id, return null if not found
|
|
||||||
string opIndex(ident_t id) {
|
|
||||||
if (!id)
|
|
||||||
return null;
|
|
||||||
if (auto p = id in _idToString)
|
|
||||||
return *p;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
private:
|
|
||||||
string[ident_t] _idToString;
|
|
||||||
ident_t[string] _stringToId;
|
|
||||||
ident_t _nextId = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// standard tags
|
|
||||||
enum Tag : elem_id {
|
|
||||||
none,
|
|
||||||
body_,
|
|
||||||
pre,
|
|
||||||
div,
|
|
||||||
span
|
|
||||||
}
|
|
||||||
|
|
||||||
/// standard attributes
|
|
||||||
enum Attr : attr_id {
|
|
||||||
none,
|
|
||||||
id,
|
|
||||||
class_,
|
|
||||||
style
|
|
||||||
}
|
|
||||||
|
|
||||||
/// standard namespaces
|
|
||||||
enum Ns : ns_id {
|
|
||||||
any = -1,
|
|
||||||
none = 0,
|
|
||||||
xmlns,
|
|
||||||
xs,
|
|
||||||
xlink,
|
|
||||||
l,
|
|
||||||
xsi
|
|
||||||
}
|
|
||||||
|
|
||||||
unittest {
|
|
||||||
import std.algorithm : equal;
|
|
||||||
//import std.stdio;
|
|
||||||
IdentMap!(elem_id) map;
|
|
||||||
map.initialize!Tag();
|
|
||||||
//writeln("running DOM unit test");
|
|
||||||
assert(map["pre"] == Tag.pre);
|
|
||||||
assert(map["body"] == Tag.body_);
|
|
||||||
assert(map[Tag.div].equal("div"));
|
|
||||||
|
|
||||||
Document doc = new Document();
|
|
||||||
auto body_ = doc.appendElement(null, "body");
|
|
||||||
assert(body_.id == Tag.body_);
|
|
||||||
assert(body_.name.equal("body"));
|
|
||||||
auto div = body_.appendElement(null, "div");
|
|
||||||
assert(body_.childCount == 1);
|
|
||||||
assert(div.id == Tag.div);
|
|
||||||
assert(div.name.equal("div"));
|
|
||||||
auto t1 = div.appendText("Some text"d);
|
|
||||||
assert(div.childCount == 1);
|
|
||||||
assert(div.child(0).text.equal("Some text"d));
|
|
||||||
auto t2 = div.appendText("Some more text"d);
|
|
||||||
assert(div.childCount == 2);
|
|
||||||
assert(div.childIndex(t1) == 0);
|
|
||||||
assert(div.childIndex(t2) == 1);
|
|
||||||
|
|
||||||
div.setAttr(Ns.none, Attr.id, "div_id");
|
|
||||||
assert(div.attrValue(Ns.none, Attr.id).equal("div_id"));
|
|
||||||
|
|
||||||
destroy(doc);
|
|
||||||
}
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,72 +0,0 @@
|
||||||
module dom.encoding;
|
|
||||||
|
|
||||||
string findCharsetDirective(ubyte[] src) {
|
|
||||||
import std.string;
|
|
||||||
import std.algorithm : min;
|
|
||||||
string encoding = null;
|
|
||||||
if (src.length >= 17) {
|
|
||||||
auto head = cast(string)src[0 .. min(1024, src.length)];
|
|
||||||
auto encPos = head.indexOf(`@charset "`);
|
|
||||||
if (encPos >= 0) {
|
|
||||||
head = head[10 .. $];
|
|
||||||
auto endPos = head.indexOf('"');
|
|
||||||
if (endPos > 0) {
|
|
||||||
head = head[0 .. endPos];
|
|
||||||
bool valid = true;
|
|
||||||
ubyte v = 0;
|
|
||||||
foreach(ch; head)
|
|
||||||
v |= ch;
|
|
||||||
if (v & 0x80) {
|
|
||||||
// only code points 0..127
|
|
||||||
// found valid @charset directive
|
|
||||||
return cast(string)head.dup;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null; // not found
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Convert CSS code bytes to utf-8.
|
|
||||||
src is source byte stream
|
|
||||||
baseEncoding is name of HTTP stream encoding or base document encoding.
|
|
||||||
*/
|
|
||||||
char[] bytesToUtf8(ubyte[] src, string streamEncoding = null, string environmentEncoding = null) {
|
|
||||||
import std.string;
|
|
||||||
import std.algorithm : min;
|
|
||||||
bool isUtf8 = false;
|
|
||||||
string encoding = null;
|
|
||||||
if (streamEncoding) {
|
|
||||||
encoding = streamEncoding;
|
|
||||||
} else {
|
|
||||||
string charsetDirectiveEncoding = findCharsetDirective(src);
|
|
||||||
if (charsetDirectiveEncoding) {
|
|
||||||
encoding = charsetDirectiveEncoding;
|
|
||||||
if (charsetDirectiveEncoding[0] == 'u' && charsetDirectiveEncoding[1] == 't' && charsetDirectiveEncoding[2] == 'f' && charsetDirectiveEncoding[3] == '-') {
|
|
||||||
isUtf8 = true; // for utf-16be, utf-16le use utf-8
|
|
||||||
encoding = "utf-8";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!encoding && environmentEncoding)
|
|
||||||
encoding = environmentEncoding;
|
|
||||||
if (!encoding) {
|
|
||||||
// check bom
|
|
||||||
// utf-8 BOM
|
|
||||||
if (src.length > 3 && src[0] == 0xEF && src[1] == 0xBB && src[2] == 0xBF) {
|
|
||||||
isUtf8 = true;
|
|
||||||
encoding = "utf-8";
|
|
||||||
src = src[3 .. $];
|
|
||||||
} else {
|
|
||||||
// TODO: support other UTF-8 BOMs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isUtf8) {
|
|
||||||
// no decoding needed
|
|
||||||
return cast(char[])src.dup;
|
|
||||||
}
|
|
||||||
// TODO: support more encodings
|
|
||||||
// unknown encoding
|
|
||||||
return null;
|
|
||||||
}
|
|
Loading…
Reference in New Issue