Skip to main content

Write in Jupyter Notebooks. Publish anywhere.

Project description

nb2wb

Write in notebooks. Publish anywhere.

nb2wb converts:

  • Jupyter notebooks (.ipynb)
  • Quarto documents (.qmd)
  • Markdown files (.md)

into platform-ready HTML for:

  • Substack
  • Medium
  • X Articles

The output is designed for copy/paste workflows where platforms often break MathJax and code formatting.


Why nb2wb

Most publishing editors strip or mangle:

  • LaTeX
  • code blocks
  • notebook outputs

nb2wb preserves fidelity by rendering complex parts as images and converting inline math to Unicode.

Content type Converted as
Inline math $...$ Unicode + light HTML formatting
Display math ($$...$$, \[...\], \begin{...}) PNG
Code input Syntax-highlighted PNG (or copyable text snippet)
Text outputs / errors PNG
image/png outputs Embedded as image data URI
image/svg+xml outputs Sanitized SVG as image data URI
text/html outputs Sanitized HTML fragment

Feature Overview

  • Converts .ipynb, .qmd, and .md from one CLI.
  • Platform-specific page wrappers for Substack, Medium, and X.
  • One-click copy toolbar in generated HTML.
  • Medium/X per-image copy buttons for reliable image transfer.
  • Optional --serve mode (local server + ngrok URL).
  • Equation labels and cross-references:
    • \label{...} in display math
    • \eqref{...} replaced with (N)
  • LaTeX rendering strategy:
    • tries full latex + dvipng
    • falls back to matplotlib mathtext
  • Inline LaTeX conversion pipeline:
    • unicode command replacement
    • superscript/subscript expansion
    • variable italicization
  • Code rendering controls:
    • Pygments theme
    • line numbers
    • font size
    • padding / border radius
  • Cell-level visibility and behavior tags:
    • hide-cell, hide-input, hide-output, latex-preamble, text-snippet
  • Markdown directives in .md via <!-- nb2wb: ... -->.
  • Quarto #| options mapped to notebook-style tags.
  • Security hardening for image fetching and HTML/SVG embedding.

Installation

pip install nb2wb

Development install:

git clone https://github.com/the-palindrome/nb2wb.git
cd nb2wb
pip install -e ".[dev]"

Quick Start

nb2wb notebook.ipynb
nb2wb notebook.ipynb -t medium
nb2wb notebook.ipynb -t x
nb2wb notebook.ipynb -o article.html
nb2wb notebook.ipynb --open
nb2wb notebook.ipynb --serve
nb2wb article.md
nb2wb article.md --execute
nb2wb report.qmd

Default output path is <input_basename>.html.


CLI Reference

nb2wb <input.{ipynb|qmd|md}> [options]
Option Meaning
-t, --target {substack,medium,x} Target platform (substack default)
-c, --config PATH YAML config file
-o, --output PATH Output HTML path
--open Open generated HTML in browser
--serve Extract images, start local server, expose via ngrok
--execute Execute code cells for .md files (ignored for .qmd, which are always executed)

Platform Behavior

Platform Paste workflow Image behavior
Substack One-click copy/paste Embedded images transfer directly
Medium Copy/paste + optional per-image copy Base64 images may be stripped by editor
X Articles Copy/paste + optional per-image copy Base64 images may be stripped by editor

--serve mode

--serve helps Medium/X workflows by replacing embedded data URIs with public HTTP URLs.

What it does:

  1. Extracts supported image MIME types from the generated HTML into images/
  2. Rewrites <img src="..."> to those files
  3. Starts local HTTP server
  4. Starts ngrok tunnel and opens the public URL

Requirements:

  • ngrok installed
  • ngrok authenticated (ngrok config add-authtoken <TOKEN>)

Input Format Support

.ipynb

  • Uses notebook cells and outputs directly.
  • Supports notebook tags in cell.metadata.tags.
  • Uses notebook/kernel metadata to infer language for syntax highlighting.

.md

Supported features:

  • Optional YAML front matter
  • Fenced code blocks with backticks or tildes
  • Per-fence tags (```python hide-input style)
  • Directive comments:
    • <!-- nb2wb: hide-input -->
    • <!-- nb2wb: hide-output -->
    • <!-- nb2wb: hide-cell -->
    • <!-- nb2wb: text-snippet -->
    • comma-separated combinations are supported
  • Special fence language:
    • latex-preamble

Execution:

  • Not executed by default
  • Executed when --execute is provided

.qmd

Supported features:

  • Optional YAML front matter
  • Quarto fenced chunks (```{python} etc.)
  • Quarto options mapped to tags:
    • #| echo: false -> hide-input
    • #| output: false -> hide-output
    • #| include: false or #| eval: false -> hide-cell
    • #| tags: [tag1, tag2] -> tags
  • Special chunk languages:
    • latex-preamble
    • output (attaches stdout to the immediately preceding code cell)

Execution:

  • .qmd code execution is always attempted by design

Cell Tags

Tag Effect
hide-cell Hide entire cell (input + output)
hide-input Hide code input
hide-output Hide outputs
latex-preamble Use cell/chunk content as LaTeX preamble and hide it
text-snippet Render code as <pre><code> instead of PNG

hide-cell applies to markdown cells too.


LaTeX Features

Display math

Detected forms include:

  • $$...$$
  • \[...\]
  • \begin{equation}...\end{equation}
  • \begin{align}...\end{align}
  • \begin{gather}...\end{gather}
  • \begin{multline}...\end{multline}
  • \begin{eqnarray}...\end{eqnarray}
  • starred variants where applicable

