mathpix-markdown-it
Version:
Mathpix-markdown-it is an open source implementation of the mathpix-markdown spec written in Typescript. It relies on the following open source libraries: MathJax v3 (to render math with SVGs), markdown-it (for standard Markdown parsing)
268 lines (190 loc) • 11.7 kB
Markdown
# PR: CSS scoping — `#preview-content`/`#setText` specificity boost for all MMD selectors
Status: Implemented
Owner: @OlgaRedozubova
---
## Context
MMD styles use two scoping mechanisms:
1. **ID selectors** (`#setText h1`, `#preview-content table`) for generic HTML elements — specificity (1,0,1), already beats any host class-based CSS.
2. **Class selectors** (`.tabular`, `.figure_img`, `.itemize`, `.enumerate`, `.hljs-*`, `.proof`, `.author`, etc.) for MMD-specific elements — were bare, vulnerable to host CSS overrides with equal or higher specificity.
When MMD content is embedded on a host page (e.g., `.docs-content table { width: 100% }`), the host styles override MMD class-based rules. This caused tabular tables to stretch, figure alignment to break, list spacing to change, and syntax highlighting colors to be overridden.
---
## Goal
- Boost specificity of all MMD class selectors by adding `#preview-content`/`#setText` scoped variants.
- Keep bare selectors as fallback for `markdownToHTML()` which returns raw HTML without wrapper.
- Clean up dead code, fix bugs, restructure style modules for readability.
- No new wrapper elements, no breaking changes to HTML output or public API signatures.
---
## Scoping Strategy
### Pattern: bare + scoped
Every MMD class selector now follows this pattern:
```css
.selector,
#preview-content .selector, #setText .selector {
/* styles */
}
```
- **Bare** (0,1,0) — fallback for `markdownToHTML()` consumers without `#setText`/`#preview-content` wrapper.
- **Scoped** (1,1,0) — beats host `.docs-content .selector` (0,1,x) when inside containers.
### Exceptions (bare only, no scoped)
| Selector | Reason |
|---|---|
| `.math-block`, `.math-inline`, `.math-error` | `math-` prefix is distinctive enough; no known collisions |
| `mjx-container` | Custom element, cannot collide with host CSS |
| `*[data-has-dotfill]`, `*[data-has-dotfill] .dotfill` | `data-` attribute scoping is sufficient |
| `svg .math-inline`, `svg mjx-container`, etc. | SVG context selectors |
### Exceptions (scoped only, no bare)
| Selector | Reason |
|---|---|
| `code`, `pre code`, `pre`, `table`, `blockquote`, `img`, `sup`, `h1`–`h6` | Generic HTML elements — bare selectors would affect all elements on host page |
---
## Non-Goals
- Adding a `.mmd` wrapper class (rejected: `#setText` already serves as wrapper).
- Renaming CSS classes `.tabular` → `.mmd-tabular` (rejected: breaking change with no real benefit).
- Shadow DOM encapsulation.
---
## Existing Protection (no changes needed)
| Element | Protection | Mechanism |
|---|---|---|
| Generic `<h1>`–`<h6>`, `<table>`, `<blockquote>`, `<pre>`, `<code>` | `#setText`, `#preview-content` | ID specificity (1,0,1) |
| `.tabular` display | `display: inline-table !important` | `!important` |
| `.tabular td` borders, padding | `border-style: none !important`, `padding !important` | `!important` |
| `.tabular tr` borders | `border-top/bottom: none !important` | `!important` |
| `<td>` text-align | `style="text-align: ..."` | Inline style |
| `<ul>/<ol>` list-style-type | `style="list-style-type: ..."` | Inline style |
| `<blockquote>` margins, padding | `style="margin: ...; padding: ..."` | Inline style (forDocx) |
---
## Changes by File
### `src/styles/index.ts` — restructured into sub-functions
Monolithic `MathpixStyle` function split into 10 named sub-functions composed via array join:
```typescript
export const MathpixStyle = (...) => [
layoutStyles({ setTextAlignJustify, maxWidth, isPptx }),
mathStyles(maxWidth),
imageStyles(),
blockquoteStyles(useColors),
codeBlockStyles(useColors),
tableStyles(useColors),
docStructureStyles(),
inlineTextStyles(useColors),
miscStyles(),
printStyles(),
].join('');
```
Scoped selectors added for: `.proof`, `.theorem`, `.main-title`, `.author`, `.section-title`, `.abstract`, `.text-url`, `mark`, `span[data-underline-type] mark`, `.smiles`, `div.svg-container`. Bare `h1, h2, ...` in `maxWidth` overflow block scoped as `#setText > h1, #setText > h2, ...`.
Dead code removed: `.empty` (never generated), `.preview-right` (used as id, not class), `scaleEquation` parameter (accepted but never used in CSS output).
Specificity side-effect fix: `.tabular` had `margin: 0` which at (0,1,0) was overridden by `#setText table { margin-bottom: 1em }` (1,0,1). After scoping, `#setText .tabular` (1,1,0) beats `#setText table` (1,0,1), dropping the margin. Fixed by replacing `margin: 0` with `margin: 0 0 1em` so `.tabular` explicitly declares its own bottom margin. Additionally, `font-size: inherit` and other defensive defaults now ensure `.tabular` renders consistently regardless of context (e.g., standalone vs nested inside a list) — previously, list context could affect table width and font size via cascade.
`useColors=false` color leaks fixed: blockquote `border-left`, table `border`, and `mark` `background-color` now gated behind `useColors`.
Bug fix: `div.svg-container` child combinator consistency (`>` for both `#preview-content` and `#setText`).
### `src/styles/styles-tabular.ts` — replaced `.table_tabular .tabular` with ID scoping
Before (commit f0e068a): specificity boosted via `.table_tabular .tabular` compound selector.
Now: replaced with `#preview-content .tabular, #setText .tabular` — cleaner, consistent with other files.
```css
.tabular,
#preview-content .tabular, #setText .tabular {
display: inline-table !important;
width: auto;
/* ... */
}
```
Also scoped: `.table_tabular`, `.tabular th/tr/td/td>p`, `.tabular td._empty`, `.tabular td .f`, `.figure_img`, `div.figure_img img`, dark theme selectors.
Note: `.sub-table` rule moved here from `index.ts` (where it didn't belong).
### `src/styles/styles-code.ts` — scoped hljs, normalized indentation
All 19 `.hljs-*` rule blocks now follow bare + scoped pattern. Indentation normalized to 0/2 (0 for selectors, 2 for properties).
### `src/styles/styles-lists.ts` — scoped all selectors
All list selectors (`ol.enumerate`, `ul.itemize`, `.li_enumerate`, `.li_level`, `.not_number`) now have bare + scoped variants.
### `src/styles/halpers.ts` → `src/styles/helpers.ts` — renamed, cleaned up
- Fixed typo in filename: `halpers` → `helpers`
- Fixed `max-width:` formatting (added space after colon)
- Added scoped variants for `.math-block`, `.smiles`, `.smiles-inline`, `.table_tabular`
- Combined h1-h6 `::-webkit-scrollbar` into single `hideScroll()` call
- Normalized indentation
### `src/mathpix-markdown-model/index.ts`
- Import path updated: `halpers` → `helpers`
- Removed `scaleEquation` from `StyleBundleOpts` interface, `buildStyles()`, and all public method signatures
### `tests/_styles.js`
- Updated `MathpixStyle` calls: removed `scaleEquation` argument
- Updated `max-width` assertion: `'max-width:800px;'` → `'max-width: 800px;'`
- Added `t()` trim helper for composition/buildStyles assertions (accounts for `parts.map(s => s.trim()).join('\n')` in `buildStyles`)
- Added `tabularStyles(isPptx=true)` snapshot test
- Added `codeStyles(false)` coverage in `getMathpixStyle(useColors=false)` test
- Added `tabularStyles(true, true)` assertion in `buildStyles isPptx` test
- All 79 tests pass
### Snapshot files
All 17 `tests/_data/_styles/*.snap.css` files regenerated (including `tabularStyles-pptx`).
---
## Style system improvements (from prior commits)
### `buildStyles(opts: StyleBundleOpts)` — single style builder
All 4 style assembly points (`loadMathJax`, `getMathpixStyleOnly`, `getMathpixStyle`, `getMathpixMarkdownStyles`) delegate to a single `buildStyles()` method.
```typescript
interface StyleBundleOpts {
setTextAlignJustify?: boolean;
useColors?: boolean;
maxWidth?: string;
isPptx?: boolean;
resetBody?: boolean;
container?: boolean;
mathjax?: boolean;
code?: boolean; // default: true
preview?: boolean;
toc?: boolean;
tocContainerName?: string;
menu?: boolean;
}
```
Module composition per caller:
| Module | loadMathJax | getMathpixStyleOnly | getMathpixStyle | getMathpixMarkdownStyles |
|---|:---:|:---:|:---:|:---:|
| resetBody | conditional | - | - | - |
| container | - | - | + | + |
| mathjax | - | + | + | + |
| MathpixStyle | + | + | + | + |
| code | + | + | + | - |
| tabular | + | + | + | + |
| lists | + | + | + | + |
| preview | - | - | if stylePreview | - |
| toc | + | - | if preview+toc | - |
| menu+clipboard | + | + | if stylePreview | - |
### `loadMathJax` DOM re-injection fix
Previously, if `#Mathpix-styles` already existed in the DOM, `loadMathJax()` skipped style update entirely. Now it updates `innerHTML` of the existing element.
### `useColors` propagation
Added `useColors` parameter to `loadMathJax`, `getMathpixStyleOnly`, `getMathpixStyle` — passed through to all style functions.
### `codeStyles` conversion
Converted from static string to function accepting `useColors` parameter.
### Pre-existing bug fixes
- Missing dot: `math-inline svg` → `.math-inline svg` in `@media print`
- Missing dot: `svg math-block` → `svg .math-block`
- Missing template interpolation: `#{containerName}` → `#${containerName}` in TocStyle
- Dead code: removed empty `if (showToc) {}`
### CSS output cleanup
- All style files normalized to 0/2 indentation (0 spaces for selectors, 2 spaces for properties).
- `buildStyles` refactored from string concatenation to `parts.map(s => s.trim()).join('\n')` — eliminates blank lines between CSS modules.
- Removed duplicate `overflow-x: auto` in `mjx-container` rule (was emitted both unconditionally and conditionally when `maxWidth` is set).
- Color constants extracted into `src/styles/colors.ts` (link, text, border, background, hljs, toc, menu, clipboard colors).
- `src/contex-menu/styles.ts` and `src/copy-to-clipboard/clipboard-copy-styles.ts` refactored: colors moved to constants, formatting normalized, minor CSS optimizations (`0px` → `0`, padding shorthand).
---
## Downstream Impact
- Consumers that override MMD class selectors (e.g., `#preview-main .tabular { width: 100% }`) at specificity (1,1,0) — same as the new scoped selectors. Consumer styles that load after MMD styles win by cascade order. No breakage expected.
- Consumers with their own `.math-block`/`.math-inline` SCSS — bare selectors preserved, no breakage.
- `auto-render.ts` uses `querySelectorAll('.math-inline, .math-block')` — DOM query, not affected by CSS changes.
---
## Constraints / Invariants
- HTML output class names unchanged — no downstream breakage.
- Public API method signatures changed: `scaleEquation` parameter removed (was unused). Positional callers must shift arguments. `buildStyles(opts)` available as named-parameter alternative.
- Inherited CSS properties (`font-family`, `color`, `line-height`) intentionally cascade from host into MMD content.
---
## Done When
- [x] All MMD class selectors scoped via `#preview-content`/`#setText` (or justified exception)
- [x] Defensive defaults for tables, figures, lists
- [x] `.table_tabular .tabular` replaced with ID scoping
- [x] hljs selectors scoped in styles-code.ts
- [x] lists selectors scoped in styles-lists.ts
- [x] `halpers.ts` → `helpers.ts` rename + cleanup
- [x] `index.ts` restructured into sub-functions
- [x] Dead code removed (`scaleEquation`, `.empty`, `.preview-right`)
- [x] `buildStyles(opts)` single builder with `StyleBundleOpts`
- [x] `loadMathJax` DOM re-injection fix
- [x] `useColors` propagated through all style functions
- [x] Pre-existing bugs fixed
- [x] Snapshot + composition + buildStyles tests (79 tests)
- [x] All existing tests pass
- [x] PR reviewed and merged