Source code for greater_tables.render.css

"""The CSS contract: one RULES dict → committed ``assets/gt.css``.

Single source of truth for the class vocabulary shared by the Python HTML
renderer and the JS walker (conformance transcripts enforce the agreement).
The [notebook-html] inline flattener consumes the same dict, so the sheet
and inline styles cannot diverge.

Principles (dev/design.md): selectors double the wrapper class (``.gt.gt``)
to beat JupyterLab/Quarto host rules without ``!important``; per-table
scalars ride as custom properties with defaults declared on ``.gt``; colors
default to ``currentColor``/transparent so an embedded fragment has no color
opinion; no widths anywhere — ``gt-nowrap``/``gt-wrap`` express intent and
the browser measures.

Semantic classes:

- alignment ``gt-l gt-r gt-c`` (right implies tabular numerals)
- role ``gt-stub gt-data``; ``gt-stub-end`` marks the last stub cell of a row
  (carries the stub/body vertical rule, spanning cells carry it for covered
  rows automatically)
- wrapping ``gt-nowrap gt-wrap``
- row breaks ``gt-break-0/1/2`` — border-top on the row's *own* cells only,
  so rowspan-covered stub columns are skipped and the rule starts exactly at
  the changed column
- column-group breaks ``gt-vbreak-0/1`` (border-left)
- flags ``gt-total gt-subtotal gt-emph gt-muted gt-neg``; ``gt-math``
"""

from __future__ import annotations

from importlib.resources import files
from pathlib import Path

__all__ = ['RULES', 'VARIABLES', 'css_text', 'write_asset']

# custom-property defaults, declared on the wrapper; HouseStyle/themes override
VARIABLES: dict[str, str] = {
    '--gt-pad-v': '0.25em',
    '--gt-pad-h': '0.6em',
    # widths: the table's ceiling and one wrapping column's, the CSS side of
    # HouseStyle.max_table_width_em and .max_prose_width_em. There are no
    # per-column widths here by design — gt-nowrap pins the columns that must
    # not be narrowed and the browser measures the rest, with better metrics
    # than any estimate of ours.
    '--gt-max-width': 'none',
    '--gt-prose-max': '18em',
    '--gt-rule-frame': '1.2pt solid',
    '--gt-rule-head': '0.7pt solid',
    '--gt-rule-0': '0.5pt solid',
    '--gt-rule-1': '0.35pt solid',
    '--gt-rule-2': '0.2pt solid',
    # column-group verticals: same defaults as the row rules, but their own
    # properties so one can be softened or removed without the other
    '--gt-vrule-0': '0.5pt solid',
    '--gt-vrule-1': '0.35pt solid',
    '--gt-rule-stub': '0.5pt solid',
    '--gt-rule-total': '0.7pt solid',
    '--gt-rule-spanner': '0.35pt solid',
    '--gt-neg-color': 'inherit',
    '--gt-caption-align': 'left',
}

RULES: dict[str, dict[str, str]] = {
    '.gt': {
        **VARIABLES,
        'width': 'fit-content',
        'max-width': 'var(--gt-max-width)',
        'line-height': '1.3',
    },
    '.gt.gt table': {
        'border-collapse': 'collapse',
        'table-layout': 'auto',
        'border-top': 'var(--gt-rule-frame)',
        'border-bottom': 'var(--gt-rule-frame)',
    },
    '.gt.gt .gt-caption': {
        'caption-side': 'top',
        'text-align': 'var(--gt-caption-align)',
        'font-weight': '600',
        'padding': '0 var(--gt-pad-h) 0.4em 0',
    },
    '.gt.gt td, .gt.gt th': {
        'padding': 'var(--gt-pad-v) var(--gt-pad-h)',
        'vertical-align': 'top',
        'background': 'transparent',
        'border': 'none',
    },
    '.gt.gt th': {'font-weight': '600'},
    '.gt.gt thead tr:last-child th': {'border-bottom': 'var(--gt-rule-head)'},
    # full-width underline beneath a group label, spanning its columns
    # edge-to-edge; adjacent groups' rules meet at the gt-vbreak vertical
    '.gt.gt th.gt-spanner': {'border-bottom': 'var(--gt-rule-spanner)'},
    '.gt.gt .gt-l': {'text-align': 'left'},
    '.gt.gt .gt-r': {
        'text-align': 'right',
        'font-variant-numeric': 'tabular-nums',
    },
    '.gt.gt .gt-c': {'text-align': 'center'},
    '.gt.gt .gt-nowrap': {'white-space': 'nowrap'},
    '.gt.gt .gt-wrap': {
        'max-width': 'var(--gt-prose-max)',
        'overflow-wrap': 'break-word',
    },
    '.gt.gt .gt-stub-end': {'border-right': 'var(--gt-rule-stub)'},
    '.gt.gt tr.gt-break-0 > td': {'border-top': 'var(--gt-rule-0)'},
    '.gt.gt tr.gt-break-1 > td': {'border-top': 'var(--gt-rule-1)'},
    '.gt.gt tr.gt-break-2 > td': {'border-top': 'var(--gt-rule-2)'},
    '.gt.gt .gt-vbreak-0': {'border-left': 'var(--gt-vrule-0)'},
    '.gt.gt .gt-vbreak-1': {'border-left': 'var(--gt-vrule-1)'},
    '.gt.gt tr.gt-total > td': {
        'font-weight': '600',
        'border-top': 'var(--gt-rule-total)',
    },
    '.gt.gt tr.gt-subtotal > td': {'font-weight': '600'},
    '.gt.gt .gt-emph': {'font-weight': '600'},
    '.gt.gt .gt-muted': {'opacity': '0.6'},
    '.gt.gt .gt-neg': {'color': 'var(--gt-neg-color)'},
    '.gt.gt .gt-math': {'white-space': 'nowrap'},
    '.gt.gt .gt-notes': {
        'font-size': '0.85em',
        'opacity': '0.75',
        'padding-top': '0.3em',
    },
    # fit mode (walker option): solver widths assume single-line ellipsis cells
    '.gt.gt.gt-fit td, .gt.gt.gt-fit th': {
        'white-space': 'nowrap',
        'overflow': 'hidden',
        'text-overflow': 'ellipsis',
    },
}


[docs] def css_text() -> str: """Serialize RULES to the stylesheet, deterministically.""" lines = ['/* generated from greater_tables.render.css — do not edit */'] for selector, props in RULES.items(): body = ' '.join(f'{k}: {v};' for k, v in props.items()) lines.append(f'{selector} {{ {body} }}') return '\n'.join(lines) + '\n'
def asset_path() -> Path: """Path of the committed stylesheet inside the package.""" return Path(str(files('greater_tables').joinpath('assets', 'gt.css')))
[docs] def write_asset() -> Path: """Regenerate the committed ``assets/gt.css`` (explicit recipe).""" path = asset_path() path.write_text(css_text(), encoding='utf-8', newline='\n') return path