🎨 Semantic themes for use with Stencila
encoda
.
Element
| null
Array.<Element>
Element
string
| Element
object
| undefined
string
| null
string
| null
| undefined
npm install @stencila/thema
/* myStyleSheet.css */
@import '@stencila/thema/dist/themes/stencila/styles.css';
/* myJavaScript.js */
@import '@stencila/thema/dist/themes/stencila';
Thema comes with several premade themes. Preview and customize the themes in the gallery, or learn how to make one from scratch.
Name | Description |
---|---|
bootstrap | A theme that pulls itself up using Twitter's Bootstrap toolkit. |
elife | A theme for the journal eLife. |
f1000 | A theme for the journal F1000Research. |
galleria | A theme for galleries of CreativeWork nodes.
|
giga | A theme for the "Giga" journals GigaScience and GigaByte. |
latex | Inspired by the look of traditional scientific manuscripts witten using LaTeX. This theme uses LatexCSS developed by David Zollikofer. |
nature | A theme for the journal Nature. |
plos | A theme for the journal PLoS. |
rpng | A theme for reproducible PNGs (rPNGs). This theme is used in Encoda when generating rPNGs. |
skeleton | A theme with lots of bones but no flesh. Designed to be used as a starting point for creating new themes, it tries to be as unopinionated as possible. |
stencila | A theme reflecting Stencila's brand and design system. It is based on the Skeleton theme, and demonstrates how to customize a theme using CSS variables. |
tufte | A theme inspired by the books and handouts of Edward Tufte. It is based on the Tufte CSS project created by Dave Liepmann. |
wilmore | A theme well suited for consuming long-form manuscripts and prose. Named after Edmond Dantés' alias, “Lord Wilmore: An Englishman, and the persona in which Dantès performs random acts of generosity.“ |
There are two primary files inside
each theme folder. The naming convention of these two files is important, and they must not be
changed since they are referred to from encoda
.
styles.css
: CSS and visual styles
specific to the theme. We use PostCSS to compile the CSS. This is done to
utilize PostCSS utilities such as autoprefixing vendor flags to selectors, and writing nested
selectors.
index.ts
: Written in TypeScript, this file is loaded asynchronously.
It is used to progressively enhance the theme with things like syntax highlighting of code
blocks.
Stencila Web Components are used to provide interactivity and enhancement to several document nodes.
Encoda will output several Schema nodes as custom HTML elements. Currently, the following node types have Web Components:
Node type | Custom element |
---|---|
CodeChunk |
<stencila-code-chunk> |
CodeExpression |
<stencila-code-expression>
|
More components will be added over
time. In the meantime, the "pseudo-component" folders in src/extensions
, provide styling
for some other node types. See the Extensions section for more details.
Extensions provide styling, and potentially interactivity, for node types that do not yet have corresponding web components. They are like fledgling web components, each with it's own CSS (and/or Javascript), that you can import into your own theme. Over time we expect extensions to be promoted to the Stencila components library, thereby obviating the need to import them explicitly.
Name | Description |
---|---|
cite | Provides styling for in-text
citations (i.e. Cite and CiteGroup nodes) and
bibliographies (i.e. CreativeWork nodes in the references property
of another CreativeWork ). |
cite-apa | Provides styling for in-text citations and bibliographies in accordance with the American Psychological Association (APA) style. |
cite-author-year | CSS styles to support author-year in-text citations as used in citation styles such as APA, Chicago and MLA. |
cite-mla | Provides styling for in-text citations and bibliographies in accordance with the Modern Language Association (MLA) style. |
cite-numeric | CSS styles to support numeric in-text citations (e.g. Vancouver, IEEE citation styles). |
code | Provides syntax highlighting
for CodeFragment
and CodeBlock
nodes using Prism. Will not style executable node types
like CodeExpression and CodeChunk which are
styled by the base Stencila Web Components. |
math | Provides styling of math nodes
using MathJax fonts and styles. Use this if there is any likely to be math content, i.e.
MathFragment
and/or MathBlock
nodes, in documents that your theme targets. |
organization | Provides styling of Organization nodes
e.g the authors
of an article, or affiliations for each author in it's authors list. |
pages | Provides a `mediaprint` CSS at-rule(https://developer.mozilla.org/en-US/docs/Web/CSS/@page) to modify properties when printing a document e.g. to PDF. |
person | Provides styling of Person nodes e.g the
authors of an
article, or authors for each citation in it's references . |
To add an extension to your theme, you simply have to import it's CSS and Javascript.
First import it's styles.css
file into your
theme's styles.css
file e.g.
@import '../../extensions/cite-apa/styles.css';
Then, if it has one, import it's
index.ts
file into
your theme's index.ts
file e.g.
import '../../extensions/cite-apa'
The first question to ask when developing a new extension is: should I? Extensions are best suited for styling / features that are:
If it's not likely to be used in
more than one or two themes then it's probably not worth creating an extension. If it's
not needed in a hurry, then it is probably better to put the effort into contributing a web
component to @stencila/components
. Having said that,
if you are more comfortable writing a simple extension here, to try out some ideas for something
that may become a fully fledged web component, we are grateful for any contributions.
The easiest way to create a new extension is using:
npm run create:extension -- myextensionname
That will update the src/extensions/index.ts
file with a new entry for your extension and create a new folder in the src/extensions
folder with the
necessary files:
src/extensions/myext
├── README.md
└── styles.css
You can create the folder and files
yourself if you prefer. Just remember to run npm run update:extensions
afterwards to
add your extension to the index. See the other extensions for approaches to writing the CSS and
Javascript / Typescript for your extension.
Some extensions perform manipulation
of the DOM to make it more amenable to achieving a particular CSS styling e.g. adding a wrapping
<div>
s. For
performance reasons these manipulations should be kept to a minimum. In some cases, it may be
better to make the necessary changes to Encoda's HTML codec. In those cases the DOM
manipulations in the extension should be commented as being temporary, and be linked to an issue in Encoda to make those changes
permanent.
The best way to get started is to develop CSS and JS for a theme with the live updating demo running.
# Clone this repository
git clone git@github.com:stencila/thema.git
cd thema
# Install dependencies
npm install
# Build auto-generated files necessary for theme functionality and development
npm run bootstrap
# Run the development server
npm run dev
Your browser should automatically open (http://localhost:8081)http://localhost:8081 with the theme gallery view. Any changes to the stylesheets and code will be automatically recompiled and reflected in the browser.
There are a few URL query parameters which can be used to control the UI of the theme preview.
Parameter | Default Value | Description |
---|---|---|
theme |
stencila |
Sets the active theme for the preview |
example |
articleKitchenSink |
Sets the content for the preview |
ui |
true |
When set to false hides the header and theme
customization sidebar from the preview page |
header |
true |
When set to false hides only the header from
the preview page |
sidebar |
true |
When set to false hides only the theme
customization sidebar from the preview page |
The easiest way to create a new theme is:
npm run create:theme -- mytheme
Theme names should be all lower case,
and start with a letter. This creates a new folder in src/themes
and the following files:
README.md
providing a description of
the theme and notes for contributors,
styles.css
file for the theme's
CSS,
index.ts
for any Typescript that the
theme may need
You can create this folder structure
for your theme manually. If you prefer to use Javascript instead of Typescript, use a index.js
file instead of
index.ts
. Then update
the list of themes in themes/themes.ts
and elsewhere using:
npm run update:themes
There are three broad approaches to developing a new theme, each epitomized in three of the themes in this repository:
skeleton
approach: define all
styles yourself, importing only the extensions
needed for the theme
stencila
approach: leverage skeleton
theme as a foundation,
overriding CSS variables and nodes as needed start from a relatively
bootstrap
approach: reuse
existing stylesheets from elsewhere by creating a mapping between Thema's semantic
selectors and existing selectors in those stylesheets
It is important to note that the
skeleton
and bootstrap
themes are
extremes of each of the approaches - they apply their approach to all document node types. Depending on your
theme, the best approach is probably some combination of these approaches for different node
types e.g. starting from scratch for some nodes and using shared
styles for others.
There are a few key rules enforced by Stylelint:
To tweak or adjust an existing theme, you may override some common CSS variables found in the themes. Please refer to the specific theme documentation for available variables.
For types defined by Schema.org
(e.g. Article
), or
extensions such as, schema.stenci.la (e.g. CodeChunk
), Bioschemas (e.g. Taxon
) etc.
[itemtype=...]
selector if possible
(i.e. if Encoda encodes it in HTML)
For properties of types defined in schemas.
[itemprop=...]
selector for singular
properties, or items of container properties
There are several additional selectors which are not found as the Stencila Schema definitions. These are:
Selector | Description | Target |
---|---|---|
:--root |
Used in place of the :root CSS pseudo selector.
It maps to the root element generated by Encoda. This is
done to avoid potential clashes with external stylesheets, and to ensure that Thema only
styles semantically annotated content. |
[data-itemscope=root] |
:--CodeBlockTypes |
Block level code elements | :--CodeBlock , :--CodeChunk |
:--CodeTypes |
Inline level code elements | :--CodeBlock , :--CodeChunk , :--Code , :--CodeError , :--CodeExpression , :--CodeFragment ,
:--SoftwareSourceCode |
:--ListTypes |
List elements, both ordered and unordered, as well as other lists such as author affiliations and article references | :--Article:--root >
:--affiliations , :--Collection , :--List , :--references > ol |
:--MediaTypes |
These are elements which usually benefit from taking up a wider screen area. Elements such as images, video elements, code blocks | :--CodeBlock , :--CodeChunk , :--Datatable , :--Figure , :--ImageObject , :--MediaObject ,
:--Table , :--VideoObject |
Some files in the src
directory are auto-generated and
should not be edited manually. Generated files include:
src/themes/themes.ts
: from the list
of sub-folders in the themes
folder
src/examples/examples.ts
: from the
functions defined in generate/examples.ts
src/examples/*
: files generated by
those functions, usually using the @stencila/encoda
package
src/selectors.css
: custom selectors
from the JSON Schema in the @stencila/schema
package
Run npm run update
when you add new themes,
addons, or examples, or upgrade one of those upstream packages. In particular, when Encoda is
upgraded it is important to regenerate HTML for the examples - npm run update
is called after npm install
to ensure
this.
Authors of themes and extensions, and
contributors to the utility functions are encouraged to add tests. We use Jest as a
testing framework. Jest ships with jsdom
which simulates a DOM
environment in Node.js, thereby allowing testing as though running in the browser. See existing
*.test.ts
files for
examples of how to do that.
We use visual regression testing powered by Sauce Labs, Percy, and WebdriverIO.
As part of the continuous integration for this repository, for each push,
To run these tests locally, run npm run test
. By default
Webdriver will try to run the tests using Chrome, but you can switch to Firefox by setting an
TEST_BROWSER=firefox
environment variable.
When testing locally, there are three
screenshot folders to be aware of inside the test/screenshots
directory:
reference
: These are the baseline
screenshots. To generate them, make sure your Git branch is free of any changes, and run npm test
.
local
: These are screenshots
generated by Webdriver tests, and will be compared to those found in the reference
directory.
diff
: If any discrepancies are found
between the reference
and local
screenshots, the differences
will be highlighted and saved to this directory.
There is a pseudo-test in test/screenshot.test.js
which can be un-skipped to help with debugging the automated running of tests.
Commit messages should follow the conventional commits specification. This is useful
(but not essential) because commit messages are used to determine the semantic version of
releases and to generate the project's CHANGELOG.md. If appropriate, use the sentence
case theme name as the scope (to help make both git log
and the CHANGELOG more
readable). Some examples,
fix(Wilmore): Fix Code, Math, DataPublished
node formatting & styles
feat(Elife): Use eLife corresponding author
envelope icon
docs(README): Add some notes on
testing
ci(Travis): Fix command to check
themes
We rely on many tools and services for which we are grateful ❤ to their developers and contributors for all their time and energy.
Tool | Use |
---|---|
Cross-browser testing platform | |
WebDriver test framework for Node.js |
Several utility functions are
provided in the util
module for
traversing and manipulating the DOM. These may be useful for theme and extension authors when
there is a need to modify the HTML structure of the document (e.g. adding additional purely
presentational elements such as social sharing buttons). Caution should be taken to not overly
modify the content. Only use these functions for things that are not possible with CSS alone.
Register a function to be executed when the DOM is fully loaded.
Param | Type | Description |
---|---|---|
func | function |
Function to register |
Examplejs ready(() => { // Use other DOM
manipulation functions here })
Element
| null
Select the first element matching a CSS selector.
Param | Type | Description |
---|---|---|
elem | Element |
The element to query (defaults
to the window.document ) |
selector | string |
The selector to match |
Example (Select the first element from the document matching selector)```js
first(':--CodeChunk') **Example** *(Select the first
element within an element matching the selector)*
js
Array.<Element>Select all elements matching a CSS selector.
Param | Type | Description |
---|---|---|
elem | Element |
The element to query (defaults
to the window.document ) |
selector | string |
The selector to match |
Example (Select all elements from the document matching selector)```js
select(':--CodeChunk') **Example** *(Select all elements
within an element matching the selector)*
js
Element
Create a new element.
Param | Type | Description |
---|---|---|
spec | string | Element |
Specification of element to create. |
attributes | object | undefined | null | boolean | number | string | Element |
Attributes for the element. |
...children | undefined | null | boolean | number | string | Element |
Child nodes to to add as text content or elements. |
Example (Create a <figure> with id, class and itemtype attributes)```js
create('figure #fig1 .fig
:--Figure') //
**Example** *(As above but using an
object to specify attributes)*
js
create('figure', { id:
'fig1', class: 'fig', itemscope: '', itemtype:
translate(':--Figure') }) **Example** *(Create a Person with a name
property)*
js
create(':--Person',
create('span :--name', 'John Doe')) // Anonymous **Example** *(Create a link around an SVG
image)*
js
create('a', {href: 'https://example.com'}, create(imageSVG)) // // <path d=".... // ['```',{target:'#',content:[],type:'Link'}]
string
| Element
Get or set the tag name of an element.
Param | Type | Description |
---|---|---|
target | Element |
The element to get or set the tag |
value | string |
The value of the tag (when setting) |
Example (Get the tag name as a lowercase string)```js
tag(elem) // "h3" **Example** *(Setting the tag
actually returns a new element)*
js
tag(tag(elem, 'h2')) //
"h2" **Example**
*(Change the tag name of an element)*
js
replace(elem, tag(elem, 'h2')) ```
object
| undefined
Get or set the attributes of an element
Param | Type | Description |
---|---|---|
target | Element |
The element to get or set the attributes |
attributes | object |
The name/value pairs of the attributes |
string
| null
Get or set the value of an attribute on an element.
Param | Type | Description |
---|---|---|
target | Element |
The element to get or set the attribute |
name | string |
The name of the attribute |
value | string |
The value of the attribute (when setting) |
Example (Set an attribute value)```js
attr(elem, "attr",
"value") **Example** *(Get an attribute)*
js
attr(elem, "attr") // "value" ```
string
| null
| undefined
Get or set the text content of an element.
Param | Type | Description |
---|---|---|
target | Element |
The element to get or set the text content |
value | string |
The value of the text content (when setting) |
Example (Set the text content)```js
text(elem, "text content")
**Example** *(Get the text
content)*
js
text(elem) // "text content" ```
Append new child elements to an element.
Param | Type | Description |
---|---|---|
target | Element |
The element to append to |
...elems | Element |
The elements to append |
Prepend new child elements to an element.
Param | Type | Description |
---|---|---|
target | Element |
The element to prepend to |
...elems | Element |
The elements to prepend |
Insert new elements before an element.
Param | Type | Description |
---|---|---|
target | Element |
The element before which the elements are to be inserted |
...elems | Element |
The elements to insert |
Insert new elements after an element.
Param | Type | Description |
---|---|---|
target | Element |
The element after which the elements are to be inserted |
...elems | Element |
The elements to insert |
Replace an element with a new element.
Param | Type | Description |
---|---|---|
target | Element |
The element to replace |
...elems | Element |
The elements to replace it with |
Wrap an element with a new element.
Param | Description |
---|---|
target | The element to wrap |
elem | The element to wrap it in |
Example (Wrap all figure captions in a <div>)```js
select(':--Figure :--caption') .forEach(caption => wrap(caption, create('div'))) ```