Book authors need a way to style their book for a PDF and ePUB. Sometimes they need additional elements to properly style. They also need a way to number and move elements around, like solutions in the back of the book.
Rather than letting them write JavaScript and execute arbitrary code, authors can describe both the styling and the movement of elements in the same file.
When converting HTML there are 3 basic things one needs to do:
Each of these are accomplished using different parts of CSS:
attr(id)
)class-add: "foo", "bar";
or tag-name-set: "h3";
)::before { content: "Example"; }
)The Motivation Page has a step-by-step introduction to the CSS features or just play around with the JSFiddle of all the steps combined
The Language Reference Page contains a list of all selectors, rules, functions that are understood.
This library aims to address several stories that could be helpful.
Note: ap-physics takes a couple minutes to load and then save
This addresses a few use-cases:
Here are some example error messages from various tests. All but one are already implemented:
test/_errors.css:1:20: WARNING: Skipping unrecognized rule 'contentssss:', maybe a typo? (test/_errors.in.html:8:1)
test/specificity.css:30:28: WARNING: Skipping this rule because it was overridden by test/specificity.css:42:13 (test/specificity.in.html:8:1)
test/move.css:14:5: WARNING: Moving 0 items using selector '.exercise'. You can add a :has() guard to prevent this warning (test/move.in.html:10:1)
test/move.css:14:5: ERROR: Tried to move an HTML element that was already marked for moving. The first move rule was matched by test/move.css:6:3. (test/move.in.html:13:6)
(stack trace here)
test/move.css:7:2: ERROR: Exception occurred while manipulating the DOM (test/move.in.html:13:6)
(stack trace here)
These are formatted in a way that can be parsed by a linter and therefore can show up in your text editor :smile: and they include both the CSS and HTML source location information.
When coupled with source maps (see below), this vastly reduces debugging time because you know exactly where in the CSS file to look and which HTML element was being processed at the time.
This addresses a few use-cases:
In this animation the left pane contains the baked HTML and the right pane contains the source that generated the code (either HTML content or CSS selectors/rules).
As you click in the left pane, the right pane updates.
(there is no trickery here, this is real working code using the plugin atom-sourcemap)
This addresses a few use-cases:
This model is inspired by both CSS and virtualdom libraries like React: you describe what you want the result to look like rather than the steps to get to the result.
No more writing the intermediate steps needed to get to the desired result (ie setting temporary attributes, multiple passes to move things around).
Since each element does not depend on the state of other elements (and there is only 1 pass), the conversion can be parallelized, further reducing the conversion time.
css-plus
generates an ${OUTPUT_HTML}.lcov
file in addition to the ${OUTPUT_HTML}.map
file which contains all the CSS covered and the HTML elements that were matched during the conversion.
See the codecov page for examples.
When building an interpreter for a language there are a few useful features that make development much easier:
x-log: "My message";
in CSSfoo.scss:13:2 [error message] (foo.html#id123)
followed by a stack tracedata-debugger="true"
attribute to the HTML (or debugger: true;
in the CSS)Adding the data-debugger="true"
attribute to an element is like setting a breakpoint in other programming languages.
It will output the following and if css-plus
is run in debugging mode, pause the debugger.
Example Output below contains the following:
/----------------------------------------------------
| Debugging data for <<./data/statistics-raw.html:div#098e1a26-e612-4449-a45e-80fa23feba02@12>>
| Matched Selectors:
| ../rulesets/books/statistics/book.scss:32:8 body > [data-type="chapter"]:has(section.summary) {...}
| Applied Declarations:
| ../rulesets/books/statistics/book.scss:18:2 content: 1 "" "." "" 2 "" " ";
| ../rulesets/books/statistics/book.scss:35:10 attrs-add: "href" "" "#" "" "098e1a26-e612-4449-a45e-80fa23feba02@12";
| ../rulesets/books/statistics/book.scss:34:10 tag-name-set: "a";
\----------------------------------------------------
This is just a script that takes 2 HTML files (raw and baked HTML) and builds a sourcemap file by looking at the id attributes of elements. It is not precise but may be "good enough" to use in the short term for things like:
It is also useful for XSLT passes (like CNXML -> HTML) that have no way to otherwise generate a sourcemap
This shows a full baked textbook in Atom in the left pane and the raw book file in the right pane. It does not open up the CSS file in the right pane because the sourcemap was not generated using css-plus
To run it yourself:
node ./sourcemap-approximator.js ${SOURCE_FILE} ${GENERATED_FILE}
There are 3 phases (annotate, build a work tree, manipulate):
Debugging Example:
Example of converting an entire book (takes a few minutes but you get progress bars and status updates):
You can install this globally or as a package in an existing project.
To install and run globally, run npm install --global css-plus
Then you can run:
css-plus --css ${INPUT_CSS} --html ${INPUT_HTML} --output ${OUTPUT_HTML}
# Or if you are lazy:
css-plus ${INPUT_CSS} ${INPUT_HTML} ${OUTPUT_HTML}
To install locally and run, type npm install --save css-plus
Then you can run the previous examples but replace css-plus
with $(npm bin)/css-plus
This takes 2 XML/HTML files (ie raw & baked) and builds a rough sourcemap using the id attribute of elements.
node ./sourcemap-approximator.js ${SOURCE_FILE} ${GENERATED_FILE}
and it generates a file at ${GENERATED_FILE}.map
run ./script/test-debug
to start up a debugger and run the tests.
To test the commandline:
$(npm bin)/inspect ./bin/css-plus ${CSS_PATH} ${HTML_PATH} ${OUTPUT_HTML_PATH}
$(npm bin)/inspect ./bin/css-plus-debug ${CSS_PATH} ${HTML_PATH} ${OUTPUT_HTML_PATH}
starts up the debugger (using node --inspect-brk
)To pause when evaluating an element, add data-debugger="true"
to the element.
tag-name-set:
!important
::for-each(1, descendant, ${SELECTOR})
:target(${ATTRIBUTE_NAME}, ${MATCH_SELECTORS...})
:is(${SELECTOR})
to add exceptionsenv(NAME, DEFAULT)
functionmove-here()
for sorting a glossary or answers to exercisescontents()
which does a deep clone but removes id'stag-name-set: none;
which unwraps the element (useful for <dt>
and <dd>
pairs)build-index(${TERM_SELECTOR})
for building an index--dry-run
which outputs an evaluation tree (for debugging)attrs-remove: *
instead of attrs-set:
because they are as interchangeable and the order-of-evaluation is easier (only need to know 2: attrs-add:
and attrs-remove:
)::for-each(1, descendant, ${SELECTOR}):has(${SELECTOR_FOR_MATCHES})
should have an additional selector argument
<div epub:type="glossary">
in EPUB3 epub:type )padding: 0; padding-top: 1em;
is different than padding-top: 1em; padding: 0;
)