Skip to content

Instantly share code, notes, and snippets.

@waylan
Last active March 2, 2023 14:26
Show Gist options
  • Save waylan/66843de1c8187e6ade1663916b4ab9cd to your computer and use it in GitHub Desktop.
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.
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)
<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