08 Mar 2014
Background
Pattern-matching mixins is the distinguishing characteristic that separates Less from other CSS preprocessors like Stylus and Sass.
Let the compiler do what it’s good at: match templates and error early (when one is missing).
LESS is a beautiful language (like CSS) precisely because it is not Turing-complete.
Instead, it explores just how far you can take a language with those restrictions.
For example, you can provide conditionals using the pattern-matching when
keyword instead of relying explicit control flow like if then else
. You can match arguments to mixins either by value or with ...
(like Ruby, CoffeeScript, and recent versions of Java).
This frees the author up to put conditional styles elsewhere in a file (much like XSLT templates). Similarly, mixins whose arguments match are always applied. This can initially be an annoying feature but for styling textbooks this is central in our design.
Case Study: Making Textbooks
In textbooks we need to style elements differently depending on where they are (context is crucial).
A subfigure (figure > figure
) should render differently than a figure
.
A table should be numbered differently when it is in a chapter (ie Table 4.3
) than when it is in an appendix (Table A3
).
Or take problems and solutions: a problem and solution at the end of a chapter should be “treated” differently than an example with a worked out problem and solution. The former should be numbered (and solution should be moved to the back of a textbook) while the latter should be included in the example (since it is a worked-out example).
Or take a definition of a term. If an equation is used then it should not be numbered; referring to an equation inside a definition does not make a whole lot of sense.
In each of these cases it is the context that is important when deciding how to style an element.
Additionally, the HTML elements differ for different outputs; online the CSS selectors are different than for a single-page book or a multi-page EUPB.
Solution: Slots and Skeletons
To handle multiple HTML formats (EPUB, online, PDF) we came up with the idea of separating the CSS selectors (skeleton) from the rules (slots).
Each book contains a very similar HTML structure (depending on the output format; PDF, EPUB, online) but very different styling (fonts, colors, numbering schemes) so we split the CSS into 2 types of files: slots for the styling and skeletons for the HTML structure and linked them together using a namespaced tree of mixins that represent the logical structure of a book.
In this way a page header would be defined as (the slot):
#page>#top>.outside() { content: 'Chapter 1'; } and used as (the _skeleton_):
@page:left { @top-left: #page>#top>.outside(); }
This allows us to separate the styling (slots) from the content (skeleton).
Context
Getting back to pattern matching; the context of an element (ie “Exercise”) is important for styling that element.
To accomplish this, we created rules for defining mixins and special “generators” for all the possible permutations of contexts.
In our system a mixin has roughly the following signature: #logical-type>.mixin(@kind; @part; @contexts...) { }
.
Let’s go through the various parts:
#logical-type
roughly corresponds to the structural element (and data-type
attribute). Some examples are exercise
, note
, term
, example
.mixin(...)
is the kind of slot (.style(...)
for the whole element, .title(...)
for the title, .numbering(...)
for how to number the element, etc)
@kind
corresponds to a specific class for a particular book (a science book may have experiment
while a history book may have did-you-know
)
@part
represents which part of a book the element occurs in (preface
, chapter
, appendix
, any
) and is largely used for numbering an element
@contexts...
represents what this logical-type
occurs inside (and where all the fun happens). Some examples would be #figure>.style(any; any; figure) { }
(a subfigure), or #table>.style(any; any; glossary)
(a table inside a glossary)
This gives us the flexibility to style the content of a book based on the logical parts (using namespaces) and the context it occurs in (using @contexts...
) and have it work for different output formats (by having different skeleton files).
To accomplish the @contexts...
bit we created some mixins that generate all permutations of different contexts. This is accomplished by adding list-expansion to LESS which allows us to expand a list when calling a mixin. For example:
.context-expander(@head; @tail...) {
.mixin-call(@tail...); // <-- The new piece added to LESS
}
Note: For most of these permutations, no mixins will match so they will be omitted from the generated CSS.
The result is a single skeleton file for each output format (EPUB, PDF, online) and a slots file (~100 lines) for each book.
Keep Reading...
07 Mar 2014
“Why CSS instead of Javascript?””
Wouldn’t it be great if authors could customize their books without having them write (or run) arbitrary JavaScript? This post shows a way to do it.
Our Problem
Our books end up being published in various formats with various support for CSS.
We use Docbook for PDFs partly because we need to move content around (ie collating exercises at the end of a chapter, making an index) and XSLT provides a way to move XML around.
Unfortunately, this means 4 things:
- developers need to learn XSLT
- we must regression-test all of our books whenever we fix a bug or add a feature
- CSS for the PDF is different for ePUB and online
- numbering things like exercises is different in a PDF than online
Fortunately, there are a few W3C Drafts that help fill in some of the gaps: Generated Content for Paged Media and CSS3 Generated and Replaced Content Module.
Intersection of Some CSS Features:
Feature |
EPUB2 |
Browsers |
PrinceXML (PDF) |
::before |
no |
yes |
yes |
counter-increment: |
no |
yes |
yes |
content: |
no |
partial |
yes |
target-text() |
no |
no |
yes |
page-break-*: |
no |
no |
yes |
move-to: |
no |
no |
no |
::outside::before |
no |
no |
no |
:has() |
no |
no |
no |
To replace Docbook and have one CSS file to style the various formats we need to support all of these features and note a few differences:
- PDF is generated using a single large HTML file (CSS needs to operate on all chapters)
- ePUB needs to be chunked into multiple HTML files (ideally using CSS
page-break-*
)
- Online, a single HTML file can be viewed outside the context of a book
Browsers
Ideally, we would be able to get access to all of these unsupported selectors and rules using the CSS Document Object Model but browsers only expose the selectors and rules they understand.
The Solution
Enter CSS-Polyfills.
The project uses LessCSS and jQuery to parse the CSS file and “bake” the changes into the HTML.
With it you can do things that are not possible using CSS supported by browsers. For example, you can style an element based on children inside:
.note:has(> .title) { /* Give it a fancier border */ }
Or, you can automatically generate a glossary at the and of a chapter based on terms in the chapter:
.term > .definition { move-to: glossary-area; }
.chapter:after {
content: pending(glossary-area);
}
You can even style links depending on the target:
a[href] {
// Use x-target-is as a switch for which link text to use
content: x-target-is(attr(href), 'figure') 'See Figure';
content: x-target-is(attr(href), 'table') 'See Table';
content: x-target-is(attr(href), '.example') 'See Example';
}
In another post, I’ll go over some of the “Freebies” that come out of this project like CSS Coverage and CSS+HTML Diffs for regression testing.
Bonuses
By parsing the CSS file and “baking” the styles into the HTML there are a few “freebies” that come out.
Easy CSS Coverage
As a free perk, you can easily generate Coverage data for your CSS files by transforming a HTML and CSS file from the commandline and filtering stderr.
HTML+CSS Diffs
To do regression tests on books we merely need to generate the “baked” HTML file twice, once with the old CSS and once with the new CSS (all the styles are “baked” into style="..."
attributes).
Then, a quick XSLT file can compare the two and generate a version of the page with <span>
tags marking the places where styling changed.
See https://github.com/philschatz/css-diff.js for a package that does this.
Keep Reading...
02 Mar 2014
CSS Diffs
Our textbooks are frequently hundreds of pages long and use a single CSS file, so making a CSS change can change content in unexpected places.
Again, CSS Polyfills and a little XSLT file makes this easy.
CSS-Diff takes an HTML and CSS file and produces an HTML file with all the styling “baked” in.
Then, the provided XSLT file can compare 2 “baked” HTML files and inject <span>
tags whenever the styles differ.
See the CSS-Diff project to run it from the commandline.
Keep Reading...
10 Dec 2013
I often hear “CSS is meant to style and HTML should describe content”, but CSS alone is not enough to make textbooks.
Sometimes you need to:
- style an element based on what’s inside (notes containing a title)
- change link text based on the target (
See Figure 2a
vs See Table 4.3
)
- move elements somewhere else in the book (answers to the back)
Well, now there is css-polyfills.js, based on some great CSS3 specs, Sizzle, and selector-set.
Simple Examples (from Bootstrap)
With css-polyfills.js you can do things that are not possible using CSS in browsers. Bootstrap’s Dismissable Alerts require adding a special class on the alert if it contains a close button. This can be accomplished without adding the alert-dismissable
class by using :has
:
.alert:has(> .close) { /* Styles for `.alert-dismissable` */ }
Bootstrap Modals and Input Groups require adding wrapper elements in order to style properly; with nested ::before
and ::outside
these are unnecessary.
You can construct the 2 additional elements (.modal
, .modal-dialog
) around .modal-content
using the following CSS:
.modal-content { ... }
.modal-content::outside { /* Styles for `.modal-dialog` */}
.modal-content::outside::outside{ /* Styles for `.modal` */ }
More Examples
You can even style links depending on the target:
a[href^="#"] {
// Use target-is as a guard for which link text to use
content: target-is(attr(href), 'figure') 'See Figure';
content: target-is(attr(href), 'table') 'See Table';
content: target-is(attr(href), '.example') 'See Example';
}
Or, you can move elements down the page (ie collate footnotes at the bottom of a wikipedia article):
.footnote { move-to: footnotes-area; }
#footnotes {
content: pending(footnotes-area);
}
You can also create the linkable footnotes on wikipedia by keeping the references near the content and use CSS to create links and move the references to the bottom of the page (See Clickable Footnotes).
Of course, there’s much more that can be accomplished using css-polyfills.js; check the README.md for more details.
Keep Reading...
01 Dec 2013
There are many CSS coverage projects but none plug directly into JS unit tests, instrumenting at the same time as the JavaScript coverage.
Assuming you have BlanketJS and Mocha tests, just grab css-coverage.js and add the following into the test harness HTML file :
<link rel="stylesheet/coverage" href="path/to/file.less" />
<script src="css-coverage.js"></script>
Less.js + Mocha + BlanketJS
The coverage script uses the less.tree
AST to parse all the selectors and the debugInfo
data to get line numbers. It adds a testdone
hook to Mocha that runs all the selectors on the page. Then, the line number and coverage counts are given to BlanketJS so LESS/CSS files are included in the coverage report.
Example Screenshot
Check out the Mocha + BlanketJS demo and the github repo for more!
Other Features
- Loading LESS files using RequireJS is supported with 0 additional work
- Instrumented less files that import other files are automatically instrumented
Keep Reading...