misaka icon indicating copy to clipboard operation
misaka copied to clipboard

Unable to call HtmlRenderer's paragraph method

Open noisytoken opened this issue 6 years ago • 5 comments

I am creating a customer renderer to adds admonition tags to Markdown.

import misaka as m
import re
import houdini as h


class CustomHTMLRenderer(m.HtmlRenderer):
    def paragraph(self, content):
        _prefix_re = re.compile(r'^\s*(!{1,4})\s+')

        CLASSES = {
            1: 'note',
            2: 'info',
            3: 'tip',
            4: 'warning',
        }

        match = _prefix_re.match(content)
        if match is None:
            return super(CustomHTMLRenderer, self).paragraph(content)  ## this call is the problem
        else:
            # do something
            pass

When paragraph() method is called I get this error:

AttributeError: 'super' object has no attribute 'paragraph'
From cffi callback <function cb_paragraph at 0x7f4fb4108b70>:
Traceback (most recent call last):
  File "/home/x/project/env/lib/python3.6/site-packages/misaka/callbacks.py", line 82, in cb_paragraph
    result = renderer.paragraph(content)
  File "/home/x/project/django_project/wiki/utils.py", line 99, in paragraph
    return super(CustomHTMLRenderer, self).paragraph(content)
AttributeError: 'super' object has no attribute 'paragraph'

noisytoken avatar Sep 14 '19 18:09 noisytoken

Unfortunately the current design doesn't support overriding parent methods, because HtmlRenderer doesn't have any methods. It just fills a struct with function pointers on init (which are not accessible from Python).

I'm planning to address this in the next major version.

For now you have to re-implement the paragraph rendering method yourself. See code below for an example.

import misaka as m
import re
import houdini as h


class CustomHTMLRenderer(m.HtmlRenderer):
    def paragraph(self, content):
        _prefix_re = re.compile(r'^\s*(!{1,4})\s+')

        CLASSES = {
            1: 'note',
            2: 'info',
            3: 'tip',
            4: 'warning',
        }

        match = _prefix_re.match(content)
        if match is None:
            return '<p>' + content + '</p>\n'
        else:
            length = len(match.group(1))
            value = CLASSES[length]
            return '<p class="' + value + '">' + content[length+1:] + '</p>\n'


if __name__ == '__main__':
    rndr = CustomHTMLRenderer(flags=('escape',))
    md = m.Markdown(rndr)
    print(md("""
some <b>text</b>

!!!! some more text
"""))

FSX avatar Sep 14 '19 21:09 FSX

Thanks that would work. BTW, how can I add Table of contents. The docs mention the HtmlTocRenderer class but doesn't give a clue about how to use it?

noisytoken avatar Sep 20 '19 18:09 noisytoken

You can add nesting_level=6 to CustomHTMLRenderer to add id attributes to the HTML headers. And HtmlTocRenderer can be used to render the input text to a nested list of header contents.

Here's your code updated with a toc:

import misaka as m
import re
import houdini as h


class CustomHTMLRenderer(m.HtmlRenderer):
    def paragraph(self, content):
        _prefix_re = re.compile(r'^\s*(!{1,4})\s+')

        CLASSES = {
            1: 'note',
            2: 'info',
            3: 'tip',
            4: 'warning',
        }

        match = _prefix_re.match(content)
        if match is None:
            return '<p>' + content + '</p>\n'
        else:
            length = len(match.group(1))
            value = CLASSES[length]
            return '<p class="' + value + '">' + content[length+1:] + '</p>\n'


def render_table_of_contents(text):
    rndr = m.HtmlTocRenderer()
    md = m.Markdown(rndr)
    return md(text)


def render_contents(text, flags):
    rndr = CustomHTMLRenderer(nesting_level=6, flags=('escape',))
    md = m.Markdown(rndr)
    return md(text)


if __name__ == '__main__':
    flags = ('escape',)
    text = """
# H1

some <b>text</b>

## H2

!!!! some more text
"""

    print(
        render_table_of_contents(text) +
        render_contents(text, flags)
    )

