Last active
March 2, 2023 14:26
-
-
Save waylan/66843de1c8187e6ade1663916b4ab9cd to your computer and use it in GitHub Desktop.
A Markdown extension which adds support for bootstrap alerts and (simple) carousels using pymdownx blocks v9.10b5.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from pymdownx.blocks import BlocksExtension | |
from pymdownx.blocks.block import Block, type_html_attribute_dict, type_html_identifier, type_string_in, \ | |
type_boolean, type_string, , type_multi | |
import xml.etree.ElementTree as etree | |
import uuid | |
class BsAlertBlock(Block): | |
NAME = 'alert' | |
ARGUMENT = None | |
OPTIONS = { | |
'type': ['primary', type_string_in([ | |
'primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark' | |
], type_html_identifier)], | |
'dismissable': [False, type_boolean], | |
'markdown': ['inline', type_string_in(['block', 'inline', 'raw', 'auto'])] | |
} | |
def on_create(self, parent): | |
''' Create wrapper div ''' | |
# Define the basic attributes needed to make this a Bootstrap alert | |
classes = f'alert alert-{self.options["type"]}' | |
if self.options['dismissable']: | |
# TODO: Maybe leave `fade show` out and let the user define them? | |
classes = f'{classes} alert-dismissible fade show' | |
attrib = { | |
'class': classes, | |
'role': 'alert' | |
} | |
# Build the containing Element | |
alert = etree.SubElement(parent, 'div', attrib=attrib) | |
if self.argument: | |
# Build the title Element | |
title = etree.SubElement(alert, 'h4', attrib={'class': 'alert-heading'}) | |
title.text = self.argument | |
return alert | |
def on_markdown(self): | |
return self.options['markdown'] | |
def on_end(self, block): | |
''' Add close button to end of block if dismissable ''' | |
if self.options['dismissable']: | |
attrib = { | |
'type':'button', | |
'class': 'btn-close', | |
'data-bs-dismiss': 'alert', | |
'aria-label': 'Close' | |
} | |
etree.SubElement(block, 'button', attrib=attrib) | |
class BsCarouselBlock(Block): | |
NAME = 'carousel' | |
ARGUMENT = False | |
OPTIONS = { | |
'controls': [True, type_boolean], | |
'indicators': [False, type_boolean], | |
'fade': [False, type_boolean], | |
'autoplay': [False, type_multi(type_boolean, type_string_in(['carousel']))] | |
} | |
def on_init(self): | |
self.inner_element = None | |
def on_validate(self, parent): | |
if 'id' not in self.options['attrs']: | |
self.options['attrs']['id'] = str(uuid.uuid4()) | |
return True | |
def on_create(self, parent): | |
attrib = {'class': 'carousel slide'} | |
if self.options['fade']: | |
# TODO: maybe add some class to the carousel-item? See Bootstrap docs. | |
attrib['class'] += ' carousel-fade' | |
if self.options['autoplay'] is not False: | |
attrib['data-bs-ride'] = str(self.options['autoplay']).lower() | |
block = etree.SubElement(parent, 'div', attrib=attrib) | |
self.inner_element = etree.SubElement(block, 'div', attrib={'class': 'carousel-inner'}) | |
if self.options['controls']: | |
# Add next/previous controls | |
for direction in ['prev', 'next']: | |
attrib = { | |
'class': f'carousel-control-{direction}', | |
'type': 'button', | |
'data-bs-target': f'#{self.options["attrs"]["id"]}', | |
'data-bs-slide': direction | |
} | |
text = 'Previous' if direction == 'prev' else 'Next' | |
el = etree.SubElement(block, 'button', attrib=attrib) | |
etree.SubElement(el, 'span', attrib={'class': f'carousel-control-{direction}-icon', 'aria-hidden': 'true'}) | |
etree.SubElement(el, 'span', attrib={'class': 'visually-hidden'}).text = text | |
return block | |
def on_add(self, block): | |
return self.inner_element | |
def on_markdown(self): | |
return 'inline' | |
def on_inline_end(self, block): | |
''' Run after inline parsing is complete. ''' | |
if self.options['indicators']: | |
indicators = etree.Element('div', attrib={'class': 'carousel-indicators'}) | |
block.insert(0, indicators) | |
for i, img in enumerate(self.inner_element.findall('img')): | |
self.inner_element.remove(img) | |
attrib={'class': 'carousel-item'} | |
if i == 0: | |
attrib['class'] = f'{attrib["class"]} active' | |
slide = etree.SubElement(self.inner_element, 'div', attrib=attrib) | |
img.set('class', 'd-block w-100') | |
slide.append(img) | |
if self.options['indicators']: | |
attrib = { | |
'data-bs-target': f'#{self.options["attrs"]["id"]}', | |
'data-bs-slide-to': f'{i}', | |
'aria-label': f'Slide {i+1}' | |
} | |
if i == 0: | |
attrib['class'] = 'active' | |
attrib['aria-current'] = 'true' | |
etree.SubElement(indicators, 'button', attrib=attrib) | |
class BsBlockExtension(BlocksExtension): | |
def extendMarkdownBlocks(self, md, block_mgr): | |
block_mgr.register(BsAlertBlock, self.getConfigs()) | |
block_mgr.register(BsCarouselBlock, self.getConfigs()) | |
def makeExtension(*args, **kwargs): | |
"""Return extension.""" | |
return BsBlockExtension(*args, **kwargs) | |
if __name__ == '__main__': | |
import markdown | |
from textwrap import dedent | |
src = dedent(''' | |
/// alert | |
This is an alert. | |
/// | |
/// alert | Title | |
This alert has a title. | |
/// | |
/// alert | Warning | |
type: warning | |
This is a **warning** alert with a title. | |
/// | |
/// alert | |
type: secondary | |
attrs: {id: custom, class: p5} | |
markdown: block | |
This alert has custom attributes defined. We have a custom id and custom padding set. | |
The custom padding is set using bootstraps' class `p5` which adds padding to all sides. | |
/// | |
/// alert | |
type: danger | |
dismissable: true | |
markdown: auto | |
You can dismiss me! | |
/// | |
/// carousel | |
#attrs: {id: 'random'} | |
indicators: true | |
#fade: true | |
#autoplay: carousel | |
![img 1](img1.jpg) | |
![img 2](img2.jpg) | |
/// | |
''') | |
body = markdown.markdown(src, extensions=[BsBlockExtension()]) | |
template = dedent(f''' | |
<!doctype html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<title>Bootstrap demo</title> | |
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous"> | |
</head> | |
<body> | |
<h1>Hello, world!</h1> | |
{body} | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN" crossorigin="anonymous"></script> | |
</body> | |
</html> | |
''') | |
with open('example.html', 'w') as f: | |
f.write(template) | |
print(body) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div class="alert alert-primary" role="alert"> | |
<p>This is an alert.</p> | |
</div> | |
<div class="alert alert-primary" role="alert"> | |
<h4 class="alert-heading">Title</h4> | |
<p>This alert has a title.</p> | |
</div> | |
<div class="alert alert-warning" role="alert"> | |
<h4 class="alert-heading">Warning</h4> | |
<p>This is a <strong>warning</strong> alert with a title.</p> | |
</div> | |
<div class="alert alert-secondary p5" id="custom" role="alert"> | |
<p>This alert has custom attributes defined. We have a custom id and custom padding set. | |
The custom padding is set using bootstraps' class <code>p5</code> which adds padding to all sides.</p> | |
</div> | |
<div class="alert alert-danger alert-dismissible fade show" role="alert"> | |
<p>You can dismiss me!</p> | |
<button aria-label="Close" class="btn-close" data-bs-dismiss="alert" type="button"></button></div> | |
<div class="carousel slide" id="9b81eb23-0ec0-4f2b-8ddd-48a36d92f409"> | |
<div class="carousel-indicators"><button aria-current="true" aria-label="Slide 1" class="active" data-bs-slide-to="0" data-bs-target="#9b81eb23-0ec0-4f2b-8ddd-48a36d92f409"></button><button aria-label="Slide 2" data-bs-slide-to="1" data-bs-target="#9b81eb23-0ec0-4f2b-8ddd-48a36d92f409"></button></div> | |
<div class="carousel-inner"> | |
<div class="carousel-item active"><img alt="img 1" class="d-block w-100" src="img1.jpg" /> | |
</div> | |
<div class="carousel-item"><img alt="img 2" class="d-block w-100" src="img2.jpg" /></div> | |
</div> | |
<button class="carousel-control-prev" data-bs-slide="prev" data-bs-target="#9b81eb23-0ec0-4f2b-8ddd-48a36d92f409" type="button"><span aria-hidden="true" class="carousel-control-prev-icon"></span><span class="visually-hidden">Previous</span></button><button class="carousel-control-next" data-bs-slide="next" data-bs-target="#9b81eb23-0ec0-4f2b-8ddd-48a36d92f409" type="button"><span aria-hidden="true" class="carousel-control-next-icon"></span><span class="visually-hidden">Next</span></button></div> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment