diff --git a/benchmark/benchmark.py b/benchmark/benchmark.py index 904e483..b296ca2 100644 --- a/benchmark/benchmark.py +++ b/benchmark/benchmark.py @@ -36,24 +36,42 @@ class benchmark(object): print('{0} is not available'.format(name)) -@benchmark('mistune') -def benchmark_mistune(text): - import mistune - mistune.markdown(text) +# @benchmark('mistune') +# def benchmark_mistune(text): +# import mistune +# mistune.markdown(text) @benchmark('misaka') def benchmark_misaka(text): import misaka as m # mistune has all these features - extensions = ( - m.EXT_NO_INTRA_EMPHASIS | m.EXT_FENCED_CODE | m.EXT_AUTOLINK | - m.EXT_TABLES | m.EXT_STRIKETHROUGH - ) + # extensions = ( + # m.EXT_NO_INTRA_EMPHASIS | m.EXT_FENCED_CODE | m.EXT_AUTOLINK | + # m.EXT_TABLES | m.EXT_STRIKETHROUGH + # ) # md = m.Markdown(m.HtmlRenderer(), extensions=extensions) # md.render(text) - m.html(text, extensions) + m.html(text, ('no-intra-emphasis', 'fenced=code', 'autolink', 'tables', 'strikethrough')) + + +@benchmark('misaka_classes') +def benchmark_misaka_classes(text): + import misaka as m + # mistune has all these features + # extensions = ( + # m.EXT_NO_INTRA_EMPHASIS | m.EXT_FENCED_CODE | m.EXT_AUTOLINK | + # m.EXT_TABLES | m.EXT_STRIKETHROUGH + # ) + # # md = m.Markdown(m.HtmlRenderer(), extensions=extensions) + # # md.render(text) + + # m.html(text, extensions) + + r = m.HtmlRenderer() + p = m.Markdown(r, ('no-intra-emphasis', 'fenced=code', 'autolink', 'tables', 'strikethrough')) + p(text) # @benchmark('markdown2') @@ -90,7 +108,7 @@ def benchmark_hoep(text): m.EXT_TABLES | m.EXT_STRIKETHROUGH | m.EXT_FOOTNOTES ) md = m.Hoep(extensions=extensions) - md.render(text.decode('utf-8')) + md.render(text) if __name__ == '__main__': diff --git a/build_ffi.py b/build_ffi.py index 7acca16..57445c3 100644 --- a/build_ffi.py +++ b/build_ffi.py @@ -56,6 +56,7 @@ ffi.set_source( #include "hoedown/buffer.h" #include "hoedown/document.h" #include "hoedown/html.h" +#include "extra.h" """, sources=( 'misaka/hoedown/version.c', @@ -67,6 +68,7 @@ ffi.set_source( 'misaka/hoedown/document.c', 'misaka/hoedown/buffer.c', 'misaka/hoedown/autolink.c', + 'misaka/extra.c', ), include_dirs=('misaka',)) @@ -274,6 +276,12 @@ hoedown_renderer *hoedown_html_toc_renderer_new( ); void hoedown_html_renderer_free(hoedown_renderer *renderer); void hoedown_html_smartypants(hoedown_buffer *ob, const uint8_t *data, size_t size); + +// --------------- +// --- extra.h --- +// --------------- + +void *misaka_get_renderer(const hoedown_renderer_data *data); """) diff --git a/misaka/api.py b/misaka/api.py index d20726a..6529031 100644 --- a/misaka/api.py +++ b/misaka/api.py @@ -1,16 +1,10 @@ # -*- coding: utf-8 -*- -import sys -import operator as op -import warnings -from inspect import getmembers, ismethod - from ._hoedown import lib, ffi - -try: - reduce -except NameError: - from functools import reduce +from .callbacks import python_callbacks, to_string +from .constants import * +from .utils import extension_map, html_flag_map, args_to_int, \ + deprecation, to_string __all__ = [ @@ -21,7 +15,7 @@ __all__ = [ 'HtmlRenderer', 'HtmlTocRenderer', - '_args_to_int', + 'args_to_int', 'extension_map', 'html_flag_map', @@ -59,81 +53,17 @@ __all__ = [ ] -def _set_constants(): - is_int = lambda n: isinstance(n, int) - - for name, value in getmembers(lib, is_int): - if not name.startswith('HOEDOWN_'): - continue - setattr(sys.modules[__name__], name[8:], value) - - -if not hasattr(sys.modules[__name__], 'EXT_TABLES'): - _set_constants() - - -extension_map = { - 'tables': EXT_TABLES, - 'fenced-code': EXT_FENCED_CODE, - 'footnotes': EXT_FOOTNOTES, - 'autolink': EXT_AUTOLINK, - 'strikethrough': EXT_STRIKETHROUGH, - 'underline': EXT_UNDERLINE, - 'highlight': EXT_HIGHLIGHT, - 'quote': EXT_QUOTE, - 'superscript': EXT_SUPERSCRIPT, - 'math': EXT_MATH, - 'no-intra-emphasis': EXT_NO_INTRA_EMPHASIS, - 'space-headers': EXT_SPACE_HEADERS, - 'math-explicit': EXT_MATH_EXPLICIT, - 'disable-indented-code': EXT_DISABLE_INDENTED_CODE, -} - -html_flag_map = { - 'skip-html': HTML_SKIP_HTML, - 'escape': HTML_ESCAPE, - 'hard-wrap': HTML_HARD_WRAP, - 'use-xhtml': HTML_USE_XHTML, -} - - IUNIT = 1024 OUNIT = 64 MAX_NESTING = 16 -def deprecation(message): - warnings.warn(message, DeprecationWarning, stacklevel=3) - - -def to_string(buffer): - if buffer == ffi.NULL or buffer.size == 0: - return '' - return ffi.string(buffer.data, buffer.size).decode('utf-8') - - -def _args_to_int(mapping, argument): - """ - Convert list of strings to an int using a mapping. - """ - if isinstance(argument, int): - if argument == 0: - return 0 - - deprecation('passing extensions and flags as constants is deprecated') - return argument - elif isinstance(argument, (tuple, list)): - return reduce(op.or_, [mapping[n] for n in argument if n in mapping], 0) - - raise TypeError('argument must be a list of strings or an int') - - def html(text, extensions=0, render_flags=0): """ Convert markdown text to HTML. """ - extensions = _args_to_int(extension_map, extensions) - render_flags = _args_to_int(html_flag_map, render_flags) + extensions = args_to_int(extension_map, extensions) + render_flags = args_to_int(html_flag_map, render_flags) ib = lib.hoedown_buffer_new(IUNIT) ob = lib.hoedown_buffer_new(OUNIT) @@ -183,13 +113,13 @@ def smartypants(text): lib.hoedown_buffer_free(ob); -class Markdown: +class Markdown(object): """ Parses markdown text and renders it using the given renderer. """ def __init__(self, renderer, extensions=0): self.renderer = renderer - self.extensions = _args_to_int(extension_map, extensions) + self.extensions = args_to_int(extension_map, extensions) def __call__(self, text): """ @@ -214,340 +144,21 @@ class Markdown: lib.hoedown_buffer_free(ob); -_callback_signatures = { - # block level callbacks - NULL skips the block - 'blockcode': 'void(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_buffer *lang, const hoedown_renderer_data *data)', - 'blockquote': 'void(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)', - 'header': 'void(hoedown_buffer *ob, const hoedown_buffer *content, int level, const hoedown_renderer_data *data)', - 'hrule': 'void(hoedown_buffer *ob, const hoedown_renderer_data *data)', - 'list': 'void(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_list_flags flags, const hoedown_renderer_data *data)', - 'listitem': 'void(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_list_flags flags, const hoedown_renderer_data *data)', - 'paragraph': 'void(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)', - 'table': 'void(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)', - 'table_header': 'void(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)', - 'table_body': 'void(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)', - 'table_row': 'void(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)', - 'table_cell': 'void(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_table_flags flags, const hoedown_renderer_data *data)', - 'footnotes': 'void(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)', - 'footnote_def': 'void(hoedown_buffer *ob, const hoedown_buffer *content, unsigned int num, const hoedown_renderer_data *data)', - 'blockhtml': 'void(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data)', - - # span level callbacks - NULL or return 0 prints the span verbatim - 'autolink': 'int(hoedown_buffer *ob, const hoedown_buffer *link, hoedown_autolink_type type, const hoedown_renderer_data *data)', - 'codespan': 'int(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data)', - 'double_emphasis': 'int(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)', - 'emphasis': 'int(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)', - 'underline': 'int(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)', - 'highlight': 'int(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)', - 'quote': 'int(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)', - 'image': 'int(hoedown_buffer *ob, const hoedown_buffer *link, const hoedown_buffer *title, const hoedown_buffer *alt, const hoedown_renderer_data *data)', - 'linebreak': 'int(hoedown_buffer *ob, const hoedown_renderer_data *data)', - 'link': 'int(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_buffer *link, const hoedown_buffer *title, const hoedown_renderer_data *data)', - 'triple_emphasis': 'int(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)', - 'strikethrough': 'int(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)', - 'superscript': 'int(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)', - 'footnote_ref': 'int(hoedown_buffer *ob, unsigned int num, const hoedown_renderer_data *data)', - 'math': 'int(hoedown_buffer *ob, const hoedown_buffer *text, int displaymode, const hoedown_renderer_data *data)', - 'raw_html': 'int(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data)', - - # low level callbacks - NULL copies input directly into the output - 'entity': 'void(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data)', - 'normal_text': 'void(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data)', - - # miscellaneous callbacks - 'doc_header': 'void(hoedown_buffer *ob, int inline_render, const hoedown_renderer_data *data)', - 'doc_footer': 'void(hoedown_buffer *ob, int inline_render, const hoedown_renderer_data *data)', -} - - -class BaseRenderer: +class BaseRenderer(object): def __init__(self): - # Use a noop method as a placeholder for render methods that are - # implemented so there's no need to check if a render method exists - # in a callback. - for attr in _callback_signatures.keys(): - if not hasattr(self, attr): - setattr(self, attr, self.noop) + self.renderer = ffi.new('hoedown_renderer *') - self._callbacks = {k: ffi.callback(v, getattr(self, '_w_' + k)) - for k, v in _callback_signatures.items()} - self.renderer = ffi.new('hoedown_renderer *', self._callbacks) + for name in python_callbacks.keys(): + if hasattr(self, name): + setattr(self.renderer, name, python_callbacks[name]) + else: + setattr(self.renderer, name, ffi.NULL) - def noop(self, *args, **kwargs): - return None - - def _w_blockcode(self, ob, text, lang, data): - text = to_string(text) - lang = to_string(lang) - - result = self.blockcode(text, lang) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - - def _w_blockquote(self, ob, content, data): - content = to_string(content) - result = self.blockquote(content) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - - def _w_header(self, ob, content, level, data): - content = to_string(content) - level = int(level) - result = self.header(content, level) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - - def _w_hrule(self, ob, data): - result = self.hrule() - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - - # flags: LIST_ORDERED, LI_BLOCK. - def _w_list(self, ob, content, flags, data): - content = to_string(content) - flags = int(flags) - is_ordered = flags & LIST_ORDERED != 0 - is_block = flags & LI_BLOCK != 0 - result = self.list(content, is_ordered, is_block) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - - # flags: LIST_ORDERED, LI_BLOCK. - def _w_listitem(self, ob, content, flags, data): - content = to_string(content) - flags = int(flags) - is_ordered = flags & LIST_ORDERED != 0 - is_block = flags & LI_BLOCK != 0 - result = self.listitem(content, is_ordered, is_block) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - - def _w_paragraph(self, ob, content, data): - content = to_string(content) - result = self.paragraph(content) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - - def _w_table(self, ob, content, data): - content = to_string(content) - result = self.table(content) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - - def _w_table_header(self, ob, content, data): - content = to_string(content) - result = self.table_header(content) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - - def _w_table_body(self, ob, content, data): - content = to_string(content) - result = self.table_body(content) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - - def _w_table_row(self, ob, content, data): - content = to_string(content) - result = self.table_row(content) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - - # flags: TABLE_ALIGNMASK, TABLE_ALIGN_LEFT, TABLE_ALIGN_RIGHT, - # TABLE_ALIGN_CENTER, TABLE_HEADER - def _w_table_cell(self, ob, content, flags, data): - content = to_string(content) - flags = int(flags) - is_header = flags & TABLE_HEADER != 0 - align_bit = flags & TABLE_ALIGNMASK - - if align_bit == TABLE_ALIGN_CENTER: - align = 'center' - elif align_bit == TABLE_ALIGN_LEFT: - align = 'left' - elif align_bit == TABLE_ALIGN_RIGHT: - align = 'right' - else: - align = '' - - result = self.table_cell(content, align, is_header) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - - def _w_footnotes(self, ob, content, data): - content = to_string(content) - result = self.footnotes(content) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - - def _w_footnote_def(self, ob, content, num, data): - content = to_string(content) - num = int(num) - result = self.footnote_def(content, num) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - - def _w_blockhtml(self, ob, text, data): - text = ffi.string(text.data, text.size).decode('utf-8') - result = self.blockhtml(text) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - - def _w_autolink(self, ob, link, type, data): - link = ffi.string(link.data, link.size).decode('utf-8') - is_email = int(type) & AUTOLINK_EMAIL != 0 - result = self.autolink(link, is_email) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - return 1 - return 0 - - def _w_codespan(self, ob, text, data): - text = ffi.string(text.data, text.size).decode('utf-8') - result = self.codespan(text) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - return 1 - return 0 - - def _w_double_emphasis(self, ob, content, data): - content = to_string(content) - result = self.double_emphasis(content) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - return 1 - return 0 - - def _w_emphasis(self, ob, content, data): - content = to_string(content) - result = self.emphasis(content) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - return 1 - return 0 - - def _w_underline(self, ob, content, data): - content = to_string(content) - result = self.underline(content) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - return 1 - return 0 - - def _w_highlight(self, ob, content, data): - content = to_string(content) - result = self.highlight(content) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - return 1 - return 0 - - def _w_quote(self, ob, content, data): - content = to_string(content) - result = self.quote(content) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - return 1 - return 0 - - def _w_image(self, ob, link, title, alt, data): - link = to_string(link) - title = to_string(title) - alt = to_string(alt) - result = self.image(link, title, alt) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - return 1 - return 0 - - def _w_linebreak(self, ob, data): - result = self.linebreak() - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - return 1 - return 0 - - def _w_link(self, ob, content, link, title, data): - content = to_string(content) - link = to_string(link) - title = to_string(title) - result = self.link(content, link, title) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - return 1 - return 0 - - def _w_triple_emphasis(self, ob, content, data): - content = to_string(content) - result = self.triple_emphasis(content) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - return 1 - return 0 - - def _w_strikethrough(self, ob, content, data): - content = to_string(content) - result = self.strikethrough(content) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - return 1 - return 0 - - def _w_superscript(self, ob, content, data): - content = to_string(content) - result = self.superscript(content) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - return 1 - return 0 - - def _w_footnote_ref(self, ob, num, data): - num = int(num) - result = self.footnote_ref(num) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - return 1 - return 0 - - def _w_math(self, ob, text, displaymode, data): - text = to_string(text) - displaymode = int(displaymode) - result = self.math(text, displaymode) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - return 1 - return 0 - - def _w_raw_html(self, ob, text, data): - text = to_string(text) - result = self.raw_html(text) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - return 1 - return 0 - - def _w_entity(self, ob, text, data): - text = to_string(text) - result = self.entity(text) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - - def _w_normal_text(self, ob, text, data): - text = to_string(text) - result = self.normal_text(text) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - - def _w_doc_header(self, ob, inline_render, data): - inline_render = int(inline_render) - result = self.doc_header(inline_render) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) - - def _w_doc_footer(self, ob, inline_render, data): - inline_render = int(inline_render) - result = self.doc_footer(inline_render) - if result: - lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + # Store the render class' handle in the render data. + self._data = ffi.new('hoedown_renderer_data *') + self.renderer.opaque = self._data + ffi.cast('hoedown_renderer_data *', self.renderer.opaque).opaque = \ + ffi.new_handle(self) class HtmlRenderer(BaseRenderer): @@ -562,21 +173,16 @@ class HtmlRenderer(BaseRenderer): by the ``Markdown`` instance. """ def __init__(self, flags=0, nesting_level=0): - flags = _args_to_int(html_flag_map, flags) + flags = args_to_int(html_flag_map, flags) self.renderer = self._new_renderer(flags, nesting_level) - callbacks = [] - for name, signature in _callback_signatures.items(): - if not hasattr(self, name): - continue + # Store the render class' handle in the render state. + state = ffi.cast('hoedown_renderer_data *', self.renderer.opaque) + state.opaque = ffi.new_handle(self) - wrapper = getattr(self, '_w_' + name) - callback = ffi.callback(signature, wrapper) - callbacks.append(callback) - setattr(self.renderer, name, callback) - - # Prevent garbage collection of callbacks. - self._callbacks = callbacks + for name in python_callbacks.keys(): + if hasattr(self, name): + setattr(self.renderer, name, python_callbacks[name]) def _new_renderer(self, flags, nesting_level): return lib.hoedown_html_renderer_new(flags, nesting_level) diff --git a/misaka/callbacks.py b/misaka/callbacks.py new file mode 100644 index 0000000..047e117 --- /dev/null +++ b/misaka/callbacks.py @@ -0,0 +1,429 @@ +# -*- coding: utf-8 -*- + +from ._hoedown import lib, ffi +from .constants import * +from .utils import to_string + + +@ffi.callback('void(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_buffer *lang, const hoedown_renderer_data *data)') +def cb_blockcode(ob, text, lang, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + text = to_string(text) + lang = to_string(lang) + + result = renderer.blockcode(text, lang) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + + +@ffi.callback('void(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)') +def cb_blockquote(ob, content, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + content = to_string(content) + result = renderer.blockquote(content) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + + +@ffi.callback('void(hoedown_buffer *ob, const hoedown_buffer *content, int level, const hoedown_renderer_data *data)') +def cb_header(ob, content, level, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + content = to_string(content) + level = int(level) + result = renderer.header(content, level) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + + +@ffi.callback('void(hoedown_buffer *ob, const hoedown_renderer_data *data)') +def cb_hrule(ob, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + result = renderer.hrule() + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + + +# flags: LIST_ORDERED, LI_BLOCK. +@ffi.callback('void(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_list_flags flags, const hoedown_renderer_data *data)') +def cb_list(ob, content, flags, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + content = to_string(content) + flags = int(flags) + is_ordered = flags & LIST_ORDERED != 0 + is_block = flags & LI_BLOCK != 0 + result = renderer.list(content, is_ordered, is_block) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + + +# flags: LIST_ORDERED, LI_BLOCK. +@ffi.callback('void(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_list_flags flags, const hoedown_renderer_data *data)') +def cb_listitem(ob, content, flags, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + content = to_string(content) + flags = int(flags) + is_ordered = flags & LIST_ORDERED != 0 + is_block = flags & LI_BLOCK != 0 + result = renderer.listitem(content, is_ordered, is_block) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + + +@ffi.callback('void(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)') +def cb_paragraph(ob, content, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + content = to_string(content) + result = renderer.paragraph(content) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + + +@ffi.callback('void(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)') +def cb_table(ob, content, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + content = to_string(content) + result = renderer.table(content) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + + +@ffi.callback('void(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)') +def cb_table_header(ob, content, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + content = to_string(content) + result = renderer.table_header(content) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + + +@ffi.callback('void(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)') +def cb_table_body(ob, content, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + content = to_string(content) + result = renderer.table_body(content) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + + +@ffi.callback('void(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)') +def cb_table_row(ob, content, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + content = to_string(content) + result = renderer.table_row(content) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + + +# flags: TABLE_ALIGNMASK, TABLE_ALIGN_LEFT, TABLE_ALIGN_RIGHT, +# TABLE_ALIGN_CENTER, TABLE_HEADER +@ffi.callback('void(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_table_flags flags, const hoedown_renderer_data *data)') +def cb_table_cell(ob, content, flags, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + content = to_string(content) + flags = int(flags) + is_header = flags & TABLE_HEADER != 0 + align_bit = flags & TABLE_ALIGNMASK + + if align_bit == TABLE_ALIGN_CENTER: + align = 'center' + elif align_bit == TABLE_ALIGN_LEFT: + align = 'left' + elif align_bit == TABLE_ALIGN_RIGHT: + align = 'right' + else: + align = '' + + result = renderer.table_cell(content, align, is_header) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + + +@ffi.callback('void(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)') +def cb_footnotes(ob, content, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + content = to_string(content) + result = renderer.footnotes(content) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + + +@ffi.callback('void(hoedown_buffer *ob, const hoedown_buffer *content, unsigned int num, const hoedown_renderer_data *data)') +def cb_footnote_def(ob, content, num, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + content = to_string(content) + num = int(num) + result = renderer.footnote_def(content, num) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + + +@ffi.callback('void(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data)') +def cb_blockhtml(ob, text, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + text = ffi.string(text.data, text.size).decode('utf-8') + result = renderer.blockhtml(text) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + + +@ffi.callback('int(hoedown_buffer *ob, const hoedown_buffer *link, hoedown_autolink_type type, const hoedown_renderer_data *data)') +def cb_autolink(ob, link, type, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + link = ffi.string(link.data, link.size).decode('utf-8') + is_email = int(type) & AUTOLINK_EMAIL != 0 + result = renderer.autolink(link, is_email) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + return 1 + return 0 + + +@ffi.callback('int(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data)') +def cb_codespan(ob, text, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + text = ffi.string(text.data, text.size).decode('utf-8') + result = renderer.codespan(text) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + return 1 + return 0 + + +@ffi.callback('int(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)') +def cb_double_emphasis(ob, content, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + content = to_string(content) + result = renderer.double_emphasis(content) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + return 1 + return 0 + + +@ffi.callback('int(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)') +def cb_emphasis(ob, content, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + content = to_string(content) + result = renderer.emphasis(content) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + return 1 + return 0 + + +@ffi.callback('int(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)') +def cb_underline(ob, content, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + content = to_string(content) + result = renderer.underline(content) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + return 1 + return 0 + + +@ffi.callback('int(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)') +def cb_highlight(ob, content, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + content = to_string(content) + result = renderer.highlight(content) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + return 1 + return 0 + + +@ffi.callback('int(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)') +def cb_quote(ob, content, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + content = to_string(content) + result = renderer.quote(content) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + return 1 + return 0 + + +@ffi.callback('int(hoedown_buffer *ob, const hoedown_buffer *link, const hoedown_buffer *title, const hoedown_buffer *alt, const hoedown_renderer_data *data)') +def cb_image(ob, link, title, alt, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + link = to_string(link) + title = to_string(title) + alt = to_string(alt) + result = renderer.image(link, title, alt) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + return 1 + return 0 + + +@ffi.callback('int(hoedown_buffer *ob, const hoedown_renderer_data *data)') +def cb_linebreak(ob, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + result = renderer.linebreak() + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + return 1 + return 0 + + +@ffi.callback('int(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_buffer *link, const hoedown_buffer *title, const hoedown_renderer_data *data)') +def cb_link(ob, content, link, title, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + content = to_string(content) + link = to_string(link) + title = to_string(title) + result = renderer.link(content, link, title) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + return 1 + return 0 + + +@ffi.callback('int(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)') +def cb_triple_emphasis(ob, content, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + content = to_string(content) + result = renderer.triple_emphasis(content) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + return 1 + return 0 + + +@ffi.callback('int(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)') +def cb_strikethrough(ob, content, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + content = to_string(content) + result = renderer.strikethrough(content) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + return 1 + return 0 + + +@ffi.callback('int(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)') +def cb_superscript(ob, content, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + content = to_string(content) + result = renderer.superscript(content) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + return 1 + return 0 + + +@ffi.callback('int(hoedown_buffer *ob, unsigned int num, const hoedown_renderer_data *data)') +def cb_footnote_ref(ob, num, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + num = int(num) + result = renderer.footnote_ref(num) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + return 1 + return 0 + + +@ffi.callback('int(hoedown_buffer *ob, const hoedown_buffer *text, int displaymode, const hoedown_renderer_data *data)') +def cb_math(ob, text, displaymode, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + text = to_string(text) + displaymode = int(displaymode) + result = renderer.math(text, displaymode) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + return 1 + return 0 + + +@ffi.callback('int(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data)') +def cb_raw_html(ob, text, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + text = to_string(text) + result = renderer.raw_html(text) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + return 1 + return 0 + + +@ffi.callback('void(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data)') +def cb_entity(ob, text, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + text = to_string(text) + result = renderer.entity(text) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + + +@ffi.callback('void(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data)') +def cb_normal_text(ob, text, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + text = to_string(text) + result = renderer.normal_text(text) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + + +@ffi.callback('void(hoedown_buffer *ob, int inline_render, const hoedown_renderer_data *data)') +def cb_doc_header(ob, inline_render, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + inline_render = int(inline_render) + result = renderer.doc_header(inline_render) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + + +@ffi.callback('void(hoedown_buffer *ob, int inline_render, const hoedown_renderer_data *data)') +def cb_doc_footer(ob, inline_render, data): + renderer = ffi.from_handle(lib.misaka_get_renderer(data)) + inline_render = int(inline_render) + result = renderer.doc_footer(inline_render) + if result: + lib.hoedown_buffer_puts(ob, result.encode('utf-8')) + + +python_callbacks = { + # block level callbacks - NULL skips the block + 'blockcode': cb_blockcode, + 'blockquote': cb_blockquote, + 'header': cb_header, + 'hrule': cb_hrule, + 'list': cb_list, + 'listitem': cb_listitem, + 'paragraph': cb_paragraph, + 'table': cb_table, + 'table_header': cb_table_header, + 'table_body': cb_table_body, + 'table_row': cb_table_row, + 'table_cell': cb_table_cell, + 'footnotes': cb_footnotes, + 'footnote_def': cb_footnote_def, + 'blockhtml': cb_blockhtml, + + # span level callbacks - NULL or return 0 prints the span verbatim + 'autolink': cb_autolink, + 'codespan': cb_codespan, + 'double_emphasis': cb_double_emphasis, + 'emphasis': cb_emphasis, + 'underline': cb_underline, + 'highlight': cb_highlight, + 'quote': cb_quote, + 'image': cb_image, + 'linebreak': cb_linebreak, + 'link': cb_link, + 'triple_emphasis': cb_triple_emphasis, + 'strikethrough': cb_strikethrough, + 'superscript': cb_superscript, + 'footnote_ref': cb_footnote_ref, + 'math': cb_math, + 'raw_html': cb_raw_html, + + # low level callbacks - NULL copies input directly into the output + 'entity': cb_entity, + 'normal_text': cb_normal_text, + + # miscellaneous callbacks + 'doc_header': cb_doc_header, + 'doc_footer': cb_doc_footer, +} diff --git a/misaka/constants.py b/misaka/constants.py new file mode 100644 index 0000000..27cf83e --- /dev/null +++ b/misaka/constants.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +import sys +from inspect import getmembers + +from ._hoedown import lib + + +def _set_constants(): + is_int = lambda n: isinstance(n, int) + + for name, value in getmembers(lib, is_int): + if not name.startswith('HOEDOWN_'): + continue + setattr(sys.modules[__name__], name[8:], value) + + +if not hasattr(sys.modules[__name__], 'EXT_TABLES'): + _set_constants() diff --git a/misaka/extra.c b/misaka/extra.c new file mode 100644 index 0000000..555406e --- /dev/null +++ b/misaka/extra.c @@ -0,0 +1,9 @@ +#include "hoedown/document.h" +#include "hoedown/html.h" + +void *misaka_get_renderer(const hoedown_renderer_data *data) { + // NOTE: Cast to a "hoedown_renderer_data *", because + // the structure is assumed to have an "opaque" field. + // Otherwise this doesn't work. + return ((hoedown_renderer_data *) data->opaque)->opaque; +} diff --git a/misaka/extra.h b/misaka/extra.h new file mode 100644 index 0000000..61fb8c1 --- /dev/null +++ b/misaka/extra.h @@ -0,0 +1,3 @@ +/* extra.h - extra stuff in C */ + +void *misaka_get_renderer(const hoedown_renderer_data *data); diff --git a/misaka/utils.py b/misaka/utils.py new file mode 100644 index 0000000..e24e3cf --- /dev/null +++ b/misaka/utils.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- + +import operator as op +import warnings + +try: + reduce +except NameError: + from functools import reduce + +from ._hoedown import ffi +from .constants import * + + +extension_map = { + 'tables': EXT_TABLES, + 'fenced-code': EXT_FENCED_CODE, + 'footnotes': EXT_FOOTNOTES, + 'autolink': EXT_AUTOLINK, + 'strikethrough': EXT_STRIKETHROUGH, + 'underline': EXT_UNDERLINE, + 'highlight': EXT_HIGHLIGHT, + 'quote': EXT_QUOTE, + 'superscript': EXT_SUPERSCRIPT, + 'math': EXT_MATH, + 'no-intra-emphasis': EXT_NO_INTRA_EMPHASIS, + 'space-headers': EXT_SPACE_HEADERS, + 'math-explicit': EXT_MATH_EXPLICIT, + 'disable-indented-code': EXT_DISABLE_INDENTED_CODE, +} + +html_flag_map = { + 'skip-html': HTML_SKIP_HTML, + 'escape': HTML_ESCAPE, + 'hard-wrap': HTML_HARD_WRAP, + 'use-xhtml': HTML_USE_XHTML, +} + + +def args_to_int(mapping, argument): + """ + Convert list of strings to an int using a mapping. + """ + if isinstance(argument, int): + if argument == 0: + return 0 + deprecation('passing extensions and flags as constants is deprecated') + return argument + elif isinstance(argument, (tuple, list)): + return reduce(op.or_, [mapping[n] for n in argument if n in mapping], 0) + raise TypeError('argument must be a list of strings or an int') + + +def deprecation(message): + warnings.warn(message, DeprecationWarning, stacklevel=3) + + +def to_string(buffer): + if buffer == ffi.NULL or buffer.size == 0: + return '' + return ffi.string(buffer.data, buffer.size).decode('utf-8') diff --git a/tests/test_misc.py b/tests/test_misc.py index c320c60..daa2d38 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -1,14 +1,15 @@ # -*- coding: utf-8 -*- from chibitest import TestCase, ok -from misaka import _args_to_int, extension_map, \ +from misaka import extension_map, \ EXT_TABLES, EXT_FENCED_CODE, EXT_FOOTNOTES +from misaka.utils import args_to_int class ArgsToIntTest(TestCase): def test_args(self): expected = EXT_TABLES | EXT_FENCED_CODE | EXT_FOOTNOTES - result = _args_to_int( + result = args_to_int( extension_map, ('tables', 'fenced-code', 'footnotes')) @@ -16,6 +17,6 @@ class ArgsToIntTest(TestCase): def test_int(self): expected = EXT_TABLES | EXT_FENCED_CODE | EXT_FOOTNOTES - result = _args_to_int(extension_map, expected) + result = args_to_int(extension_map, expected) ok(result) == expected