CSS Import (.css)

Kevin Daily
User Interface Developer
Wolfram Research

ABSTRACT

The cascading style sheet (CSS) is the standard for styling HTML documents. Wolfram Desktop notebooks use a similar but distinct style sheet system. This talk introduces a robust importer for CSS documents to validate and translate CSS declarations into Wolfram Language options. CSS can optionally be imported as uninterpreted data or directly as a notebook style sheet. Strategies for conversion are discussed.

Goals of the project

No hacks!

Follow the CSS specifications

Image: https://commons.wikimedia.org/wiki/File:CSS3_taxonomy_and_status_by_Sergey_Mavrody.svg

Summary of functions

Import

Import[..., “CSS”] - import a CSS file into a structured Dataset expression

ExtractCSSFromXML - import detected CSS from within symbolic XML

Selector language

CSSSelector - construct a CSS selector object

CSSTargets - get positions of XMLElement expressions found by a CSS selector

Converting CSS to WL

CSSCascade - perform the CSS cascade algorithm and convert result to WL options

CSSInheritance - perform CSS inheritance from ancestor XML elements

Comparison of style rule formats

Wolfram Language

Cell[StyleData["Section"],FontSize->14,FontColor->RGBColor[0,0,1]]

CSS

.section {font-size:14pt; color:blue}

Comparison of style rule formats

Wolfram Language

Option name

Cell[StyleData["Section"],FontSize->14,FontColor->RGBColor[0,0,1]]

CSS

Property

.section {font-size:14pt; color:blue}

Comparison of style rule formats

Wolfram Language

Option value

Cell[StyleData["Section"],FontSize->14,FontColor->RGBColor[0,0,1]]

CSS

Value

.section {font-size:14pt; color:blue}

Comparison of style rule formats

Wolfram Language

Option

Cell[StyleData["Section"],FontSize->14,FontColor->RGBColor[0,0,1]]

CSS

Declaration

.section {font-size:14pt; color:blue}

Comparison of style rule formats

Wolfram Language

Options (option sequence)

Cell[StyleData["Section"],FontSize->14,FontColor->RGBColor[0,0,1]]

CSS

Declaration block

.section {font-size:14pt; color:blue}

Comparison of style rule formats

Wolfram Language

Style name

Cell[StyleData["Section"],FontSize->14,FontColor->RGBColor[0,0,1]]

CSS

Selector

.section {font-size:14pt; color:blue}

Comparison of style rule formats

Wolfram Language

Style data cell

Cell[StyleData["Section"],FontSize->14,FontColor->RGBColor[0,0,1]]

CSS

Rule

.section {font-size:14pt; color:blue}

Imported CSS structure

PacletDirectoryAdd[NotebookDirectory[]];
Needs["CSSTools`"]

cssData=ImportString["
  h1 {
    font-size:14pt; 
    color:blue}
  h2 {font-family:arial}", 
"CSS"]

How are selectors represented?

sel=(RowDefault["#CSS ", "h1"]);
sel//InputForm

You can access its properties directly:

sel["Specificity"]

Imported CSS structure: parsing

Selectors

The head CSSSelector also acts as an object constructor if given a string argument. The selector parser detects selector syntax errors.

CSSSelector["h1"]

CSSSelector["bad - combinator"]

CSSSelector["badAttribute [att!=b]"]

CSSSelector["badnamespace|type"]

Rules

Unparsable properties are skipped:

ImportString["h1 {unknown}","CSS"]

An unrecognized property is not an error:

ImportString["h1 {unknown:value}","CSS"]

Validation can pinpoint the reason for the error:

ImportString["h1 {font-size:14}","CSS"]

Using interpreted CSS

cssData

Perform the CSS cascade algorithm to combine the options into a set.

opts=CSSCascade[
All  (* CSS property e.g. {"font-size"} or "color" *),
Cell(* WL scope e.g. Box|Cell|Notebook *),
cssData,
All  (* CSS selectors e.g. {"h1"} or CSSSelector["h1"]*)]

Immediately use the determined options:

Style["Foo",Sequence@@opts]

More on the CSS cascade

