markdown-badges 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- markdown_badges/__init__.py +161 -0
- markdown_badges/catalogue.py +163 -0
- markdown_badges/parsing.py +132 -0
- markdown_badges/py.typed +0 -0
- markdown_badges/styling.py +94 -0
- markdown_badges-1.0.0.dist-info/METADATA +207 -0
- markdown_badges-1.0.0.dist-info/RECORD +9 -0
- markdown_badges-1.0.0.dist-info/WHEEL +4 -0
- markdown_badges-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Badges for Markdown.
|
|
2
|
+
|
|
3
|
+
Write ``!<name>`` anywhere (prose, headings, table cells, list items) and it
|
|
4
|
+
renders as a small inline pill. Names come from a catalogue that ships with the
|
|
5
|
+
package, grouped into three types: ``priority`` badges carry a severity rank,
|
|
6
|
+
``status`` badges say where an item sits in a workflow, and ``branding`` badges
|
|
7
|
+
carry a logo inlined as a ``data:`` URI.
|
|
8
|
+
|
|
9
|
+
The whole catalogue is active out of the box. Narrow it with ``catalogue``, add
|
|
10
|
+
or recolour entries with ``badges``, and opt into a task-list shorthand with
|
|
11
|
+
``shorthand``. A badge value is written into the badge's ``style`` attribute as
|
|
12
|
+
its ``background-color``, so it may carry further CSS declarations after a
|
|
13
|
+
``;``. The text colour is derived from the leading colour.
|
|
14
|
+
|
|
15
|
+
A keyword is left literal when escaped (``\\!high``) or written inside a code
|
|
16
|
+
span, because the inline processor is registered below Python-Markdown's own
|
|
17
|
+
``escape`` and ``backtick`` patterns.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from collections.abc import Mapping
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from markdown import Extension, Markdown
|
|
24
|
+
|
|
25
|
+
from markdown_badges.catalogue import CATALOGUE, Badge, BadgeType, catalogue_for
|
|
26
|
+
from markdown_badges.parsing import (
|
|
27
|
+
INLINE_PRIORITY,
|
|
28
|
+
TREE_PRIORITY,
|
|
29
|
+
BadgeInlineProcessor,
|
|
30
|
+
ShorthandTreeprocessor,
|
|
31
|
+
badges_in,
|
|
32
|
+
inline_re,
|
|
33
|
+
priority_of,
|
|
34
|
+
rank_of,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"CATALOGUE",
|
|
39
|
+
"Badge",
|
|
40
|
+
"BadgeType",
|
|
41
|
+
"MarkdownBadgesExtension",
|
|
42
|
+
"badges_in",
|
|
43
|
+
"catalogue_for",
|
|
44
|
+
"makeExtension",
|
|
45
|
+
"priority_of",
|
|
46
|
+
"rank_of",
|
|
47
|
+
"resolve_badges",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
_TYPE_NAMES = ", ".join(t.value for t in BadgeType)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _badge_type(name: str, where: str) -> BadgeType:
|
|
54
|
+
"""The BadgeType called `name`, or a ValueError naming the valid ones."""
|
|
55
|
+
try:
|
|
56
|
+
return BadgeType(name)
|
|
57
|
+
except ValueError:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
f"markdown-badges: {where} names an unknown badge type {name!r}; "
|
|
60
|
+
f"valid types are {_TYPE_NAMES}"
|
|
61
|
+
) from None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _check_value(name: str, value: Any) -> str:
|
|
65
|
+
"""The badge value as a string, or a ValueError. Content is not restricted:
|
|
66
|
+
a value may extend the badge's declaration list past the first `;`."""
|
|
67
|
+
if not isinstance(value, str):
|
|
68
|
+
raise ValueError(
|
|
69
|
+
f"markdown-badges: badge {name!r} has a non-string value {value!r}; "
|
|
70
|
+
"give it a CSS colour string instead, for example '#7b1fa2'"
|
|
71
|
+
)
|
|
72
|
+
if not value.strip():
|
|
73
|
+
raise ValueError(
|
|
74
|
+
f"markdown-badges: badge {name!r} has an empty value; "
|
|
75
|
+
"give it a CSS colour string instead, for example '#7b1fa2'"
|
|
76
|
+
)
|
|
77
|
+
return value
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def resolve_badges(
|
|
81
|
+
catalogue_scope: list[str], user_badges: Mapping[str, Mapping[str, Any]]
|
|
82
|
+
) -> dict[str, Badge]:
|
|
83
|
+
"""The active badge map: the scoped catalogue with `user_badges` merged over.
|
|
84
|
+
|
|
85
|
+
A user name that already exists is replaced in place, keeping its position
|
|
86
|
+
and its catalogue type. A new name is inserted after the last badge of the
|
|
87
|
+
same type, so a new priority outranks every catalogue priority."""
|
|
88
|
+
scoped = [_badge_type(n, "catalogue") for n in catalogue_scope]
|
|
89
|
+
ordered: list[Badge] = list(catalogue_for(*scoped).values()) if scoped else []
|
|
90
|
+
|
|
91
|
+
for type_name, entries in user_badges.items():
|
|
92
|
+
badge_type = _badge_type(type_name, "badges")
|
|
93
|
+
if not isinstance(entries, Mapping):
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"markdown-badges: badges[{type_name!r}] is {entries!r}, not a table; "
|
|
96
|
+
"it must map badge name to value, for example {'blocker': '#7b1fa2'}"
|
|
97
|
+
)
|
|
98
|
+
for name, raw in entries.items():
|
|
99
|
+
value = _check_value(name, raw)
|
|
100
|
+
position = next((i for i, b in enumerate(ordered) if b.name == name), None)
|
|
101
|
+
if position is not None:
|
|
102
|
+
kept = ordered[position]
|
|
103
|
+
ordered[position] = Badge(name, value, kept.type, kept.note)
|
|
104
|
+
continue
|
|
105
|
+
last = max((i for i, b in enumerate(ordered) if b.type is badge_type), default=None)
|
|
106
|
+
new = Badge(name, value, badge_type)
|
|
107
|
+
if last is None:
|
|
108
|
+
ordered.append(new)
|
|
109
|
+
else:
|
|
110
|
+
ordered.insert(last + 1, new)
|
|
111
|
+
|
|
112
|
+
return {b.name: b for b in ordered}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class MarkdownBadgesExtension(Extension):
|
|
116
|
+
"""Registers the inline `!<name>` keyword and the optional shorthand."""
|
|
117
|
+
|
|
118
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
119
|
+
self.config = {
|
|
120
|
+
"catalogue": [
|
|
121
|
+
[t.value for t in BadgeType],
|
|
122
|
+
"Badge types to load from the shipped catalogue; [] disables it",
|
|
123
|
+
],
|
|
124
|
+
"badges": [{}, "Extra or recoloured badges, keyed by badge type"],
|
|
125
|
+
"shorthand": [{}, "Task-list marker -> badge name"],
|
|
126
|
+
}
|
|
127
|
+
if "levels" in kwargs:
|
|
128
|
+
raise ValueError(
|
|
129
|
+
"markdown-badges: the 'levels' option was removed in 1.0. Declare priority "
|
|
130
|
+
"badges under badges.priority instead, for example "
|
|
131
|
+
"badges={'priority': {'blocker': '#7b1fa2'}}. See MIGRATING.md."
|
|
132
|
+
)
|
|
133
|
+
super().__init__(**kwargs)
|
|
134
|
+
|
|
135
|
+
def extendMarkdown(self, md: Markdown) -> None:
|
|
136
|
+
badges = resolve_badges(
|
|
137
|
+
list(self.getConfig("catalogue", [])), dict(self.getConfig("badges", {}) or {})
|
|
138
|
+
)
|
|
139
|
+
shorthand: dict[str, Badge] = {}
|
|
140
|
+
for marker, name in (self.getConfig("shorthand", {}) or {}).items():
|
|
141
|
+
if name not in badges:
|
|
142
|
+
raise ValueError(
|
|
143
|
+
f"markdown-badges: shorthand {marker!r} points at badge {name!r}, "
|
|
144
|
+
"which is not in scope; add it under badges, or widen catalogue"
|
|
145
|
+
)
|
|
146
|
+
shorthand[marker] = badges[name]
|
|
147
|
+
|
|
148
|
+
if shorthand:
|
|
149
|
+
md.treeprocessors.register(
|
|
150
|
+
ShorthandTreeprocessor(md, shorthand), "badges-shorthand", TREE_PRIORITY
|
|
151
|
+
)
|
|
152
|
+
if badges:
|
|
153
|
+
md.inlinePatterns.register(
|
|
154
|
+
BadgeInlineProcessor(inline_re(list(badges)), md, badges),
|
|
155
|
+
"badges-inline",
|
|
156
|
+
INLINE_PRIORITY,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def makeExtension(**kwargs: Any) -> MarkdownBadgesExtension:
|
|
161
|
+
return MarkdownBadgesExtension(**kwargs)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""The badge catalogue: the data every install gets without any config.
|
|
2
|
+
|
|
3
|
+
To add a badge, append a `Badge` to `CATALOGUE`. A branding badge needs its
|
|
4
|
+
logo as a single-path SVG in `ICONS`; pick a base colour and an icon fill that
|
|
5
|
+
agree, because the badge text colour is derived from the base.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import math
|
|
9
|
+
import urllib.parse
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from enum import Enum
|
|
12
|
+
|
|
13
|
+
__all__ = ["CATALOGUE", "Badge", "BadgeType", "catalogue_for", "claude_burst"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BadgeType(Enum):
|
|
17
|
+
"""What a badge means. Only PRIORITY badges carry a severity rank."""
|
|
18
|
+
|
|
19
|
+
PRIORITY = "priority"
|
|
20
|
+
STATUS = "status"
|
|
21
|
+
BRANDING = "branding"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class Badge:
|
|
26
|
+
"""One badge: a keyword, the CSS it renders with, and what it means."""
|
|
27
|
+
|
|
28
|
+
name: str
|
|
29
|
+
value: str
|
|
30
|
+
type: BadgeType
|
|
31
|
+
note: str = ""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def claude_burst(rays: int = 11, r_out: float = 11.0, r_in: float = 1.0) -> str:
|
|
35
|
+
"""The Claude starburst: `rays` tapered spokes around a common center."""
|
|
36
|
+
w_out, w_in, cx, cy = 1.15, 2.05, 12.0, 12.0
|
|
37
|
+
parts: list[str] = []
|
|
38
|
+
for i in range(rays):
|
|
39
|
+
angle = 2 * math.pi * i / rays - math.pi / 2
|
|
40
|
+
ca, sa = math.cos(angle), math.sin(angle)
|
|
41
|
+
px, py = -sa, ca
|
|
42
|
+
corners = [
|
|
43
|
+
(cx + ca * r_in + px * w_in / 2, cy + sa * r_in + py * w_in / 2),
|
|
44
|
+
(cx + ca * r_out + px * w_out / 2, cy + sa * r_out + py * w_out / 2),
|
|
45
|
+
(cx + ca * r_out - px * w_out / 2, cy + sa * r_out - py * w_out / 2),
|
|
46
|
+
(cx + ca * r_in - px * w_in / 2, cy + sa * r_in - py * w_in / 2),
|
|
47
|
+
]
|
|
48
|
+
parts.append("M" + "L".join(f"{x:.2f} {y:.2f}" for x, y in corners) + "Z")
|
|
49
|
+
return "".join(parts)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Single-path logo marks on a 24x24 viewBox. Copy the existing values verbatim
|
|
53
|
+
# from scripts/gen_badges.py on this branch: ICONS["gitlab"] and ICONS["github"].
|
|
54
|
+
ICONS: dict[str, str] = {
|
|
55
|
+
"gitlab": (
|
|
56
|
+
"M23.955 13.587l-1.342-4.135-2.664-8.189a.455.455 0 00-.867 0L16.418 9.45H7.582"
|
|
57
|
+
"L4.919 1.263a.455.455 0 00-.867 0L1.386 9.45.044 13.587a.924.924 0 00.331 1.03"
|
|
58
|
+
"L12 23.054l11.625-8.436a.92.92 0 00.33-1.031"
|
|
59
|
+
),
|
|
60
|
+
"github": (
|
|
61
|
+
"M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82"
|
|
62
|
+
"-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633"
|
|
63
|
+
" 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236"
|
|
64
|
+
" 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332"
|
|
65
|
+
"-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322"
|
|
66
|
+
" 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23"
|
|
67
|
+
" 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805"
|
|
68
|
+
" 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69"
|
|
69
|
+
".825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
|
70
|
+
),
|
|
71
|
+
"claude": claude_burst(),
|
|
72
|
+
"docker": (
|
|
73
|
+
"M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a"
|
|
74
|
+
".185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00"
|
|
75
|
+
".186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102"
|
|
76
|
+
".082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h"
|
|
77
|
+
"-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0"
|
|
78
|
+
" 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102"
|
|
79
|
+
".083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185"
|
|
80
|
+
"H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186"
|
|
81
|
+
".186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888"
|
|
82
|
+
"c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184"
|
|
83
|
+
"-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a"
|
|
84
|
+
".185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1"
|
|
85
|
+
".887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00"
|
|
86
|
+
"-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c"
|
|
87
|
+
"-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-"
|
|
88
|
+
"2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595"
|
|
89
|
+
".332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c"
|
|
90
|
+
".545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 "
|
|
91
|
+
"2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418"
|
|
92
|
+
" 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046"
|
|
93
|
+
"l.098-.288Z"
|
|
94
|
+
),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Shared geometry for an icon badge: the mark sits left of the text.
|
|
98
|
+
_ICON_LAYOUT = (
|
|
99
|
+
"background-repeat:no-repeat;background-position:0.45em center;"
|
|
100
|
+
"background-size:0.8em;padding-left:1.75em"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _icon_value(color: str, icon: str, fill: str = "#fff") -> str:
|
|
105
|
+
"""A badge value whose background carries `icon` as an inline data: URI."""
|
|
106
|
+
svg = (
|
|
107
|
+
f"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' "
|
|
108
|
+
f"fill='{fill}'><path d='{ICONS[icon]}'/></svg>"
|
|
109
|
+
)
|
|
110
|
+
uri = "data:image/svg+xml," + urllib.parse.quote(svg, safe="")
|
|
111
|
+
return f"{color};background-image:url('{uri}');{_ICON_LAYOUT}"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
CATALOGUE: tuple[Badge, ...] = (
|
|
115
|
+
# Priority, in ascending severity order. Rank is position in this run.
|
|
116
|
+
Badge("trivial", "#78909c", BadgeType.PRIORITY, "Nice to have."),
|
|
117
|
+
Badge("low", "#2e7d32", BadgeType.PRIORITY, "Green."),
|
|
118
|
+
Badge("medium", "#f9a825", BadgeType.PRIORITY, "Amber."),
|
|
119
|
+
Badge("high", "#ef6c00", BadgeType.PRIORITY, "Orange."),
|
|
120
|
+
Badge("critical", "#d32f2f", BadgeType.PRIORITY, "Red."),
|
|
121
|
+
Badge("blocker", "#7b1fa2", BadgeType.PRIORITY, "Work that cannot start."),
|
|
122
|
+
# Status: where an item sits in a workflow.
|
|
123
|
+
Badge("todo", "#1565c0", BadgeType.STATUS, "Not started."),
|
|
124
|
+
Badge("wip", "#0277bd", BadgeType.STATUS, "In progress."),
|
|
125
|
+
Badge("review", "#6a1b9a", BadgeType.STATUS, "Waiting on a reviewer."),
|
|
126
|
+
Badge("blocked", "#b71c1c", BadgeType.STATUS, "Waiting on someone else."),
|
|
127
|
+
Badge("approved", "#2e7d32", BadgeType.STATUS, "Signed off, not yet shipped."),
|
|
128
|
+
Badge("done", "#37474f", BadgeType.STATUS, "Finished."),
|
|
129
|
+
Badge("onhold", "#8d6e63", BadgeType.STATUS, "Paused on purpose."),
|
|
130
|
+
Badge("experimental", "#00838f", BadgeType.STATUS, "Not stable yet."),
|
|
131
|
+
Badge("deprecated", "#5d4037", BadgeType.STATUS, "On the way out."),
|
|
132
|
+
# Branding: a logo inlined as a data: URI, so no network request.
|
|
133
|
+
Badge("gitlab", _icon_value("#7759c2", "gitlab"), BadgeType.BRANDING, "GitLab purple."),
|
|
134
|
+
Badge("github", _icon_value("#181717", "github"), BadgeType.BRANDING, "GitHub near-black."),
|
|
135
|
+
Badge(
|
|
136
|
+
"claude",
|
|
137
|
+
_icon_value("#d97757", "claude", fill="#1f1e1d"),
|
|
138
|
+
BadgeType.BRANDING,
|
|
139
|
+
"Claude coral, dark mark.",
|
|
140
|
+
),
|
|
141
|
+
Badge(
|
|
142
|
+
"docker",
|
|
143
|
+
_icon_value("#1d63ed", "docker"),
|
|
144
|
+
BadgeType.BRANDING,
|
|
145
|
+
"Docker blue, white whale.",
|
|
146
|
+
),
|
|
147
|
+
# AWS has no icon: simple-icons carries no Amazon marks (only a wordmark "aws"),
|
|
148
|
+
# and the badge text already renders AWS, so a wordmark would read "aws aws".
|
|
149
|
+
Badge(
|
|
150
|
+
"aws",
|
|
151
|
+
"#232f3e",
|
|
152
|
+
BadgeType.BRANDING,
|
|
153
|
+
"AWS squid ink. No mark.",
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def catalogue_for(*types: BadgeType) -> dict[str, Badge]:
|
|
159
|
+
"""The catalogue, filtered to `types`, in catalogue order.
|
|
160
|
+
|
|
161
|
+
No arguments means every type."""
|
|
162
|
+
wanted = set(types) if types else set(BadgeType)
|
|
163
|
+
return {b.name: b for b in CATALOGUE if b.type in wanted}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Finding badge keywords in text, and rendering them inline."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import xml.etree.ElementTree as etree
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
|
|
7
|
+
from markdown import Markdown
|
|
8
|
+
from markdown.inlinepatterns import InlineProcessor
|
|
9
|
+
from markdown.treeprocessors import Treeprocessor
|
|
10
|
+
|
|
11
|
+
from markdown_badges.catalogue import Badge, BadgeType, catalogue_for
|
|
12
|
+
from markdown_badges.styling import badge_element, badge_html
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"INLINE_PRIORITY",
|
|
16
|
+
"TREE_PRIORITY",
|
|
17
|
+
"BadgeInlineProcessor",
|
|
18
|
+
"ShorthandTreeprocessor",
|
|
19
|
+
"badges_in",
|
|
20
|
+
"inline_re",
|
|
21
|
+
"priority_of",
|
|
22
|
+
"rank_of",
|
|
23
|
+
"shorthand_re",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
# Below Python-Markdown's `escape` (180), so `\!high` stays literal, and below
|
|
27
|
+
# `backtick` (190), so a keyword inside a code span survives verbatim.
|
|
28
|
+
INLINE_PRIORITY = 175
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def inline_re(names: Sequence[str]) -> str:
|
|
32
|
+
"""`!<name>` regex for `names`: not preceded by a word char or another `!`,
|
|
33
|
+
ending on a word boundary. Longer names first so none shadows a longer one."""
|
|
34
|
+
alts = "|".join(re.escape(n) for n in sorted(names, key=len, reverse=True))
|
|
35
|
+
return rf"(?<![\w!])!({alts})\b"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _resolve(badges: Mapping[str, Badge] | None) -> Mapping[str, Badge]:
|
|
39
|
+
return catalogue_for() if badges is None else badges
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _priority_names(badges: Mapping[str, Badge]) -> list[str]:
|
|
43
|
+
return [b.name for b in badges.values() if b.type is BadgeType.PRIORITY]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def rank_of(name: str, badges: Mapping[str, Badge] | None = None) -> int:
|
|
47
|
+
"""Severity rank of `name` among the priority badges of `badges`, or -1.
|
|
48
|
+
|
|
49
|
+
A status or branding badge has no rank and returns -1."""
|
|
50
|
+
names = _priority_names(_resolve(badges))
|
|
51
|
+
return names.index(name) if name in names else -1
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def badges_in(text: str, badges: Mapping[str, Badge] | None = None) -> list[Badge]:
|
|
55
|
+
"""Every badge keyword found in `text`, of any type, in document order.
|
|
56
|
+
|
|
57
|
+
This is a plain-text scan, not a Markdown parse: unlike the rendered badge,
|
|
58
|
+
a keyword inside a code span or escaped as `\\!high` still counts."""
|
|
59
|
+
resolved = _resolve(badges)
|
|
60
|
+
if not resolved:
|
|
61
|
+
return []
|
|
62
|
+
pattern = inline_re(list(resolved))
|
|
63
|
+
return [resolved[m.group(1)] for m in re.finditer(pattern, text)]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def priority_of(text: str, badges: Mapping[str, Badge] | None = None) -> str | None:
|
|
67
|
+
"""The highest-ranked priority badge name in `text`, or None.
|
|
68
|
+
|
|
69
|
+
Status and branding badges are ignored. Same plain-text caveat as
|
|
70
|
+
`badges_in`."""
|
|
71
|
+
resolved = _resolve(badges)
|
|
72
|
+
found = [b.name for b in badges_in(text, resolved) if b.type is BadgeType.PRIORITY]
|
|
73
|
+
if not found:
|
|
74
|
+
return None
|
|
75
|
+
return max(found, key=lambda name: rank_of(name, resolved))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class BadgeInlineProcessor(InlineProcessor):
|
|
79
|
+
"""Render an inline `!<name>` keyword as a badge span."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, pattern: str, md: Markdown, badges: Mapping[str, Badge]) -> None:
|
|
82
|
+
super().__init__(pattern, md)
|
|
83
|
+
self.badges = badges
|
|
84
|
+
|
|
85
|
+
# The stub types `handleMatch` on the legacy one-argument `Pattern` base,
|
|
86
|
+
# so the correct two-argument InlineProcessor signature needs the ignore.
|
|
87
|
+
def handleMatch( # type: ignore[override]
|
|
88
|
+
self, m: re.Match[str], data: str
|
|
89
|
+
) -> tuple[etree.Element, int, int]:
|
|
90
|
+
return badge_element(self.badges[m.group(1)]), m.start(0), m.end(0)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# Above pymdownx.tasklist (25), so the marker is read from pristine
|
|
94
|
+
# `[ ] <marker> text` before tasklist turns it into a checkbox.
|
|
95
|
+
TREE_PRIORITY = 26
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def shorthand_re(markers: Sequence[str]) -> re.Pattern[str]:
|
|
99
|
+
"""Checkbox prefix, one configured marker, then required whitespace.
|
|
100
|
+
|
|
101
|
+
Markers are matched longest-first, so `!!` wins over `!`."""
|
|
102
|
+
alts = "|".join(re.escape(m) for m in sorted(markers, key=len, reverse=True))
|
|
103
|
+
return re.compile(
|
|
104
|
+
rf"^(?P<checkbox> *\[(?:x|X| )\] +)(?P<marker>{alts})\s+(?P<rest>.*)", re.DOTALL
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ShorthandTreeprocessor(Treeprocessor):
|
|
109
|
+
"""Rewrite a task-list item whose text starts with a configured marker."""
|
|
110
|
+
|
|
111
|
+
def __init__(self, md: Markdown, markers: Mapping[str, Badge]) -> None:
|
|
112
|
+
super().__init__(md)
|
|
113
|
+
self.markers = markers
|
|
114
|
+
self.pattern = shorthand_re(list(markers))
|
|
115
|
+
|
|
116
|
+
def _rewrite(self, holder: etree.Element) -> bool:
|
|
117
|
+
m = self.pattern.match(holder.text or "")
|
|
118
|
+
if m is None:
|
|
119
|
+
return False
|
|
120
|
+
badge = self.md.htmlStash.store(badge_html(self.markers[m.group("marker")]))
|
|
121
|
+
holder.text = m.group("checkbox") + badge + m.group("rest")
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
def run(self, root: etree.Element) -> None:
|
|
125
|
+
for li in root.iter("li"):
|
|
126
|
+
if self._rewrite(li):
|
|
127
|
+
continue
|
|
128
|
+
# Loose lists wrap the checkbox text in a child <p>.
|
|
129
|
+
if len(li):
|
|
130
|
+
first = next(iter(li))
|
|
131
|
+
if first.tag == "p":
|
|
132
|
+
self._rewrite(first)
|
markdown_badges/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Badge appearance: colour resolution and the rendered <span>."""
|
|
2
|
+
|
|
3
|
+
import xml.etree.ElementTree as etree
|
|
4
|
+
|
|
5
|
+
from markdown_badges.catalogue import Badge
|
|
6
|
+
|
|
7
|
+
__all__ = ["BADGE_STYLE", "badge_element", "badge_html", "text_color", "to_hex6"]
|
|
8
|
+
|
|
9
|
+
# Shared pill geometry. The per-badge value and text colour are appended.
|
|
10
|
+
BADGE_STYLE = (
|
|
11
|
+
"display:inline-block;padding:0.05em 0.45em;margin-right:0.15em;"
|
|
12
|
+
"border-radius:0.35em;font-size:0.62em;font-weight:700;line-height:1.5;"
|
|
13
|
+
"letter-spacing:0.04em;text-transform:uppercase;vertical-align:middle;"
|
|
14
|
+
"-webkit-user-select:none;user-select:none;"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Common CSS named colours, so a badge value can use a name and still get an
|
|
18
|
+
# auto-contrasted text colour. Anything unresolvable falls back to white text.
|
|
19
|
+
_NAMED_COLORS = {
|
|
20
|
+
"black": "#000000",
|
|
21
|
+
"white": "#ffffff",
|
|
22
|
+
"gray": "#808080",
|
|
23
|
+
"grey": "#808080",
|
|
24
|
+
"silver": "#c0c0c0",
|
|
25
|
+
"red": "#ff0000",
|
|
26
|
+
"maroon": "#800000",
|
|
27
|
+
"orange": "#ffa500",
|
|
28
|
+
"yellow": "#ffff00",
|
|
29
|
+
"olive": "#808000",
|
|
30
|
+
"lime": "#00ff00",
|
|
31
|
+
"green": "#008000",
|
|
32
|
+
"teal": "#008080",
|
|
33
|
+
"aqua": "#00ffff",
|
|
34
|
+
"cyan": "#00ffff",
|
|
35
|
+
"blue": "#0000ff",
|
|
36
|
+
"navy": "#000080",
|
|
37
|
+
"purple": "#800080",
|
|
38
|
+
"fuchsia": "#ff00ff",
|
|
39
|
+
"magenta": "#ff00ff",
|
|
40
|
+
"rebeccapurple": "#663399",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def to_hex6(color: str) -> str | None:
|
|
45
|
+
"""Normalize a CSS colour to six hex digits, or None if unresolvable.
|
|
46
|
+
|
|
47
|
+
Accepts 3-/4-/6-/8-digit hex (any alpha channel is dropped) and the common
|
|
48
|
+
named colours. A value carrying extra CSS declarations is read up to the
|
|
49
|
+
first `;`, which is its `background-color`."""
|
|
50
|
+
c = color.split(";", 1)[0].strip().lower()
|
|
51
|
+
c = _NAMED_COLORS.get(c, c)
|
|
52
|
+
if not c.startswith("#"):
|
|
53
|
+
return None
|
|
54
|
+
h = c[1:]
|
|
55
|
+
if not h or not all(ch in "0123456789abcdef" for ch in h):
|
|
56
|
+
return None
|
|
57
|
+
if len(h) in (3, 4):
|
|
58
|
+
h = "".join(ch * 2 for ch in h)
|
|
59
|
+
if len(h) in (6, 8):
|
|
60
|
+
return h[:6]
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def text_color(bg: str) -> str:
|
|
65
|
+
"""Black or white, whichever has the higher WCAG contrast against `bg`."""
|
|
66
|
+
hex6 = to_hex6(bg)
|
|
67
|
+
if hex6 is None:
|
|
68
|
+
return "#fff"
|
|
69
|
+
r, g, b = (int(hex6[i : i + 2], 16) / 255 for i in (0, 2, 4))
|
|
70
|
+
|
|
71
|
+
def lin(c: float) -> float:
|
|
72
|
+
return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
|
|
73
|
+
|
|
74
|
+
lum = 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b)
|
|
75
|
+
contrast_black = (lum + 0.05) / 0.05
|
|
76
|
+
contrast_white = 1.05 / (lum + 0.05)
|
|
77
|
+
return "#000" if contrast_black >= contrast_white else "#fff"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def badge_element(badge: Badge) -> etree.Element:
|
|
81
|
+
"""The badge as an inline <span>.
|
|
82
|
+
|
|
83
|
+
`badge.value` joins the declaration list verbatim, so a value such as
|
|
84
|
+
`#b71c1c;background-image:url(...)` adds declarations to the badge."""
|
|
85
|
+
el = etree.Element("span")
|
|
86
|
+
el.set("class", f"badge badge--{badge.name}")
|
|
87
|
+
el.set("style", f"{BADGE_STYLE}background-color:{badge.value};color:{text_color(badge.value)};")
|
|
88
|
+
el.text = badge.name
|
|
89
|
+
return el
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def badge_html(badge: Badge) -> str:
|
|
93
|
+
"""The badge as an HTML string with a trailing space, for the HTML stash."""
|
|
94
|
+
return etree.tostring(badge_element(badge), encoding="unicode") + " "
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: markdown-badges
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python-Markdown extension rendering inline !name badges, with a catalogue of priority, status and branding badges
|
|
5
|
+
Project-URL: Homepage, https://github.com/antoinekh/markdown-priority-badges
|
|
6
|
+
Project-URL: Repository, https://github.com/antoinekh/markdown-priority-badges
|
|
7
|
+
Project-URL: Issues, https://github.com/antoinekh/markdown-priority-badges/issues
|
|
8
|
+
Project-URL: Documentation, https://github.com/antoinekh/markdown-priority-badges/blob/master/docs/badges.md
|
|
9
|
+
Project-URL: Changelog, https://github.com/antoinekh/markdown-priority-badges/blob/master/CHANGELOG.md
|
|
10
|
+
Author: Antoine Keranflec'h
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: badge,badges,branding,catalogue,documentation,markdown,mkdocs,priority,status,tasklist,zensical
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
|
+
Classifier: Framework :: MkDocs
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Topic :: Documentation
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
|
27
|
+
Classifier: Typing :: Typed
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
|
+
Requires-Dist: markdown>=3.5
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# markdown-badges
|
|
33
|
+
|
|
34
|
+
[](https://github.com/antoinekh/markdown-priority-badges/actions/workflows/ci.yml)
|
|
35
|
+
[](https://pypi.org/project/markdown-badges/)
|
|
36
|
+
[](https://pypi.org/project/markdown-badges/)
|
|
37
|
+
[](https://github.com/antoinekh/markdown-priority-badges/blob/master/LICENSE)
|
|
38
|
+
|
|
39
|
+
A Python-Markdown extension that renders small inline **badges** from a `!name` keyword: priority, status, or brand. Works in Zensical, MkDocs, or plain Python-Markdown. The badge ships its own inline styles, so no external CSS is required.
|
|
40
|
+
|
|
41
|
+
## Why?
|
|
42
|
+
|
|
43
|
+
This is not a replacement for admonitions / callouts (`!!! warning`, `> [!NOTE]`). Those wrap a block of explanatory text. Badges are the opposite: tiny inline pills you can drop anywhere, but that fit especially nicely into a **list item, todo, or table cell**, so status or severity is scannable at a glance without turning the line into a block. The intended usage is exactly that split: reach for a callout when you have a paragraph to say, and reach for a badge to mark some rows.
|
|
44
|
+
|
|
45
|
+
## Badges
|
|
46
|
+
|
|
47
|
+
Write `!name` anywhere (prose, headings, table cells, list items) and it renders as a small inline pill:
|
|
48
|
+
|
|
49
|
+
```markdown
|
|
50
|
+
This migration is !critical and blocks the release.
|
|
51
|
+
|
|
52
|
+
## !high Rotate the keys
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+

|
|
56
|
+
|
|
57
|
+
Only a name in scope matches, so an ordinary `!`, `!important`, or `!highest` in text is never touched. To write a name literally, escape it (`\!high`) or put it in a code span (`` `!high` ``).
|
|
58
|
+
|
|
59
|
+
## Badge types
|
|
60
|
+
|
|
61
|
+
Every badge belongs to one of three types.
|
|
62
|
+
|
|
63
|
+
| Type | Meaning | Examples |
|
|
64
|
+
| --- | --- | --- |
|
|
65
|
+
| `priority` | Carries a severity rank, from least to most severe. Only this type is considered by `priority_of` and `rank_of`. | `!trivial` `!low` `!medium` `!high` `!critical` `!blocker` |
|
|
66
|
+
| `status` | Says where an item sits in a workflow. No rank. | `!todo` `!wip` `!review` `!blocked` `!approved` `!done` `!onhold` `!experimental` `!deprecated` |
|
|
67
|
+
| `branding` | A brand mark. Most carry a logo inlined as a `data:` URI, so a page makes no network request for it; `aws` is a plain colour with no logo, because no CC0 AWS mark exists and the badge text already reads AWS. | `!gitlab` `!github` `!claude` `!docker` `!aws` |
|
|
68
|
+
|
|
69
|
+
## Catalogue
|
|
70
|
+
|
|
71
|
+
Every badge above ships with the package and is active out of the box, no config required.
|
|
72
|
+
|
|
73
|
+

|
|
74
|
+
|
|
75
|
+
Full list with keyword, value, and resolved text colour: **[docs/badges.md](https://github.com/antoinekh/markdown-priority-badges/blob/master/docs/badges.md)**.
|
|
76
|
+
|
|
77
|
+
## Narrowing the catalogue
|
|
78
|
+
|
|
79
|
+
The `catalogue` option is a list of type names, defaulting to all three (`priority`, `status`, `branding`). Pass a subset to load fewer of them, or `[]` to disable the catalogue entirely.
|
|
80
|
+
|
|
81
|
+
```toml
|
|
82
|
+
# zensical.toml
|
|
83
|
+
[project.markdown_extensions.markdown_badges]
|
|
84
|
+
catalogue = ["priority", "status"] # drop the branding badges
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
# plain Python-Markdown
|
|
89
|
+
from markdown_badges import MarkdownBadgesExtension
|
|
90
|
+
markdown.markdown(text, extensions=[MarkdownBadgesExtension(catalogue=["priority", "status"])])
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Adding and recolouring badges
|
|
94
|
+
|
|
95
|
+
The `badges` option is a mapping of type name to a name -> value map, merged over the catalogue: an existing name is recoloured in place, keeping its position and its type, and a new name is inserted after the last badge of its own type, so a new priority outranks every catalogue priority.
|
|
96
|
+
|
|
97
|
+
```toml
|
|
98
|
+
[project.markdown_extensions.markdown_badges.badges.priority]
|
|
99
|
+
showstopper = "#000000" # a new priority, ranked above every catalogue one
|
|
100
|
+
critical = "#8e0000" # an existing name: recolours it, keeping its rank
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from markdown_badges import MarkdownBadgesExtension
|
|
105
|
+
markdown.markdown(text, extensions=[MarkdownBadgesExtension(badges={"priority": {"blocker": "#7b1fa2"}})])
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Colors may be 3-, 4-, 6-, or 8-digit hex (`#7b1fa2`, `#eee`, `#eeeeeeff`) or a common CSS name (`red`, `yellow`, `rebeccapurple`); the badge text color auto-contrasts against them. Any alpha channel is ignored for the contrast calculation.
|
|
109
|
+
|
|
110
|
+
## Extended values
|
|
111
|
+
|
|
112
|
+
A badge value becomes the badge's `background-color`, so anything after a `;` becomes a further declaration on that badge. Use it to give a badge an icon, a gradient, or a shadow, with no site CSS:
|
|
113
|
+
|
|
114
|
+
```toml
|
|
115
|
+
[project.markdown_extensions.markdown_badges.badges.status]
|
|
116
|
+
# A background image, plus the padding that makes room for it.
|
|
117
|
+
icon = "#b71c1c;background-image:url('data:image/svg+xml,…');background-repeat:no-repeat;background-position:0.4em center;background-size:0.85em;padding-left:1.7em"
|
|
118
|
+
# A gradient instead of a flat fill.
|
|
119
|
+
gradient = "#4a148c;background-image:linear-gradient(90deg,#4a148c,#c2185b)"
|
|
120
|
+
# A colored ring and halo.
|
|
121
|
+
glow = "#111;box-shadow:0 0 0 2px #ff1744,0 0 10px #ff1744"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The contrast calculation reads the leading colour, up to the first `;`, so the badge text stays legible against the base you picked.
|
|
125
|
+
|
|
126
|
+
### Custom logo badges
|
|
127
|
+
|
|
128
|
+
Inline a single-path logo as a `data:` URI and you get a brand badge that costs no network request. Pick the base colour and the logo fill together: the badge text colour is chosen from the base, so a white mark needs a base dark enough to resolve to white text, and a dark mark needs a light one.
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
import urllib.parse
|
|
132
|
+
|
|
133
|
+
svg = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='#fff'><path d='M0 0h24v24H0z'/></svg>"
|
|
134
|
+
uri = "data:image/svg+xml," + urllib.parse.quote(svg, safe="")
|
|
135
|
+
value = f"#0052cc;background-image:url('{uri}');background-repeat:no-repeat;background-position:0.45em center;background-size:0.8em;padding-left:1.75em"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Put the resulting `value` under `badges.branding` (or any type) with the name you want the keyword to use, for example `badges={"branding": {"jira": value}}`. For the recipe used to build the shipped branding badges, including the SVG-encoding helper, see `_icon_value` in `src/markdown_badges/catalogue.py`.
|
|
139
|
+
|
|
140
|
+
## Task-list shorthand
|
|
141
|
+
|
|
142
|
+
Not built in by default. The `shorthand` option maps any task-list marker to any badge name, so you can pick your own markers, or restore the old `!` / `!!` behaviour:
|
|
143
|
+
|
|
144
|
+
```toml
|
|
145
|
+
[project.markdown_extensions.markdown_badges.shorthand]
|
|
146
|
+
"!" = "high"
|
|
147
|
+
"!!" = "critical"
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
```markdown
|
|
151
|
+
- [ ] !blocker Waiting on vendor API access
|
|
152
|
+
- [ ] !! Ship the security patch today
|
|
153
|
+
- [ ] ! Review the migration PR
|
|
154
|
+
- [ ] !medium Update the runbook
|
|
155
|
+
- [ ] !low Tidy up log formatting
|
|
156
|
+
- [x] !! Rotate the leaked credentials
|
|
157
|
+
- [ ] Weekly backup check
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
<img alt="Todo list with badges" src="https://raw.githubusercontent.com/antoinekh/markdown-priority-badges/master/docs/img/todo-badges.png" width="560">
|
|
161
|
+
|
|
162
|
+
The marker must come right after the checkbox and be followed by a space, so `- [ ] !important note` is left untouched. Works with `-`, `*`, `+` bullets and both `[ ]` / `[x]` states. Requires `pymdownx.tasklist` to be enabled alongside this extension.
|
|
163
|
+
|
|
164
|
+
## Reusing the parser
|
|
165
|
+
|
|
166
|
+
`badges_in`, `priority_of`, and `rank_of` are exposed for tools that aggregate or filter task items (for example a todo dashboard). Each takes an optional `Mapping[str, Badge]` argument, defaulting to the whole catalogue; pass the result of `resolve_badges` or `catalogue_for` to match your own config instead.
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from markdown_badges import badges_in, priority_of, rank_of
|
|
170
|
+
|
|
171
|
+
badges_in("!blocker vendor waiting !wip") # -> [Badge(name="blocker", ...), Badge(name="wip", ...)]
|
|
172
|
+
priority_of("ping !high vendor") # -> "high"
|
|
173
|
+
priority_of("weekly backup") # -> None (no priority badge)
|
|
174
|
+
rank_of("blocker") # -> 5 (severity index among priority badges)
|
|
175
|
+
rank_of("wip") # -> -1 (not a priority badge)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`badges_in` returns every badge found in the text, of any type, in document order. `priority_of` returns the name of the highest-ranked `priority` badge found, or `None`; `status` and `branding` badges are ignored. `rank_of` gives a badge's severity index among the priority badges, or `-1` if it has none.
|
|
179
|
+
|
|
180
|
+
> [!NOTE]
|
|
181
|
+
> These are plain-text scans, not a Markdown parse. Unlike the rendered badge, a keyword inside a code span or escaped as `\!high` still counts.
|
|
182
|
+
|
|
183
|
+
## Install & enable
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
uv add markdown-badges
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
(or `pip install markdown-badges`)
|
|
190
|
+
|
|
191
|
+
Zensical (`zensical.toml`):
|
|
192
|
+
|
|
193
|
+
```toml
|
|
194
|
+
[project.markdown_extensions.markdown_badges]
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Plain Python-Markdown:
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
markdown.markdown(text, extensions=["markdown_badges"])
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Add `pymdownx.tasklist` to `extensions` too if you enable the `shorthand` option. The badge renders as `<span class="badge badge--<name>" style="...">...</span>`. The `badge` classes are kept for optional site-side overriding, but no CSS is needed by default.
|
|
204
|
+
|
|
205
|
+
## Migrating from 0.2.0
|
|
206
|
+
|
|
207
|
+
The package was renamed from `markdown-priority-badges` to `markdown-badges`, and the config and API changed along with it: see **[MIGRATING.md](https://github.com/antoinekh/markdown-priority-badges/blob/master/MIGRATING.md)**.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
markdown_badges/__init__.py,sha256=Klvg5dX9nQtAwvNhp8fSK8Z3dWzZV4IAq4oIIrT8e1w,6200
|
|
2
|
+
markdown_badges/catalogue.py,sha256=82hw416XCtI7E44UVhB7aeFXHr4-5jx2zHGvmVQpYko,7848
|
|
3
|
+
markdown_badges/parsing.py,sha256=MBjy_Vr1FUvMuXEFxtJe5w2F9lH2EcgAxgEKA-EYeBE,4857
|
|
4
|
+
markdown_badges/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
markdown_badges/styling.py,sha256=BFjMZVskLkibcADgdzjHVY1NsSVlF00M5007vz9j4s4,3182
|
|
6
|
+
markdown_badges-1.0.0.dist-info/METADATA,sha256=9UZMZzo4HOTMQhOHgGx7NhpnzeEl13BkMkP99RoPYmQ,11046
|
|
7
|
+
markdown_badges-1.0.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
8
|
+
markdown_badges-1.0.0.dist-info/licenses/LICENSE,sha256=7KW5IkJYijN9ByJSPh7zxkO65Ny_NDUUnDgwe5LlMtg,1076
|
|
9
|
+
markdown_badges-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Antoine Keranflec'h
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|