recipe_grid.renderer: Rendering recipes as tables

Recipes are rendered from their recipe data model descriptions (recipe_grid.recipe) into tabular form for display.

Table rendering is split into two parts. First an abstract tabular description is generated and then secondly this is rendered into its final form (i.e. HTML). This decomposition should make it possible for future non-HTML output formats to be supported.

The abstract table representation is defined in recipe_grid.renderer.table, the recipe-to-table conversion in recipe_grid.renderer.recipe_to_table and finally, table-to-HTML conversion in recipe_grid.renderer.html

recipe_grid.renderer.table: Abstract table description

The following output-agnostic data structure is used to represent a recipe in tabular form.

The basic data structure consists of a Table object which contains a set of nested lists containing a 2D array of Cell and ExtendedCell objects. Each entry in the array represents a cell in the table.

Where a cell spans multiple rows or columns, the top-left cell is defined with a Cell instance (with Cell.rows and Cell.columns set appropriately) and the occluded cells are filled with ExtendedCell instances. For convenience, the Table.from_dict() class method is provided which can automatically fill in ExtendedCell instances when only :py:class`Cell`s are given.

class recipe_grid.renderer.table.Table(cells: Sequence[Sequence[recipe_grid.renderer.table.Cell[T] | recipe_grid.renderer.table.ExtendedCell[T]]])
cells: Sequence[Sequence[Cell[T] | ExtendedCell[T]]]

The table cells. A dense 2D array indexed as cells[row][column] with spaces covered by extended cells being denoted using ExtendedCell.

classmethod from_dict(table_dict: Mapping[Tuple[int, int], Cell[T]]) Table[T]

Construct a Table from a dictionary mapping (row, column) to Cell.

to_dict() Mapping[Tuple[int, int], Cell[T]]

Return a dictionary mapping from (row, column) to Cell (omitting ExtendedCells).

property columns: int

Number of columns in this table

property rows: int

Number of rows in this table

class recipe_grid.renderer.table.Cell(value: ~T, rows: int = 1, columns: int = 1, border_left: recipe_grid.renderer.table.BorderType = <BorderType.normal: 2>, border_right: recipe_grid.renderer.table.BorderType = <BorderType.normal: 2>, border_top: recipe_grid.renderer.table.BorderType = <BorderType.normal: 2>, border_bottom: recipe_grid.renderer.table.BorderType = <BorderType.normal: 2>)
value: T

The value contained in this cell.

rows: int = 1
columns: int = 1

The number of columns (to the right) or rows (below) over which this cell extends.

border_left: BorderType = 2
border_right: BorderType = 2
border_top: BorderType = 2
border_bottom: BorderType = 2

The border styles for this cell.

class recipe_grid.renderer.table.ExtendedCell(cell: Cell[T], drow: int, dcolumn: int)

Represents parts of a table which are filled not with a new cell but with the extension of an adjacent cell. (Used as a ‘dummy’ value in Table.)

cell: Cell[T]

A reference to the cell which occludes this cell.

drow: int
dcolumn: int

The delta in coordinate from the cell this ExtendedCell resides.

Cells have four borders whose display styles are dictated by the border_* attributes of the Cell. The border styles are:

class recipe_grid.renderer.table.BorderType(value)

An enumeration.

none = 1

No border. Used only for cells which are to be rendered ‘outside’ the table.

normal = 2

A normal border style which will surround most cells.

sub_recipe = 3

A border drawn round a sub recipe; typically thicker than the normal border style.

recipe_grid.renderer.recipe_to_table: Recipe to Table transformation

The following routine converts a single recipe tree (RecipeTreeNode) into the equivalent Table form. It should be used to convert each recipe tree in Recipe.recipe_trees into its own table in the rendered output.

recipe_grid.renderer.recipe_to_table.recipe_tree_to_table(recipe_tree: recipe_grid.recipe.RecipeTreeNode) recipe_grid.renderer.table.Table