Equation numbering and references

  • \label{eq:name} assigns equation number
  • \eqref{eq:name} is replaced with (N) across the document

Inline math

Inline $...$ expressions are converted to Unicode-oriented text with script handling.


LaTeX Preamble Sources

All of these are combined:

  1. Config (latex.preamble)
  2. Notebook markdown cells tagged latex-preamble
  3. .md fenced blocks labeled latex-preamble
  4. .qmd chunks {latex-preamble}

Note:

  • preamble only affects full LaTeX (try_usetex: true path)
  • matplotlib mathtext fallback ignores custom preamble

Output Rendering Details

For code cells:

  • Source code -> syntax-highlighted PNG
  • Footer includes execution count and language label
  • Text output / tracebacks -> muted output PNG block

Rich outputs:

  • image/png -> embedded directly
  • image/svg+xml -> sanitized and embedded as data:image/svg+xml;base64,...
  • text/html -> sanitized HTML fragment

Raw notebook cells are skipped.


Configuration

Pass with:

nb2wb notebook.ipynb -c config.yaml

Complete config schema (with defaults)

# Global defaults
image_width: 1920
border_radius: 14

code:
  font_size: 48
  theme: "monokai"
  line_numbers: true
  font: "DejaVu Sans Mono"
  image_width: 1920
  padding_x: 100
  padding_y: 100
  separator: 0
  background: ""       # empty = use theme background
  border_radius: 14

latex:
  font_size: 48
  dpi: 150
  color: "black"
  background: "white"
  padding: 68          # pixels
  image_width: 1920
  try_usetex: true
  preamble: ""
  border_radius: 0

Inheritance behavior:

  • code.image_width and latex.image_width inherit top-level image_width unless overridden
  • code.border_radius and latex.border_radius inherit top-level border_radius unless overridden

Platform defaults applied automatically

When target is medium or x, defaults are adjusted for narrower layouts:

  • top-level image_width: 680
  • code.font_size: 42
  • code.image_width: 1200
  • code.padding_x: 30
  • code.padding_y: 30
  • code.separator: 0
  • latex.font_size: 35
  • latex.padding: 50
  • latex.image_width: 1200

Substack keeps base defaults unless your config overrides them.

Code themes

Any Pygments style works. Example:

python -c "from pygments.styles import get_all_styles; print(sorted(get_all_styles()))"

Security Model

nb2wb includes guardrails for image ingestion and HTML embedding:

Image URL/file safety

  • Only http / https remote image URLs are allowed
  • Requests to private/loopback hosts are blocked (SSRF protection)
  • Redirect targets are re-validated (no public-to-private redirect bypass)
  • Download timeout and max size checks are enforced
  • MIME type allowlist is enforced for fetched/read images
  • Local image paths:
    • absolute paths rejected
    • .. traversal rejected
    • symlink escape outside current working directory rejected

Embedded content sanitization

  • Markdown-generated HTML is sanitized before embedding
  • text/html outputs are sanitized
  • SVG outputs are sanitized, then embedded via image data URI
  • Dangerous tags/attributes/URI schemes are stripped or neutralized

Important:

  • Notebook execution (.qmd or --execute) runs code. Treat untrusted notebooks as untrusted code.
  • Sanitization is best-effort, not a browser sandbox.

Requirements

Core dependencies:

  • Python >=3.9
  • nbformat
  • nbconvert
  • ipykernel
  • matplotlib
  • Pillow
  • Pygments
  • PyYAML
  • markdown
  • unicodeit

Optional system tools:

  • LaTeX + dvipng (for highest-fidelity display math rendering)
  • ngrok (for --serve)

Development

Run tests:

pytest
pytest tests/unit/
pytest tests/integration/
pytest tests/workflow/

Format:

black nb2wb tests
isort nb2wb tests

Limitations

  • Platforms can change paste behavior without notice.
  • Medium/X may still require per-image copy depending editor behavior.
  • Extremely complex custom HTML can be altered by sanitization.
  • .qmd execution requires a working Jupyter kernel setup.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nb2wb-0.1.2.tar.gz (40.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nb2wb-0.1.2-py3-none-any.whl (45.9 kB view details)

Uploaded Python 3

File details

Details for the file nb2wb-0.1.2.tar.gz.

File metadata

  • Download URL: nb2wb-0.1.2.tar.gz
  • Upload date:
  • Size: 40.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for nb2wb-0.1.2.tar.gz
Algorithm Hash digest
SHA256 6c21572a2e7e5317c02bdc62453e08cfda9feb6adac5292e0509daaa5400e0c3
MD5 d2eb07aa06227cb333ee67a1c65aa596
BLAKE2b-256 953e2ed1fffbdf606ebc48097c104b5708ddfd910cc37961e7c24719b9f8c313

See more details on using hashes here.

Provenance

The following attestation bundles were made for nb2wb-0.1.2.tar.gz:

Publisher: publish.yml on the-palindrome/nb2wb

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file nb2wb-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: nb2wb-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 45.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for nb2wb-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 1f1d6be72d5690700f78a6e152cc76658ad558fa13694e706f637614099fcebd
MD5 d5ca4910f1d5552e05b315e30060a6d7
BLAKE2b-256 e8e8efafc91e3cdf285508133f9b71e86fc5783bb5069c26a2cae0a3b8d3dcb9

See more details on using hashes here.

Provenance

The following attestation bundles were made for nb2wb-0.1.2-py3-none-any.whl:

Publisher: publish.yml on the-palindrome/nb2wb

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page