cssData2=ImportString["
.h {font-size:30px !important}
.header {font-size:20px} 
.header2 {font-size:17px!important} 
h1 {font-size:10px}","CSS"]

Specificity

".header" and "h1" have the same importance but ".header" has a higher specificity.

CSSCascade["font-size",Cell,cssData2,{".header","h1"}]

Importance

".header" and ".h" have the same specificity, but ".h" has a higher importance.

CSSCascade["font-size",Cell,cssData2,{".h",".header"}]

Order

".h" and ".header2" have the same specificity and importance, but ".header2" comes last.

CSSCascade["font-size",Cell,cssData2,{".h",".header2"}]

Importing from mulitple external files

exampleDir=FileNameJoin[{Lookup[PacletInformation["CSSTools"],"Location"],"Examples"}];

From a file or multiple files:

FilePrint[FileNameJoin[{exampleDir,"mystyle1.css"}]]

FilePrint[FileNameJoin[{exampleDir,"mystyle2.css"}]]

Block[{$Path=Prepend[$Path,exampleDir]},Import[FileNameJoin[{exampleDir,"mystyle1.css"}],"CSS"]]

CSS embedded in XML

XML can be imported as symbolic XML. Any embedded CSS can be extracted from the symbolic XML.

html=FileNameJoin[{exampleDir,"index.html"}];

FilePrint[html]

doc=Import[html,"XML"];

cssDocData=ExtractCSSFromXML[doc,"RootDirectory"->exampleDir];
cssDocData[[;;3]]

Using interpreted CSS from XML extraction

An XML document (symbolic XML) has a structure where XMLElement instances can inherit styles from ancestor elements.

cssDocData[[{2,3}]]

CSSInheritance looks at a symbolic XML position and pulls in any inheritable properties. CSSCascade would miss this inheritance.

CSSCascade[All,Cell,cssDocData,"h1"]

CSSInheritance[{2,3,2,3,1},Cell,cssDocData]

Other import formats

Directly to a WL stylesheet

ImportString["h1 {font-size:14pt; color:blue}", {"CSS", "Stylesheet"}]

Only raw CSS structure (no interpretation)

ImportString["h1 {font-size:14pt; color:blue}", {"CSS", "RawData"}]

Future development

More properties e.g. vendor-specific, SVG, etc.

More CSS Level 3 modules...

Bonus workflow 1: getting web properties directly

Start a web session:

web=StartWebSession[];

WebExecute[web,{"OpenPage"->"https://wolfram.com"}]

Get an element of interest

elem = WebExecute[web,"LocateElements"->"Tag"->"p"][[1]];

Use JavaScript code to extract its computed styles i.e. the styles after performing the cascade and inheritance.

code = "
let para = document.querySelector('p');
let compStyles = window.getComputedStyle(para);
return compStyles;";

code1 = "
let para = document.querySelector('p');
let compStyles = window.getComputedStyle(para);
let len = compStyles.length;
var result = [];
for( i=0; i<len; i++ ) {
result[i] = compStyles.getPropertyValue(compStyles[i]);
};
return result;";

The JavaScript code is run via WebExecute.

allComputed=ImportString["dummy{"<>StringRiffle[StringRiffle[#,":"]&/@Transpose[{WebExecute["JavascriptExecute"->code],WebExecute["JavascriptExecute"->code1]}],";"]<>"}","CSS"];

allComputed[[1,"Block"]]//Length

available=Select[allComputed[[1,"Block"]],!FailureQ[#Interpretation]&];

Length@available

interpreted=CSSCascade[{"font","color","margin","padding","border","background"},Cell,allComputed,All]

Close the web session when you’re done:

DeleteObject[web]

Bonus workflow 2: working with symbolic XML

A helper function to create StyleData cells of a particular type from the CSS example data.

styleDataCell[name_,type_,pos_]:=Cell[StyleData[name],Sequence@@CSSInheritance[pos,type,cssDocData]]

A style data cell at the Notebook scope:

body=styleDataCell["Notebook",Notebook,CSSTargets[doc,"body"][[1,1]]]

A sequence of style data cells at the Cell scope.

h3=styleDataCell["question",Cell,CSSTargets[doc,"h3"][[1,1]]];
h1=styleDataCell["h1",Cell,CSSTargets[doc,"h1"][[1,1]]];
li=styleDataCell["li",Cell,CSSTargets[doc,"li"][[1,1]]];
p=styleDataCell["p",Cell,CSSTargets[doc,"p"][[1,1]]];
h2=styleDataCell["h2",Cell,CSSTargets[doc,"h2"][[1,1]]];

A special case: the source XML contains an element "h3" with an inline style, but is otherwise identical to other "h3" elements. Since we already created a StyleData["question"] cell with these properties, we exclude the duplicates.

h3special= Cell[StyleData["questionSpecial", StyleDefinitions->StyleData["question"]],Sequence@@Complement[
CSSInheritance[CSSTargets[doc,"h3"][[1,-1]],Cell,cssDocData],
CSSInheritance[CSSTargets[doc,"h3"][[1,1]],Cell,cssDocData]]];

Create the custom stylesheet to use later.

customStylesheet=NotebookPut@Notebook[{Cell[StyleData[StyleDefinitions->"Default.nb"]],body,h1,h2,h3,h3special,p,li},StyleDefinitions->"PrivateStylesheetFormatting.nb"];

NotebookSave[customStylesheet,FileNameJoin[{exampleDir,"CustomStylesheet.nb"}]]

Compare this stylesheet against a web browser’s rendering of the same content:

SystemOpen[FileNameJoin[{exampleDir,"index.html"}]]

SetOptions[
NotebookOpen[FileNameJoin[{exampleDir,"BareNotebook.nb"}]],
StyleDefinitions->FileNameJoin[{exampleDir,"CustomStylesheet.nb"}]]