Convert a recipe tree into tabular form.

To display the resulting Table, cell contents should be rendered as indicated below.

Firstly, the obvious cases:

  • Ingredient is shown as the ingredient quantity and name etc.

  • Reference is shown as its label (and quantity/proportion etc.)

  • Step is shown as its description. The cells immediately to the left in the generated table will contain the inputs to this step.

Finally, cells containing a SubRecipe may appear in the table for one of two purposes:

  • For single-output sub recipes, the cell containing the SubRecipe will be located above the cells containing the body of the sub recipe. This cell should be rendered in the style of a heading and containing the output name as the text.

  • For multiple-output sub recipes, thec cell containing the SubRecipe will be immediately to the right of the cells defining the sub-recipe and should be rendered as a list all of the output names for the sub recipe, for example as a bulleted list.

    Note

    This cell will have all but its left border style set to be BorderType.none to give the appearance of the output list being located out to the right of the rest of the table.

Unless otherwise stated, all cells will have all borders set to BorderType.normal with the exception of those borders of cells near the edges of a sub recipe block. These will have a style of BorderType.sub_recipe.

Note

The _root argument is for internal use and must be left unspecified when called by external users. Internally, it indicates if the node passed to this function is the root node of its recipe tree.

recipe_grid.renderer.html: HTML Table Renderer

This module implements Table to HTML conversion in the following routine:

recipe_grid.renderer.html.render_recipe_tree(recipe_tree: RecipeTreeNode, id_prefix: str = 'sub-recipe-') str

Render a recipe tree as a HTML table.

The id_prefix argument may be used to specify the prefix added to all anchor IDs used in this recipe tree. The same prefix must be specified when rendering every recipe tree within a series of Recipe blocks. When a single HTML page may contain multiple, separate recipes, different prefixes should be used for each to ensure links to not conflict.

In the generated HTML, the following CSS class names are used which should be styled accordingly using a suitable stylesheet.

The generated table is mostly self explnatory, though ingredients and references may contain a <ul> (with the CSS class rg-quantity-conversions) listing quantities using alternative units. This list may be styled using CSS as a mouse-over hint (for example) or hidden entirely, as required.

CSS Classes

In the generated HTML, the following CSS class names are used

  • Top level (<table>) classes:

    rg-table

    Applied to each generated <table>.

  • Cell (<td>) semantic classes

    rg-ingredient

    Applied to each cell containing an ingredient.

    rg-reference

    Applied to each cell containing an reference.

    rg-step

    Applied to each cell containing a step description.

    rg-sub-recipe-header

    Applied to each cell acting as a header for a sub recipe.

    rg-sub-recipe-outputs

    Applied to the cell on the right-hand-end of a table representing a sub recipe with multiple outputs.

  • Cell (<td>) border styling classes

    rg-border-top-none, rg-border-bottom-none, rg-border-left-none, rg-border-right-none

    Indicates the respective border should be omitted.

    rg-border-top-sub-recipe, rg-border-bottom-sub-recipe, rg-border-left-sub-recipe, rg-border-right-sub-recipe

    Indicates the respective border should be drawn with an emphasised (e.g. bold) stroke.

    Where the above classes are given, they are given for both ‘sides’ of the border (e.g. when rg-border-left-none is used in one cell, the cell immediately to its left always has the matching class rg-border-right-none).

    When not overridden by the classes above, all other cell borders should be solid (and the <table> should have no border at all), and inter-cell spacing should be collapsed.

  • Inline styles

    rg-quantity-unitless

    Applied to the <span> surrounding the number in a unitless quantity. For example the “3” in “3 eggs”.

    rg-quantity-with-conversions

    Applied to the <span> surrounding the number and unit and list of unit conversions in a quantity. For example the “1 tsp <ul>…</ul>” in “1 tsp <ul>…</ul> of salt”.

    rg-quantity-without-conversions

    Applied to the <span> surrounding the number and unit in a quantity where no unit conversions are given. For example the “1 sack” in “1 sack of potatoes”.

    rg-quantity-conversions

    Applied to lists (<ul>>) of alternative-unit quantity values.

    rg-proportion

    Applied to the <span> surrounding the proportion in a reference. For example in “1/2 of the sauce”, this would surround the “1/2 of the” part.

    Note

    The number inside will additionally be enclosed in a <span> with the class rg-scaled-value.

    rg-proportion-remainder

    Applied to the <span> surrounding the proportion indicating a remainder in reference. For example in “remainder of the sauce”, this would surround the “remainder of the” part.

    rg-sub-recipe-output-list

    Applies to the list (ul) of output names in a cell containing a sub recipe with multiple outputs.

    rg-scaled-value

    Applies to the <span> wrapping all strings containing scaled values in the recipe. For example, ingredient quantities or text wrapped in { and } in the recipe source.

Example CSS stylesheet:

A sample stylesheet (actually, the one used by the Recipe Grid static site generator) is given below:

/**
 * CSS For styling recipe grid tables, and nothing else.
 */


/**
 * Colour definitions.
 *
 * Warning: There are a couple of SVGs in data URLs below which unfortunately
 * have these colours hard-coded and so need to be changed. Search for
 * 'data:image/svg+xml;utf8' in your editor...
 */
:root {
    --rg-table-normal-colour: var(--normal-colour, #000000);
    --rg-table-accent-colour: var(--accent-colour, #FE5E41);
    --rg-table-inverted-fg-colour: var(--inverted-fg-colour,#FFFFFF);
}

/**
 * General table layout
 */
table.rg-table {
    margin-top: 8px;
    margin-bottom: 8px;
    
    border-spacing : 0;
    border-collapse : collapse;
}

table.rg-table tr td {
    text-align : left;
    vertical-align: middle;
    
    margin : 0;
    padding : 0;
    
    padding-left  : 10px;
    padding-right : 10px;
    padding-top   : 3px;
    padding-bottom: 3px;
    
    border-style : solid;
    border-width : 1px;
    border-color : var(--rg-table-normal-colour);
}

/**
 * Border specifying classes.
 */

table.rg-table tr td.rg-border-right-none { border-right-style: none; }
table.rg-table tr td.rg-border-left-none { border-left-style: none; }
table.rg-table tr td.rg-border-top-none { border-top-style: none; }
table.rg-table tr td.rg-border-bottom-none { border-bottom-style: none; }

table.rg-table tr td.rg-border-right-sub-recipe { border-right-width: 3px; }
table.rg-table tr td.rg-border-left-sub-recipe { border-left-width: 3px; }
table.rg-table tr td.rg-border-top-sub-recipe { border-top-width: 3px; }
table.rg-table tr td.rg-border-bottom-sub-recipe { border-bottom-width: 3px; }


/**
 * Specialisations for cell types (e.g. ingredient or sub recipe header)
 */

table.rg-table tr td.rg-ingredient {
    font-weight: 600;
}

table.rg-table tr td.rg-reference {
    font-style: italic;
}

table.rg-table tr td.rg-reference a:before {
    content: "\2196";
}

table.rg-table tr td.rg-sub-recipe-header {
    background-color: var(--rg-table-normal-colour);
    color: var(--rg-table-inverted-fg-colour);
    border-color: var(--rg-table-normal-colour);
    
    border-top: none;
}

table.rg-table tr td.rg-sub-recipe-outputs {
    padding: 0;
}

table.rg-table tr td.rg-sub-recipe-outputs ul {
    display: flex;
    flex-direction: column;
    align-items: start;
    justify-content: center;
    
    padding: 0;
    margin: 0;
}

table.rg-table tr td.rg-sub-recipe-outputs ul li {
    display: block;
    font-weight: bold;
    
    margin: 0;
    padding-left  : 32px;
    padding-right : 8px;
    padding-top   : 3px;
    padding-bottom: 3px;
    
    border-top-right-radius: 4px;
    border-bottom-right-radius: 4px;
    
    background-repeat: no-repeat;
    background-position-x: left;
    background-position-y: center;
    /* NB: The fill colour in this image should be modified to be 
     * the same as --rg-table-normal-colour */
    background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="12" viewBox="0 0 6.35 3.175"><path d="M4.058 0v1.191H0v.794h4.058v1.19L6.35 1.588 4.058 0z" fill="%23000000"/></svg>');
}


/**
 * Unit conversion popups
 */
.rg-quantity-with-conversions ul.rg-quantity-conversions {
    position: absolute;
    z-index: 999;
    
    margin: -8px;
    margin-top: 4px;
    padding: 8px;
    
    background-color: white;
    
    border-style: solid;
    border-width: 2px;
    border-color: black;
    
    border-radius: 4px;
    
    filter: drop-shadow(4px 4px 8px #00000033);
    
    list-style: none;
    text-align: left;
}

/* Draw an arrow pointing up at the value. */
.rg-quantity-with-conversions ul.rg-quantity-conversions:before {
    content: "";
    display: block;
    position: absolute;
    top: -16px;
    
    box-sizing: content-box;
    width: 0;
    height: 0;
    
    border-style: solid;
    border-color: black;
    border-width: 8px;
    border-left-color: transparent;
    border-right-color: transparent;
    border-top-color: transparent;
    
}

.rg-quantity-with-conversions ul.rg-quantity-conversions li {
    padding-bottom: 8px;
}
.rg-quantity-with-conversions ul.rg-quantity-conversions li:last-child {
    padding-bottom: 0;
}

/* Show on unit conversions on mouseover/focus */
.rg-quantity-with-conversions:focus {
    outline: none;
}
.rg-quantity-with-conversions .rg-quantity-conversions {
    visibility: hidden;
}
.rg-quantity-with-conversions:hover .rg-quantity-conversions,
.rg-quantity-with-conversions:focus-within .rg-quantity-conversions {
    visibility: visible;
    pointer-events: auto;
}
.rg-quantity-with-conversions:hover .rg-quantity-conversions {
    z-index: 1000;
    pointer-events: none;
}

/* Hide conversions in print */
@media print {
    .rg-quantity-with-conversions .rg-quantity-conversions {
        display: none;
    }
}


/**
 * Apply accent colouring when a table cell is targeted.
 */

table.rg-table:target {
    border-color : var(--rg-table-accent-colour);
}
table.rg-table:target tr td.rg-border-right-sub-recipe {
    border-right-color: var(--rg-table-accent-colour);
}
table.rg-table:target tr td.rg-border-left-sub-recipe {
    border-left-color: var(--rg-table-accent-colour);
}
table.rg-table:target tr td.rg-border-top-sub-recipe {
    border-top-color: var(--rg-table-accent-colour);
}
table.rg-table:target tr td.rg-border-bottom-sub-recipe {
    border-bottom-color: var(--rg-table-accent-colour);
}


table.rg-table:target tr td.rg-sub-recipe-header {
    background-color: var(--rg-table-accent-colour);
    color: var(--rg-table-inverted-fg-colour);
    border-color: var(--rg-table-accent-colour);
}

table.rg-table tr td.rg-sub-recipe-outputs ul li:target {
    color: var(--rg-table-inverted-fg-colour);
    background-color: var(--rg-table-accent-colour);
    
    /* NB: The fill colour in this image should be modified to be 
     * the same as --rg-table-inverted-fg-colour */
    background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="12" viewBox="0 0 6.35 3.175"><path d="M4.058 0v1.191H0v.794h4.058v1.19L6.35 1.588 4.058 0z" fill="%23FFFFFF"/></svg>');
}