|
@@ -0,0 +1,318 @@
|
|
|
+unit Fresnel.CSSStyleInspector;
|
|
|
+
|
|
|
+{$mode ObjFPC}{$H+}
|
|
|
+
|
|
|
+interface
|
|
|
+
|
|
|
+uses
|
|
|
+ Classes, SysUtils, fpCSSTree, fpCSSResParser, fpCSSResolver, Fresnel.DOM, Fresnel.Classes,
|
|
|
+ Fresnel.Controls, Fresnel.Events, FCL.Events;
|
|
|
+
|
|
|
+type
|
|
|
+
|
|
|
+ { TCSSStyleInspector }
|
|
|
+
|
|
|
+ TCSSStyleInspector = class(TDiv)
|
|
|
+ private
|
|
|
+ FRulesDiv: TDiv;
|
|
|
+ FLock: integer;
|
|
|
+ FInspectNeeded: boolean;
|
|
|
+ FTarget: TFresnelElement;
|
|
|
+ procedure OnTargetDomChanged(Event: TAbstractEvent);
|
|
|
+ procedure SetTarget(const AValue: TFresnelElement);
|
|
|
+ protected
|
|
|
+ procedure Inspect;
|
|
|
+ procedure AddRule(Row: integer; Selectors, Origin: TCSSString; RuleEl: TCSSRuleElement);
|
|
|
+ procedure Notification(AComponent: TComponent; Operation: TOperation); override;
|
|
|
+ public
|
|
|
+ const
|
|
|
+ RulesDivClass = 'CSSStyleInspRules';
|
|
|
+ RuleDivClass = 'CSSStyleInspRule';
|
|
|
+ RuleHeaderDivClass = 'CSSStyleInspHeader';
|
|
|
+ RuleSelectorLabelClass = 'CSSStyleInspSelector';
|
|
|
+ RuleBracketLabelClass = 'CSSStyleInspRuleBracket';
|
|
|
+ RuleOriginLabelClass = 'CSSStyleInspOrigin';
|
|
|
+ RuleFooterDivClass = 'CSSStyleInspFooter';
|
|
|
+ public
|
|
|
+ constructor Create(AOwner: TComponent); override;
|
|
|
+ destructor Destroy; override;
|
|
|
+ class function GetCSSTypeStyle: TCSSString; override;
|
|
|
+ procedure BeginUpdate;
|
|
|
+ procedure EndUpdate;
|
|
|
+ property Target: TFresnelElement read FTarget write SetTarget;
|
|
|
+ property RulesDiv: TDiv read FRulesDiv;
|
|
|
+ end;
|
|
|
+
|
|
|
+implementation
|
|
|
+
|
|
|
+{ TCSSStyleInspector }
|
|
|
+
|
|
|
+procedure TCSSStyleInspector.SetTarget(const AValue: TFresnelElement);
|
|
|
+var
|
|
|
+ VP: TFresnelViewport;
|
|
|
+begin
|
|
|
+ if FTarget=AValue then Exit;
|
|
|
+ FTarget:=AValue;
|
|
|
+ if FTarget<>nil then
|
|
|
+ begin
|
|
|
+ if (FTarget=Self) or Contains(FTarget) then
|
|
|
+ raise EFresnel.Create('20250912162555');
|
|
|
+ FreeNotification(FTarget);
|
|
|
+
|
|
|
+ VP:=Target.Viewport;
|
|
|
+ if VP<>nil then
|
|
|
+ begin
|
|
|
+ VP.AddEventListener(evtViewportCSSApplied,@OnTargetDomChanged);
|
|
|
+ if VP.Rendered then
|
|
|
+ Inspect;
|
|
|
+ end;
|
|
|
+ end;
|
|
|
+end;
|
|
|
+
|
|
|
+procedure TCSSStyleInspector.OnTargetDomChanged(Event: TAbstractEvent);
|
|
|
+begin
|
|
|
+ writeln('TCSSStyleInspector.OnTargetDomChanged ',Event.Sender<>nil);
|
|
|
+ Inspect;
|
|
|
+end;
|
|
|
+
|
|
|
+procedure TCSSStyleInspector.Inspect;
|
|
|
+var
|
|
|
+ VP: TFresnelViewport;
|
|
|
+ Rules: TCSSSharedRuleList;
|
|
|
+ aTargetValues: TCSSAttributeValues;
|
|
|
+ aRule: TCSSSharedRule;
|
|
|
+ RuleEl: TCSSRuleElement;
|
|
|
+ i, j, Row: Integer;
|
|
|
+ aStyleSheet: TCSSResolver.TStyleSheet;
|
|
|
+ RuleOrigin, Selectors: TCSSString;
|
|
|
+ aSelector: TCSSElement;
|
|
|
+begin
|
|
|
+ if csDestroying in ComponentState then
|
|
|
+ exit;
|
|
|
+ if FLock>0 then
|
|
|
+ begin
|
|
|
+ FInspectNeeded:=true;
|
|
|
+ exit;
|
|
|
+ end;
|
|
|
+ FInspectNeeded:=false;
|
|
|
+
|
|
|
+ if (Target=nil) or (Target=Self) or Contains(Target) then
|
|
|
+ begin
|
|
|
+ // clear
|
|
|
+ exit;
|
|
|
+ end;
|
|
|
+
|
|
|
+ writeln('TCSSStyleInspector.Inspect ',Target.GetPath);
|
|
|
+
|
|
|
+ VP:=Target.Viewport;
|
|
|
+
|
|
|
+ aTargetValues:=TCSSAttributeValues.Create;
|
|
|
+ try
|
|
|
+ VP.Resolver.Compute(Target,Target.StyleElement,Rules,aTargetValues);
|
|
|
+
|
|
|
+ Row:=0;
|
|
|
+
|
|
|
+ // Element.Style aka inline style
|
|
|
+ AddRule(Row,'Element.Style','',Target.StyleElement);
|
|
|
+ inc(Row);
|
|
|
+
|
|
|
+ // CSS rules
|
|
|
+ for i:=length(Rules.Rules)-1 downto 0 do
|
|
|
+ begin
|
|
|
+ aRule:=Rules.Rules[i];
|
|
|
+ RuleEl:=aRule.Rule;
|
|
|
+
|
|
|
+ // find source
|
|
|
+ RuleOrigin:=RuleEl.SourceFileName;
|
|
|
+ aStyleSheet:=VP.Resolver.FindStyleSheetWithElement(RuleEl);
|
|
|
+ if aStyleSheet<>nil then
|
|
|
+ begin
|
|
|
+ if aStyleSheet.Origin=cssoUserAgent then begin
|
|
|
+ RuleOrigin:=aStyleSheet.Name; // usually the classname
|
|
|
+ end else if aStyleSheet.Origin=cssoAuthor then begin
|
|
|
+ // stylesheet of viewport aka form
|
|
|
+ RuleOrigin:=VP.Name;
|
|
|
+ end else begin
|
|
|
+ writeln('TCSSStyleInspector.Inspect unknown stylesheet ',aStyleSheet.Origin,' Name=',aStyleSheet.Name);
|
|
|
+ // todo: check application stylesheets
|
|
|
+ end;
|
|
|
+ end;
|
|
|
+ RuleOrigin+='('+IntToStr(RuleEl.SourceRow)+','+IntToStr(RuleEl.SourceCol)+')';
|
|
|
+
|
|
|
+ Selectors:='';
|
|
|
+ for j:=0 to RuleEl.SelectorCount-1 do
|
|
|
+ begin
|
|
|
+ aSelector:=RuleEl.Selectors[j];
|
|
|
+ if Selectors>'' then Selectors+=', ';
|
|
|
+ Selectors+=aSelector.AsFormattedString;
|
|
|
+ end;
|
|
|
+ writeln('TCSSStyleInspector.Inspect ',i,' Spec=',aRule.Specificity,' Selector="',Selectors,'" Src=',RuleOrigin);
|
|
|
+
|
|
|
+ AddRule(Row,Selectors,RuleOrigin,RuleEl);
|
|
|
+ inc(Row);
|
|
|
+ end;
|
|
|
+
|
|
|
+ // delete old nodes
|
|
|
+ while RulesDiv.NodeCount>Row do
|
|
|
+ RulesDiv.Nodes[RulesDiv.NodeCount-1].Free;
|
|
|
+ finally
|
|
|
+ aTargetValues.Free;
|
|
|
+ end;
|
|
|
+end;
|
|
|
+
|
|
|
+procedure TCSSStyleInspector.AddRule(Row: integer; Selectors, Origin: TCSSString;
|
|
|
+ RuleEl: TCSSRuleElement);
|
|
|
+var
|
|
|
+ i, j: Integer;
|
|
|
+ ChildEl, KeyEl: TCSSElement;
|
|
|
+ DeclEl: TCSSDeclarationElement;
|
|
|
+ ResolvedKeyEl: TCSSResolvedIdentifierElement;
|
|
|
+ Src: String;
|
|
|
+ CurDiv, HeaderDiv, FooterDiv: TDiv;
|
|
|
+ BracketCloseLabel, BracketOpenLabel, SelectorLabel, OriginLabel: TLabel;
|
|
|
+begin
|
|
|
+ writeln('TCSSStyleInspector.AddRule Row=',Row,' Selector="',Selectors,'" Origin="',Origin,'"');
|
|
|
+
|
|
|
+ if Row<RulesDiv.NodeCount then
|
|
|
+ begin
|
|
|
+ CurDiv:=RulesDiv.Nodes[Row] as TDiv;
|
|
|
+ HeaderDiv:=CurDiv.Nodes[0] as TDiv;
|
|
|
+ SelectorLabel:=HeaderDiv.Nodes[0] as TLabel;
|
|
|
+ SelectorLabel.Caption:=Selectors;
|
|
|
+ OriginLabel:=HeaderDiv.Nodes[2] as TLabel;
|
|
|
+ OriginLabel.Caption:=Origin;
|
|
|
+ end else begin
|
|
|
+ CurDiv:=TDiv.Create(Self);
|
|
|
+ CurDiv.CSSClasses.Text:=RuleDivClass;
|
|
|
+ CurDiv.Parent:=RulesDiv;
|
|
|
+
|
|
|
+ // header: "selector { origin"
|
|
|
+ HeaderDiv:=TDiv.Create(Self);
|
|
|
+ HeaderDiv.CSSClasses.Text:=RuleHeaderDivClass;
|
|
|
+ HeaderDiv.Parent:=CurDiv;
|
|
|
+
|
|
|
+ SelectorLabel:=TLabel.Create(Self);
|
|
|
+ SelectorLabel.CSSClasses.Text:=RuleBracketLabelClass;
|
|
|
+ SelectorLabel.Caption:=Selectors;
|
|
|
+ SelectorLabel.Parent:=HeaderDiv;
|
|
|
+
|
|
|
+ BracketOpenLabel:=TLabel.Create(Self);
|
|
|
+ BracketOpenLabel.CSSClasses.Text:=RuleBracketLabelClass;
|
|
|
+ BracketOpenLabel.Caption:='{';
|
|
|
+ BracketOpenLabel.Parent:=HeaderDiv;
|
|
|
+
|
|
|
+ OriginLabel:=TLabel.Create(Self);
|
|
|
+ OriginLabel.CSSClasses.Text:=RuleOriginLabelClass;
|
|
|
+ OriginLabel.Caption:=Origin;
|
|
|
+ OriginLabel.Parent:=HeaderDiv;
|
|
|
+
|
|
|
+ // footer: "}"
|
|
|
+ FooterDiv:=TDiv.Create(Self);
|
|
|
+ FooterDiv.CSSClasses.Text:=RuleHeaderDivClass;
|
|
|
+ FooterDiv.Parent:=CurDiv;
|
|
|
+
|
|
|
+ BracketCloseLabel:=TLabel.Create(Self);
|
|
|
+ BracketCloseLabel.CSSClasses.Text:=RuleBracketLabelClass;
|
|
|
+ BracketCloseLabel.Caption:='}';
|
|
|
+ BracketCloseLabel.Parent:=FooterDiv;
|
|
|
+ end;
|
|
|
+
|
|
|
+ // header: "selector { Origin"
|
|
|
+
|
|
|
+ if RuleEl<>nil then
|
|
|
+ begin
|
|
|
+ for i:=0 to RuleEl.ChildCount-1 do
|
|
|
+ begin
|
|
|
+ ChildEl:=RuleEl.Children[i];
|
|
|
+ //writeln(' Child ',i,' ',ChildEl.ClassName);
|
|
|
+ if ChildEl is TCSSDeclarationElement then
|
|
|
+ begin
|
|
|
+ DeclEl:=TCSSDeclarationElement(ChildEl);
|
|
|
+ Src:='';
|
|
|
+ if DeclEl.KeyCount<>1 then
|
|
|
+ begin
|
|
|
+ // todo: multi key declaration
|
|
|
+ continue;
|
|
|
+ end;
|
|
|
+ KeyEl:=DeclEl.Keys[0];
|
|
|
+ if KeyEl is TCSSResolvedIdentifierElement then
|
|
|
+ begin
|
|
|
+ ResolvedKeyEl:=TCSSResolvedIdentifierElement(KeyEl);
|
|
|
+ Src+=ResolvedKeyEl.Name;
|
|
|
+
|
|
|
+ Src+=' : ';
|
|
|
+ For j:=0 to DeclEl.ChildCount-1 do
|
|
|
+ begin
|
|
|
+ if j>0 then
|
|
|
+ Src+=', ';
|
|
|
+ Src+=DeclEl.Children[j].AsFormattedString;
|
|
|
+ end;
|
|
|
+ if DeclEl.IsImportant then
|
|
|
+ Src+=' !important';
|
|
|
+ if ResolvedKeyEl.NumericalID<=CSSIDNone then
|
|
|
+ begin
|
|
|
+ // unknown attribute
|
|
|
+ writeln('TCSSStyleInspector.AddRule Unknown attribute: ',i,' "',Src,'"');
|
|
|
+ end else begin
|
|
|
+ // todo: check for validity
|
|
|
+ // todo: check if already set
|
|
|
+ // todo: split shorthands
|
|
|
+ writeln('TCSSStyleInspector.AddRule Known: ',i,' "',Src,'"');
|
|
|
+ end;
|
|
|
+ end else begin
|
|
|
+ // todo: unknown declaration
|
|
|
+ writeln('TCSSStyleInspector.AddRule Invalid: ',i,' "',DeclEl.AsFormattedString,'"');
|
|
|
+ end;
|
|
|
+ // todo: !important
|
|
|
+ end;
|
|
|
+ end;
|
|
|
+ end;
|
|
|
+end;
|
|
|
+
|
|
|
+procedure TCSSStyleInspector.Notification(AComponent: TComponent; Operation: TOperation);
|
|
|
+begin
|
|
|
+ inherited Notification(AComponent, Operation);
|
|
|
+ if Operation=opRemove then
|
|
|
+ begin
|
|
|
+ if Target=AComponent then
|
|
|
+ Target:=nil;
|
|
|
+ end;
|
|
|
+end;
|
|
|
+
|
|
|
+constructor TCSSStyleInspector.Create(AOwner: TComponent);
|
|
|
+begin
|
|
|
+ inherited Create(AOwner);
|
|
|
+
|
|
|
+ FRulesDiv:=TDiv.Create(Self);
|
|
|
+ with RulesDiv do begin
|
|
|
+ CSSClasses.Text:=RulesDivClass;
|
|
|
+ Parent:=Self;
|
|
|
+ end;
|
|
|
+end;
|
|
|
+
|
|
|
+destructor TCSSStyleInspector.Destroy;
|
|
|
+begin
|
|
|
+ inherited Destroy;
|
|
|
+end;
|
|
|
+
|
|
|
+class function TCSSStyleInspector.GetCSSTypeStyle: TCSSString;
|
|
|
+begin
|
|
|
+ Result:='.'+RulesDivClass+' { }';
|
|
|
+end;
|
|
|
+
|
|
|
+procedure TCSSStyleInspector.BeginUpdate;
|
|
|
+begin
|
|
|
+ inc(FLock);
|
|
|
+end;
|
|
|
+
|
|
|
+procedure TCSSStyleInspector.EndUpdate;
|
|
|
+begin
|
|
|
+ if FLock=0 then
|
|
|
+ raise EFresnel.Create('20250912163434');
|
|
|
+ dec(FLock);
|
|
|
+ if FLock=0 then
|
|
|
+ Inspect;
|
|
|
+end;
|
|
|
+
|
|
|
+end.
|
|
|
+
|