FSX avatar Sep 24 '19 18:09 FSX

Unfortunately the current design doesn't support overriding parent methods, because HtmlRenderer doesn't have any methods. It just fills a struct with function pointers on init (which are not accessible from Python).

I'm planning to address this in the next major version.

For now you have to re-implement the paragraph rendering method yourself. See code below for an example.

import misaka as m
import re
import houdini as h


class CustomHTMLRenderer(m.HtmlRenderer):
    def paragraph(self, content):
        _prefix_re = re.compile(r'^\s*(!{1,4})\s+')

        CLASSES = {
            1: 'note',
            2: 'info',
            3: 'tip',
            4: 'warning',
        }

        match = _prefix_re.match(content)
        if match is None:
            return '<p>' + content + '</p>\n'
        else:
            length = len(match.group(1))
            value = CLASSES[length]
            return '<p class="' + value + '">' + content[length+1:] + '</p>\n'


if __name__ == '__main__':
    rndr = CustomHTMLRenderer(flags=('escape',))
    md = m.Markdown(rndr)
    print(md("""
some <b>text</b>

!!!! some more text
"""))

This code renders the HTML_HARD_WRAP flag useless.

noisytoken avatar Nov 01 '19 15:11 noisytoken

You can add nesting_level=6 to CustomHTMLRenderer to add id attributes to the HTML headers. And HtmlTocRenderer can be used to render the input text to a nested list of header contents.

Here's your code updated with a toc:

import misaka as m
import re
import houdini as h


class CustomHTMLRenderer(m.HtmlRenderer):
    def paragraph(self, content):
        _prefix_re = re.compile(r'^\s*(!{1,4})\s+')

        CLASSES = {
            1: 'note',
            2: 'info',
            3: 'tip',
            4: 'warning',
        }

        match = _prefix_re.match(content)
        if match is None:
            return '<p>' + content + '</p>\n'
        else:
            length = len(match.group(1))
            value = CLASSES[length]
            return '<p class="' + value + '">' + content[length+1:] + '</p>\n'


def render_table_of_contents(text):
    rndr = m.HtmlTocRenderer()
    md = m.Markdown(rndr)
    return md(text)


def render_contents(text, flags):
    rndr = CustomHTMLRenderer(nesting_level=6, flags=('escape',))
    md = m.Markdown(rndr)
    return md(text)


if __name__ == '__main__':
    flags = ('escape',)
    text = """
# H1

some <b>text</b>

## H2

!!!! some more text
"""

    print(
        render_table_of_contents(text) +
        render_contents(text, flags)
    )

There is one problem with this code. Consider the following:

# from PIL import Image
# import webp
#
# img = Image.open('/home/x/Downloads/img1.png')
# webp.save_image(img, '/home/x/Downloads/error.webp', quality=80)

import misaka as m
import re
import houdini as h


class CustomHTMLRenderer(m.HtmlRenderer):
    def paragraph(self, content):
        _prefix_re = re.compile(r'^\s*(!{1,4})\s+')

        CLASSES = {
            1: 'note',
            2: 'info',
            3: 'tip',
            4: 'warning',
        }

        match = _prefix_re.match(content)
        if match is None:
            return '<p>' + content + '</p>\n'
        else:
            length = len(match.group(1))
            value = CLASSES[length]
            return '<p class="' + value + '">' + content[length+1:] + '</p>\n'


def render_table_of_contents(text):
    rndr = m.HtmlTocRenderer()
    md = m.Markdown(rndr)
    return md(text)


def render_contents(text, flags):
    rndr = CustomHTMLRenderer(nesting_level=6, flags=('escape',))
    md = m.Markdown(rndr)
    return md(text)


if __name__ == '__main__':
    flags = ('escape',)
    text = """
# H1

some <b>text</b>

## H2

!!!! some more text

```python
# comment

print("hello")
print(
    render_table_of_contents(text) +
    render_contents(text, flags)
)

Here the line `# comment` is part of python code, but it is still being rendered in toc.

noisytoken avatar Jul 14 '20 06:07 noisytoken