Skip to main content

Python Markdown extension to add custom parametrizable and nestable blocks

Project description

Custom blocks for Markdown

CI Coverage PyPi license: AGPL v3 downloads

This Python-Markdown extension defines a common markup for parametrizable and nestable components that can be extended by defining a plain Python function.

Includes some sample components for div containers, admonitions, figures, link cards... and embeds from common sites (youtube, vimeo, twitter...)

What is it?

This extension parses markup structures like this one:

::: mytype "value 1" param2=value2
    Indented content

delegating the HTML generation to custom functions (generators) you can define or redefine for the type (mytype, in the example) to suit your needs. For example, we could bind mytype to this generator:

def mygenerator(ctx, param1, param2):
   """Quick and dirty generator, would need escaping"""
    return f"""<div attrib1="{param1}" attrib2="{param2}">{ctx.content}</div>"""

With the previous markdown, it will generate:

<div attrib1="value 1" attrib2="value2">Indented Content</div>

The extension also provides several useful generators:

  • container: A classed div with arbitrary classes, attributes and content (This is the default when no type matches)
  • figure: Figures with caption and more
  • admonition: Admonitions (quite similar to the standard extra extension)
  • twitter: Embeded tweets
  • youtube: Embeded videos from youtube...
  • vimeo: Embeded videos from vimeo...
  • linkcard: External link cards (like Facebook and Twitter do, when you post a link)
  • verkami: Fund raising project widget in Verkami
  • goteo: Fund raising project widget in Goteo

They are examples, you can always rewrite them to suit your needs.

Why this?

Markdown, has a quite limited set of structures, and you often end up writing html by hand: A figure, an embed... If you use that structure multiple times, whenever you find a better way, you end up updating the structures in many places. That's why you should use (or develop) a markdown extension to ease the proces.

There is a catch. Extensions struggle to use a unique markup to avoid conflicts with other extensions. Because of that, the trend is having a lot of different markups, even for extensions sharing purpose. When you find a better extension for your figures, again, it is likely you have to edit all your figures, once more, because the markup is different.

Also coding an extension is hard. Markdown extension API is necessarily complex to address many scenarios. But this extension responds just to this single but general scenario:

I want to generate this piece of html which depends on those parameters and might include a given content.

So...

Why using a common markup for that many different structures?
This way, markup syntax explosion is avoided, and users do not have to learn a new syntax. Also, developing new block types is easier if you can reuse the same parser.

Why using a type name to identify the structure?
A name as part of the markup clarifies the block meaning on reading. Also provides a hook to change the behaviour while keeping the semantics.

Why defining a common attribute markup?
A common attribute markup is useful to stablish a general mapping between markup attributes and Python function parameters. The generator function signature defines the attributes that can be used and the extension does the mapping with no extra glue required.

Why using indentation to indicate inner content?
It visually shows the scope of the block and allows nesting. If the content is reparsed as Markdown, it could still include other components with their inner content a level deeper.

We all stand on giants' shoulders so take a look at the long list of markdown extensions and other software that inspired and influenced ideas for this extension. Kudos for all of them.

Installation and setup

To install:

$ pip install markdown-customblocks

From command line:

markdown -x customblocks ...

From Python:

import markdown
md = markdown.Markdown(
    extensions=["customblocks"],
    extension_configs=dict(
        customblocks={
           ...
	}
    ),
md.convert(markdowncontent)

In order to enable it in Pelican:

MARKDOWN = {
    'extensions': [
        'customblocks',
    ],
}

General markup syntax

This is a more complete example of custom block usage:

::: mytype param1 key1=value1 "param with many words" key2="value2 with words"
    Indented **content**

    The block ends whenever the indentation stops
This unindented line is not considered part of the block

The line starting with ::: is the headline. It specifies, first, the block type (mytype) followed by a set of values. Such values can be either single worded or quoted. Also some values may explicit a target parameter with a key.

After the headline, several lines of indented content may follow, and the block ends at the very first line back to the previous indentation. Emtpy lines are included and there is no need of an empty line to end the block.

By using indentation you don't need a closing tag, but if you miss it, you might place a closing ::: at the same level of the headline.

A block type may interpret the content as markdown as well. So you can have nested blocks by adding extra indentation. For example:

::: recipe
    # Sweet water
    ::: ingredients "4 persons"
        - two spons of suggar
        - a glass of tap water
    ::: mealphoto sweetwater.jpg
        Looks gorgeus!
    Drop the suggar into the glass. Stir.

Implementing a generator

A block type can be defined just by hooking the generator function to the type.

MARKDOWN = {
    ...
    'extensions_configs': {
        'customblocks': {
            'generators': {
                # by direct symbol reference
                'mytype': myparentmodule.mymodule.mytype,
                # or using import strings (notice the colon)
                'aka_mytype': 'myparentmodule.mymodule:mytype',
            }
        },
    },
}

The signature of the generator will determine the attributes taken from the headline.

def mytype(ctx, param1, myflag:bool, param2, param3, yourflag=True, param4='default2'):
    ...

The first parameter, ctx, is the context. If you don't use it, you can skip it. But it is useful if you want to receive some context parameters like:

  • ctx.parent: the parent node
  • ctx.content: the indented part of the block, with the indentation removed
  • ctx.parser: the markdown parser, can be used to parse the inner content or any other markdown code
  • ctx.type: the type of the block
    • If you reuse the same function for different types, this is how you diferentiate them
  • ctx.metadata: A dictionary with metadata from your metadata plugin.
  • ctx.config: A dictionary passed from extension_configs.customblocks.config

Besides ctx, the rest of function parameters are filled using values parsed from head line. Unlike Python, you can interleave in the headline values with and without keys. They are resolved as follows:

  • Explicit key: When a key in the headline matches a keyable parameter name in the generator, the value is assigned to it
  • Flag: Generator arguments annotated as bool (like example's myflag), or defaulting to True or False, (like example's yourflag) are considered flags
    • When a keyless value matches a flag name in the generator (myflag), True is passed
    • When it matches the flag name prefixed with no (nomyflag), False is passed
  • Positional: Remaining headline values and function parameters are assigned one-to-one by position
  • Restricted: Restrictions on how to receive the values (keyword-only and positional-only) are respected and they will receive only values from either key or keyless values
  • Varidics: If the signature contains key (**kwds) or positional (*args) varidic variables, any remaining key and keyless values from the headline are assigned to them

Following Markdown phylosophy, errors are warned but do not stop the processing, so:

  • Unmatched function parameters without a default value will be warned and assigned an empty string.
  • Unused headline values will be warned and ignored.

A generator can use several strategies to generate content:

  • Return an html string (single root node)
  • Return a markdown.etree Element object
  • Manipulate ctx.parent to add the content and return None

In order to construct an ElementTree, we recommend using the Hyperscript utility. Resulting code will be more compact and readable and makes proper escaping when injecting values.

Predefined generators

Container (customblocks.generators.container)

This is the default generator when no other generator matches the block type. It can be used to generate html div document structure with markdown.

It creates a <div> element with the type name as class. Keyless values are added as additional classes and key values are added as attributes for the div element.

*args : added as additional classes for the outter div

**kwds : added as attributes for the outter div

The following example:

::: sidebar left style="width: 30em"
    ::: widget
        # Social
        ...
    ::: widget
        # Related
        ...

Renders as:

<div class='sidebar left' style="width: 30em">
    <div class='widget'>
        <h1>Social</h1>
        <p>...</p>
    </div>
    <div class='widget'>
        <h1>Related</h1>
        <p>...</p>
    </div>
</div>

Admonition (customblocks.generators.admonition)

An admonition is a specially formatted text out of the main flow which remarks a piece of text, often in a box or with a side icon to identify it as that special type of text.

Admonition generator is, by default, assigned to the following types: attention, caution, danger, error, hint, important, note, tip, warning.

So you can write:

::: danger
    Do not try to do this at home

In order to generate:

<div class="admonition danger">
<p class="admonition-title">Danger</p>
<p>Do not try to do this at home</p>
</div>

Generated code emulates the one generated by ReST admonitions (which is also emulated by markdown.extra.admonition). So, you can benefit from existing styles and themes.

title : in the title box show that text instead of the

*args : added as additional classes for the outter div

**kwds : added as attributes for the outter div

Warning: If you are migrating from extra.admonition, be careful as extra identifies title using the quotes, while customblocks will take the first parameter as title and next values as additional classes. If you like having the classes before, you should explicit the title key.

::: danger blinking title="Super danger"
    Do **not** try to do this at home

Figure (customblocks.generators.figure)

An image as captioned figure. The content is taken as caption.

::: figure images/myimage.jpg alt='an image' nice
    This is a **nice** image.

Renders into:

<figure class="nice">
  <a href="images/myimage.jpg target="_blank">
    <img src="images/myimage.jpg" alt="an image" />
  </a>
  <figcaption>
    <p>This is a <b>nice</b> image</p>
  </figcaption>
</figure>

url : the url to the image

alt (keyword only) : image alt attribute

title (keyword only) : image title attribute

lightbox (bool) : whether to open a lightbox on click or not

*args : additional classes for root <figure> tag

**kwds : additional attributes for root <figure> tag

In order lightbox to work you must add the following css to your page:

/* this is aesthetic */
figure {
 border: 1pt solid lightgrey;
 background: #efefef;
 color: #111;
 padding: 3pt;
}
figure {
 display: inline-block;
}
figure figcaption {
 width: 100%;
 text-align: center;
}
figure img {
 object-fit: contain;
 margin: auto 0;
 max-width: 100%;
 max-height: 100%;
 width: 100%;
}
figure.centered {
 display: block;
 margin: auto;
 text-align: center;
}
figure.lightbox {
 transition: 0.5s;
 transition-property: background;
}
figure.lightbox:target {
 transition: 0.5s;
 transition-property: background;
 position: fixed;
 top: 0;
 bottom: 0;
 left: 0;
 right: 0;
 background: black;
 background: rgba(0,0,0,.98);
 color: grey;
 height: 100% !important;
 width: 100% !important;
 padding: 0;
 margin: 0;
}
figure.lightbox .lightbox-background {
 display: none;
}
figure.lightbox:target .lightbox-background {
 position: fixed;
 display: block;
 width: 100%;
 position: absolute;
 height: 100%;
}
figure.lightbox:target img {
 display: block;
 margin: 2% auto;
 width: 100vw;
 height: auto;
 max-width: 90%;
 max-height: 80%;
}

TODO: Thumbnails, figure enumeration, fetch external images.

Link card (customblocks.generators.linkcard)

A link card is a informative box about an external source. It is similar to the card that popular apps like Wordpress, Facebook, Twitter, Telegram, Slack... generate when you embed/post a link.

The generator downloads the target url and extracts social metadata: Featured image, title, description...

::: linkcard https://css-tricks.com/essential-meta-tags-social-media/

url : The url to embed as card

wideimage (Flag, default True) : Whether the featured image will be shown wide, if not, a small thumb will be shown

Content, if provided will be used as excerpt instead of the summary in the page.

Additionally you can provide the following keyword parameters to override information extracted from the url:

  • image: the image heading the card
  • title: the caption
  • description: the text describing the link
  • siteurl: a link to the main site
  • sitename: the name of the main site
  • siteicon: the site icon

Youtube (customblocks.generators.youtube)

This generator generates an embeded youtube video.

::: youtube HUBNt18RFbo nocontrols left-align

autoplay (flag, default False) : starts the video as soon as it is loaded

loop (flag, default False) : restart again the video once finished

controls (flag, default True) : show the controls

*args : added as additional class for the outter div

**kwds : added as attributes for the outter div

Indented content is ignored.

Recommended css:

.videowrapper {
    position:relative;
    padding-bottom:56.25%;
    overflow:hidden;
    height:0;
    width:100%
}
.videowrapper iframe {
    position:absolute;
    left:0;
    top:0;
    width:100%;
    height:100%;
}

Or you could set youtube_inlineFluidStyle config to True and the style will be added inline to every video.

Vimeo (customblocks.generators.vimeo)

This generator generates an embeded vimeo video.

::: vimeo 139579122  nocontrols left-align

autoplay (flag, default False) : starts the video as soon as it is loaded

loop (flag, default False) : restart again the video once finished

bylabel (flag, default True) : Shows the video author's name

portrait (flag, default False) : Shows the video author's avatar

*args : added as additional class for the outter div

**kwds : added as attributes for the outter div

Content is ignored.

Twitter (customblocks.generators.twitter)

Embeds a tweet.

::: twitter marcmushu 1270395360163307530 theme=dark lang=es track=true

user: : the user that wrote the tweet

tweet : the tweet id (a long number)

theme (optional, default light) : It can be either dark or light

hideimages : Do not show attached images in the embedded

align : left, center or right

conversation : whether to add or not the full thread

Verkami (customblocks.generators.verkami)

Embeds a Verkami fund raising campaign widget.

::: verkami 26588 landscape

id : The id of the project (can be the number or the full id)

landscape (Flag, default False) : instead of a portrait widget generate a landscape one

Goteo (customblocks.generators.goteo)

Embeds a Goteo fund raising campaign widget.

::: goteo my-cool-project

id : The id of the project

Generator tools

Common code has been extracted from predefined generators. If you need this functionality you are encouraged to use them.

  • Hyperscript: to generate html
  • PageInfo: to extract metadata from a webpage
  • Fetcher: to download resources with file based cache

Hyperscript generation

You can generate html with strings or using etree; but there is a more elegant option.

Hyperscript is the idea of writing code that generates html/xml as nested function calls that look like the the actual xml structure. This can be done by using the customblocks.utils.E function which has this signature:

def E(tag, *children, **attributes): ...

tag is the name of the tag (pre, div, strong...). An empty string is equivalent to div. It can have appended several .classname that will be added as element class.

Any keyword parameter will be taken as element attributes. You can use the special _class attribute to append more classes. Notice the underline, as class is a reserved word in Python.

children takes the keyless parameters and they can be:

  • None: then it will be ignored
  • dict: it will be merged with the attributes
  • str: it will be added as text
  • etree.Element: it will be added as child node
  • customblocks.utils.Markdown: will append parsed markdown (see below)
  • Any tuple, list or iterable: will add each item following previous rules
from customblocks.utils import E, Markdown

def mygenerator(ctx, image):
	return (
		E('.mytype',
			dict(style="width: 30%; align: left"),
			E('a', dict(href=image),
				E('img', src=image),
			),
			Markdown(ctx.content, ctx.parser),
		)
	)

PageInfo

utils.pageinfo.PageInfo is a class that retrieves meta information from html pages by means of its properties.

Properties are computed lazily and use cache. Once you get one property for a given page, later uses will have little impact.

Any attribute you explicit on the constructor will override the ones derived from actual content.

info = PageInfo(html, url='http://site.com/path/page.html')
info.sitename # the name of the site (meta og:site_name or the domain
info.siteicon # the favicon or similar
info.siteurl  # the base url of the site (not the page)
info.title    # page title (from og:title meta or `<title>` content)
info.description # short description (from og:description or twitter:description)
info.image    # featured image (from og:image or twitter:image, or site image)

Fetcher

A fetcher object is a wrapper around the requests library that uses caching to avoid downloading once and again remote resource each time you compile the markdown file.

The first time a resource is succesfully downloaded by a fetcher the request response is stored in the provided folder in a yaml file which has the mangled url as name. Successive tries to download it just take the content of that file to construct a query.

from customblocks.utils import Fetcher

fetcher = Fetcher('mycachedir')
response = fetcher.get('https://canvoki.net/codder')
# to force next call
fetcher.remove('https://canvoki.net/codder')

Release history

See CHANGES.md

TODO

  • Default css for generators
  • Flags: coerce to bool?
  • Annotations: coerce to any type
  • Fetcher:
    • configurable cache dir
    • file name too long
    • handle connection errors
  • Linkcard:
    • Look for short description by class (ie wikipedia)
  • Youtube:
  • Twitter
    • Privacy safe mode
  • Figure flags:
    • no flag
      • Un modified url
    • local (when remote url)
      • download
      • place it on a given dir
      • set url to local path
    • inline
      • download
      • detect mime type
      • compute base 64
      • set url to data url
    • thumb
      • download
      • generate a thumb
      • place the thumb on thumb dir
      • when combined with 'inline'
        • url to the local path
      • when combined with 'local'
        • link to the image
    • lightbox
    • sized

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

markdown-customblocks-1.2.0.tar.gz (47.0 kB view details)

Uploaded Source

Built Distribution

markdown_customblocks-1.2.0-py3-none-any.whl (41.4 kB view details)

Uploaded Python 3

File details

Details for the file markdown-customblocks-1.2.0.tar.gz.

File metadata

  • Download URL: markdown-customblocks-1.2.0.tar.gz
  • Upload date:
  • Size: 47.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.8.0 pkginfo/1.8.2 readme-renderer/33.0 requests/2.27.1 requests-toolbelt/0.9.1 urllib3/1.26.8 tqdm/4.63.0 importlib-metadata/4.11.2 keyring/23.5.0 rfc3986/2.0.0 colorama/0.4.4 CPython/3.9.10

File hashes

Hashes for markdown-customblocks-1.2.0.tar.gz
Algorithm Hash digest
SHA256 6d5e828a8937c967cf196158d79e6ee9e62a30916343dc7d7676c91fd69328c8
MD5 36dc727358d1f9734caab205fc80715b
BLAKE2b-256 6016bc21ffa16b3078d47677cf81f30fb594e183ba38bde2f8f9e3b9ead1a8c2

See more details on using hashes here.

File details

Details for the file markdown_customblocks-1.2.0-py3-none-any.whl.

File metadata

  • Download URL: markdown_customblocks-1.2.0-py3-none-any.whl
  • Upload date:
  • Size: 41.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.8.0 pkginfo/1.8.2 readme-renderer/33.0 requests/2.27.1 requests-toolbelt/0.9.1 urllib3/1.26.8 tqdm/4.63.0 importlib-metadata/4.11.2 keyring/23.5.0 rfc3986/2.0.0 colorama/0.4.4 CPython/3.9.10

File hashes

Hashes for markdown_customblocks-1.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 554454652e95ab42fc4a1d8a95d181b894e1771da4c618438ce75fcaf721f3bb
MD5 d4e4ecbd91ab6ed6fc82befa28611eac
BLAKE2b-256 12f8fc830fc5f9ac6c73118aa71dda2e9bbedf905f7ebae3dcf772b738cb69b5

See more details on using hashes here.

Supported by

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