aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Schadt <kingdread@gmx.de>2025-12-23 15:42:25 +0100
committerDaniel Schadt <kingdread@gmx.de>2025-12-23 15:42:25 +0100
commit670e13da2b802ccb47c3805251627394eeac3523 (patch)
tree62ddd47662d98789b7582327d8c24fbdfa20a682
downloadcbp-670e13da2b802ccb47c3805251627394eeac3523.tar.gz
cbp-670e13da2b802ccb47c3805251627394eeac3523.tar.bz2
cbp-670e13da2b802ccb47c3805251627394eeac3523.zip
initial commit
-rw-r--r--cbp/__init__.py87
-rw-r--r--cbp/__pycache__/__init__.cpython-313.pycbin0 -> 4973 bytes
-rw-r--r--cbp/template.jinja2129
-rw-r--r--palettes.toml139
-rw-r--r--poetry.lock167
-rw-r--r--pyproject.toml20
6 files changed, 542 insertions, 0 deletions
diff --git a/cbp/__init__.py b/cbp/__init__.py
new file mode 100644
index 0000000..5d3ad46
--- /dev/null
+++ b/cbp/__init__.py
@@ -0,0 +1,87 @@
+import click
+import jinja2
+import markdown
+import re
+import tomllib
+from dataclasses import dataclass
+from markupsafe import Markup
+from pathlib import Path
+
+
+def to_hex(r: float) -> str:
+ as_int = int(round(r * 255))
+ return f"{as_int:02x}"
+
+
+@dataclass
+class Color:
+ r: float
+ g: float
+ b: float
+ name: str | None = None
+
+ @classmethod
+ def from_dict(cls, d):
+ if "r" in d and "g" in d and "b" in d:
+ result = cls(d["r"], d["g"], d["b"])
+ elif "R" in d and "G" in d and "B" in d:
+ result = cls(d["R"] / 255, d["G"] / 255, d["B"] / 255)
+ elif "RGB" in d:
+ match = re.match("#(..)(..)(..)", d["RGB"])
+ result = cls(
+ int(match.group(1), 16) / 255,
+ int(match.group(2), 16) / 255,
+ int(match.group(3), 16) / 255,
+ )
+ if "name" in d:
+ result.name = d["name"]
+ return result
+
+ def hex(self):
+ return f"#{to_hex(self.r)}{to_hex(self.g)}{to_hex(self.b)}"
+
+ def tuple(self):
+ return f"({self.r}, {self.g}, {self.b})"
+
+
+@dataclass
+class Palette:
+ title: str
+ description: str
+ source: str
+ colors: list[Color]
+ slug: str | None
+
+ def html_description(self):
+ return Markup(markdown.markdown(self.description))
+
+
+def load_palettes(filename: Path) -> list[Palette]:
+ with open(filename, "rb") as fo:
+ palettes = tomllib.load(fo)
+ return [
+ Palette(
+ title=p.get("title", ""),
+ description=p.get("description", ""),
+ source=p.get("source", ""),
+ slug=p.get("slug"),
+ colors=[
+ Color.from_dict(c) for c in p["colors"]
+ ],
+ )
+ for p in palettes["palette"]
+ ]
+
+
+@click.command()
+@click.option("--output", "-o", default="palettes.html", type=click.Path())
+@click.argument("palettes_path")
+def main(output, palettes_path):
+ palettes = load_palettes(Path(palettes_path))
+ env = jinja2.Environment(
+ loader=jinja2.PackageLoader("cbp", ""),
+ autoescape=True,
+ )
+ template = env.get_template("template.jinja2")
+ html = template.render({"palettes": palettes})
+ Path(output).write_text(html)
diff --git a/cbp/__pycache__/__init__.cpython-313.pyc b/cbp/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..6db70ab
--- /dev/null
+++ b/cbp/__pycache__/__init__.cpython-313.pyc
Binary files differ
diff --git a/cbp/template.jinja2 b/cbp/template.jinja2
new file mode 100644
index 0000000..24ebb75
--- /dev/null
+++ b/cbp/template.jinja2
@@ -0,0 +1,129 @@
+<html>
+ <head>
+ <title>Colorblind Palettes</title>
+<style>
+body {
+ margin-left: 5em;
+ font-family: sans-serif;
+}
+
+p {
+ max-width: 60em;
+}
+
+a {
+ text-decoration: none;
+ color: #332288;
+}
+
+a:hover {
+ color: #88ccee;
+}
+
+h2 {
+ margin-top: 5ex;
+
+ small {
+ color: #bbbbbb;
+ }
+}
+
+.palette-display {
+ display: flex;
+ gap: 1em;
+
+ textarea {
+ width: 50em;
+ height: 40ex;
+ }
+}
+
+.color-name {
+ width: 6em;
+ text-align: right;
+}
+
+.color-preview {
+ width: 50px;
+ height: 50px;
+}
+
+.color-hex {
+ font-size: 140%;
+ font-weight: 350;
+}
+</style>
+ </head>
+ <h1>Colorblind Palettes</h1>
+
+ <p>
+ There are a few resources around that have examples for colorblind-friendly palettes.
+ However, many of them are tedious to use, as they provide PDF/PNG images, or packages for programming languages.
+ This page collects some palettes in an easy-to-use/easy-to-copy manner, especially for <code>matplotlib</code>:
+ The "RGB Tuples" or hex codes can be copied verbatim as <code>color</code> for <code>matplotlib</code>'s plotting functions.
+ </p>
+
+ <p>
+ <strong>See also</strong>
+ </p>
+
+ <ul>
+ <li><a target="_blank" href="https://www.nceas.ucsb.edu/sites/default/files/2022-06/Colorblind%20Safe%20Color%20Schemes.pdf">NCEAS Science Communication Resource Corner</a></li>
+ <li><a target="_blank" href="https://sronpersonalpages.nl/~pault/data/colourschemes.pdf">Paul Tol's Colour Schemes</a></li>
+ </ul>
+
+ <p><strong>Contents</strong></p>
+
+ <ul>
+ {% for palette in palettes %}
+ <li><a href="#{{ palette.slug }}">{{ palette.title }}</a> ({{ palette.colors | length }} colors)</li>
+ {% endfor %}
+ </ul>
+
+ {% for palette in palettes %}
+ <h2>
+ <a class="anchor" name="{{ palette.slug }}" href="#{{ palette.slug }}">&para;</a>
+ {{ palette.title }}
+ <small>{{ palette.colors | length }} colors</small>
+ </h2>
+ <p><strong>Source:</strong> <a target="_blank" href="{{ palette.source }}">{{ palette.source }}</a></p>
+
+ {{ palette.html_description() }}
+
+ <div class="palette-display">
+ <div>
+ <table>
+ <thead>
+ <tr>
+ <th></th>
+ <th></th>
+ <th>Hex</th>
+ <th>RGB-Tuple</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for color in palette.colors %}
+ <tr>
+ <td class="color-name">{{ color.name | d("", true) }}</td>
+ <td style="background-color: {{ color.hex() }}" class="color-preview"></td>
+ <td class="color-hex"><code>{{ color.hex() }}</code></td>
+ <td><code>{{ color.tuple() }}</code></td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ </div>
+
+ <div>
+ <p><strong>Python (full palette)</strong></p>
+ <textarea readonly>
+[
+{%- for color in palette.colors %}
+ {{ color.tuple() }},
+{%- endfor %}
+]
+</textarea>
+ </div>
+ </div>
+ {% endfor %}
+</html>
diff --git a/palettes.toml b/palettes.toml
new file mode 100644
index 0000000..0032620
--- /dev/null
+++ b/palettes.toml
@@ -0,0 +1,139 @@
+[[palette]]
+slug = "seaborn-colorblind"
+title = "Seaborn colorblind"
+source = "https://seaborn.pydata.org/tutorial/color_palettes.html#qualitative-color-palettes"
+description = """
+Adjusted version of matplotlib's default `tab10` palette for color blindness.
+"""
+colors = [
+ {name = "blue", r = 0.00392156862745098, g = 0.45098039215686275, b = 0.6980392156862745},
+ {name = "orange", r = 0.8705882352941177, g = 0.5607843137254902, b = 0.0196078431372549},
+ {name = "green", r = 0.00784313725490196, g = 0.6196078431372549, b = 0.45098039215686275},
+ {name = "red", r = 0.8352941176470589, g = 0.3686274509803922, b = 0.0},
+ {name = "purple", r = 0.8, g = 0.47058823529411764, b = 0.7372549019607844},
+ {name = "brown", r = 0.792156862745098, g = 0.5686274509803921, b = 0.3803921568627451},
+ {name = "pink", r = 0.984313725490196, g = 0.6862745098039216, b = 0.8941176470588236},
+ {name = "gray", r = 0.5803921568627451, g = 0.5803921568627451, b = 0.5803921568627451},
+ {name = "olive", r = 0.9254901960784314, g = 0.8823529411764706, b = 0.2},
+ {name = "cyan", r = 0.33725490196078434, g = 0.7058823529411765, b = 0.9137254901960784},
+]
+
+
+[[palette]]
+slug = "paul-tol-bright"
+title = "Paul Tol's Bright"
+source = "https://sronpersonalpages.nl/~pault/data/colourschemes.pdf"
+description = """
+Bright qualitative scheme.
+Works well for plot lines and map regions, but not as text background.
+"""
+colors = [
+ {name = "blue", R = 68, G = 119, B = 170},
+ {name = "cyan", R = 102, G = 204, B = 238},
+ {name = "green", R = 34, G = 136, B = 51},
+ {name = "yellow", R = 204, G = 187, B = 68},
+ {name = "red", R = 238, G = 102, B = 119},
+ {name = "purple", R = 170, G = 51, B = 119},
+ {name = "grey", R = 187, G = 187, B = 187},
+]
+
+
+[[palette]]
+slug = "paul-tol-muted"
+title = "Paul Tol's Muted"
+source = "https://sronpersonalpages.nl/~pault/data/colourschemes.pdf"
+description = """
+Muted qualitative color scheme.
+Works well for plot lines and map regions, but not as text background.
+"""
+colors = [
+ {name = "indigo", R = 51, G = 34, B = 136},
+ {name = "cyan", R = 136, G = 204, B = 238},
+ {name = "teal", R = 68, G = 170, B = 153},
+ {name = "green", R = 17, G = 119, B = 51},
+ {name = "olive", R = 153, G = 153, B = 51},
+ {name = "sand", R = 221, G = 204, B = 119},
+ {name = "rose", R = 204, G = 102, B = 119},
+ {name = "wine", R = 136, G = 34, B = 85},
+ {name = "purple", R = 170, G = 68, B = 153},
+ {name = "pale grey", R = 221, G = 221, B = 221},
+]
+
+
+[[palette]]
+slug = "paul-tol-hc"
+title = "Paul Tol's High Contrast"
+source = "https://sronpersonalpages.nl/~pault/data/colourschemes.pdf"
+description = """
+High-contrast scheme, optimized for contrast.
+Also works when converted to grayscale.
+Technically, `"white"` (`#FFFFFF`) is also part of this scheme, but somewhat pointless to include.
+"""
+colors = [
+ {name = "yellow", R = 221, G = 170, B = 51},
+ {name = "red", R = 187, G = 85, B = 102},
+ {name = "blue", R = 0, G = 68, B = 136},
+ {name = "black", R = 0, G = 0, B = 0},
+]
+
+
+[[palette]]
+slug = "okabe-ito"
+title = "Okabe and Ito's palette"
+source = "https://jfly.uni-koeln.de/color/"
+description = """
+Masatake Okabe and Kei Ito propose this palette, which has unambiguous vivid colors with names that are easy to identify.
+"""
+colors = [
+ {name = "black", R = 0, G = 0, B = 0},
+ {name = "orange", R = 230, G = 159, B = 0},
+ {name = "sky blue", R = 86, G = 180, B = 233},
+ {name = "bluish green", R = 0, G = 158, B = 115},
+ {name = "yellow", R = 240, G = 228, B = 66},
+ {name = "blue", R = 0, G = 114, B = 178},
+ {name = "vermilion", R = 213, G = 94, B = 0},
+ {name = "reddish purple", R = 204, G = 121, B = 167},
+]
+
+
+[[palette]]
+slug = "sckolnick-5"
+title = "Sckolnick's Bright Contrast"
+source = "https://msk678.github.io/Farbenblind.html#five-colors-bright-contrasts"
+description = """
+Has only 5 colors, but a high contrast between them.
+"""
+colors = [
+ {RGB = "#D55E00"},
+ {RGB = "#CC79A7"},
+ {RGB = "#0072B2"},
+ {RGB = "#F0E442"},
+ {RGB = "#009E73"},
+]
+
+
+[[palette]]
+slug = "krzywinski"
+title = "Krzywinski Palette"
+source = "https://mk.bcgsc.ca/biovis2012/krzywinski-visualizing-biological-data.pdf"
+description = """
+This palette is quite large with 15 colors, but individual colors are not as nicely distinguishable as with other palettes that have fewer colors.
+Only use this if you *really* need 15 colors, and be prepared that there might be ambiguity (e.g., between the first two colors).
+"""
+colors = [
+ {RGB = "#000000"},
+ {RGB = "#004949"},
+ {RGB = "#009292"},
+ {RGB = "#FF6DB6"},
+ {RGB = "#FFB6DB"},
+ {RGB = "#490092"},
+ {RGB = "#006DDB"},
+ {RGB = "#B66DFF"},
+ {RGB = "#6DB6FF"},
+ {RGB = "#B6DBFF"},
+ {RGB = "#920000"},
+ {RGB = "#924900"},
+ {RGB = "#DB6D00"},
+ {RGB = "#24FF24"},
+ {RGB = "#FFFF6D"},
+]
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..a5f9092
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,167 @@
+# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
+
+[[package]]
+name = "click"
+version = "8.3.1"
+description = "Composable command line interface toolkit"
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"},
+ {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+groups = ["main"]
+markers = "platform_system == \"Windows\""
+files = [
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+
+[[package]]
+name = "jinja2"
+version = "3.1.6"
+description = "A very fast and expressive template engine."
+optional = false
+python-versions = ">=3.7"
+groups = ["main"]
+files = [
+ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"},
+ {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"},
+]
+
+[package.dependencies]
+MarkupSafe = ">=2.0"
+
+[package.extras]
+i18n = ["Babel (>=2.7)"]
+
+[[package]]
+name = "markdown"
+version = "3.10"
+description = "Python implementation of John Gruber's Markdown."
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c"},
+ {file = "markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e"},
+]
+
+[package.extras]
+docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"]
+testing = ["coverage", "pyyaml"]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.3"
+description = "Safely add untrusted strings to HTML/XML markup."
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"},
+ {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"},
+ {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"},
+ {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"},
+ {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"},
+ {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"},
+ {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"},
+ {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"},
+ {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"},
+ {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"},
+ {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"},
+ {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"},
+ {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"},
+ {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"},
+ {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"},
+ {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"},
+ {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"},
+ {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"},
+ {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"},
+ {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"},
+ {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"},
+ {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"},
+ {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"},
+ {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"},
+ {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"},
+ {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"},
+ {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"},
+ {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"},
+ {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"},
+ {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"},
+ {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"},
+ {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"},
+ {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"},
+ {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"},
+ {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"},
+ {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"},
+ {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"},
+ {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"},
+ {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"},
+ {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"},
+ {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"},
+ {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"},
+ {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"},
+ {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"},
+ {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"},
+ {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"},
+ {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"},
+ {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"},
+ {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"},
+ {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"},
+ {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"},
+ {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"},
+ {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"},
+ {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"},
+ {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"},
+ {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"},
+ {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"},
+ {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"},
+ {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"},
+ {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"},
+ {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"},
+ {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"},
+ {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"},
+ {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"},
+ {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"},
+ {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"},
+ {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"},
+]
+
+[metadata]
+lock-version = "2.1"
+python-versions = ">=3.13"
+content-hash = "079fbe62b1a65bcb19690d907978a05144a5b7ee5a2a6433fc55af053065cbb8"
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..9239890
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,20 @@
+[project]
+name = "cbp"
+version = "0.1.0"
+description = ""
+authors = [
+ {name = "Your Name",email = "you@example.com"}
+]
+requires-python = ">=3.13"
+dependencies = [
+ "jinja2 (>=3.1.6,<4.0.0)",
+ "markdown (>=3.10,<4.0)",
+ "click (>=8.3.1,<9.0.0)"
+]
+
+[project.scripts]
+cbp = "cbp:main"
+
+[build-system]
+requires = ["poetry-core>=2.0.0,<3.0.0"]
+build-backend = "poetry.core.masonry.api"