def exercise_filter(elem, doc): if isinstance(elem, pf.Para) and len(elem.content) == 0: return [] ## Remove empty paragraphs ... elif isinstance(elem, pf.Header) and ( "exercise" in elem.classes): # No need to use level 3 and elem.level==3: if "reset" in elem.classes: doc.exercisecount = 1 else: doc.exercisecount += 1 #print(sys.stderr,"Exercise detected",file=sys.stderr) doc.inside_exercise = True return [] elif isinstance(elem, pf.Header) and ( "question" in elem.classes): # No need to use level 3 and elem.level==3: if "reset" in elem.classes: doc.questioncount = 1 else: doc.questioncount += 1 #print(sys.stderr,"Exercise detected",file=sys.stderr) doc.inside_question = True return [] elif doc.inside_exercise: if isinstance(elem, pf.Para) and len(elem.content) > 0: cogollo = pf.Str(str(doc.exercisecount) + ".-") elem.content = [pf.Strong(cogollo), pf.Space] + list(elem.content) doc.inside_exercise = False return elem elif doc.inside_question: if isinstance(elem, pf.Para) and len(elem.content) > 0: cogollo = pf.Str(str(doc.questioncount) + ".-") elem.content = [pf.Strong(cogollo), pf.Space] + list(elem.content) doc.inside_question = False return elem
def action(elem, doc): """The panflute filter main method, called once per element.""" global shift, workaround_level_overflow, workaround_level_underflow if isinstance(elem, pf.Header): level_old = elem.level level_new = level_old + shift if level_new > MAX_LEVEL: eprint( "After shifting header levels by %d, '%s' would be on level %d, " "which is above the max level %d." % (shift, elem.identifier, level_new, MAX_LEVEL)) if workaround_level_overflow: eprint( "Thus we convert it to an emphazised text paragraph instead." ) if level_new == (MAX_LEVEL + 1): elem = pf.Para(pf.Strong(*elem.content)) else: elem = pf.Para(pf.Emph(*elem.content)) else: raise OverflowError() elif level_new < MIN_LEVEL: eprint( "After shifting header levels by %d, '%s' would be on level %d, " "which is below the min level %d." % (shift, elem.identifier, level_new, MIN_LEVEL)) if workaround_level_underflow: eprint("Thus we leave it at the min level.") else: raise OverflowError() else: elem.level = level_new return elem
def header(elem, doc): if not isinstance(elem, pf.Plain): return None return pf.Div(pf.Plain(pf.RawInline('\\centering', 'latex'), pf.Strong(*elem.content)), attributes={'style': 'text-align:center'})
def build_header(elem): # We use a `pf.RawInline` here because setting the `align` # attribute on `pf.Div` does not work for some reason. header = pf.Plain(pf.RawInline('<div align="center">', 'html'), pf.Strong(*elem.content), pf.RawInline('</div>', 'html')) width = float( elem.attributes['width']) if 'width' in elem.attributes else 0 return header, width
def action(elem, doc): if isinstance(elem, pf.Image) and doc.format == 'html': global counter strong = pf.Strong(pf.Str(f'Obr. {counter}: ')) elem.content.insert(0, strong) counter += 1 elem.classes.append('img-responsive')
def action(elem, doc): if type(elem) == pf.Str: if len(match_key.findall(elem.text)) > 0: var_full = match_full.findall(elem.text)[0] var_key = match_key.findall(elem.text)[0] if var_key in doc.variables: new_string = elem.text.replace(var_full, str(doc.variables[var_key])) if doc.boldface_variables: var_value = pf.Strong(pf.Str(new_string)) else: var_value = pf.Str(new_string) else: var_value = pf.Strong( pf.Str('{{Missing variable: {}}}'.format(var_key))) doc.missing_variables.add(var_key) return (var_value)
def _unknown_environment_debug(env_name, elem): """Handle unknown latex environment. Output visual feedback about the unknown environment. """ classes = ELEMENT_CLASSES["DEBUG_UNKNOWN_ENV"] + [slugify(env_name)] div = pf.Div(classes=classes) div.content.extend([ pf.Para(pf.Strong(*destringify("Unhandled environment:"))), pf.CodeBlock(elem.text), ]) return div
def test_to_inline(self): """It should convert different elements correctly to inline""" content1 = pf.Para(pf.Strong(pf.Str("just some text"))) transformed1 = content1.content[0] content2 = pf.Div( pf.Para(pf.Strong(pf.Str("again")), pf.Space, pf.Emph(pf.Str("normal")))) content3 = pf.Div( pf.Para( pf.Span(pf.Str("foo"), classes=["1st-span-class"]), pf.Span( pf.Strong(pf.Str("Unhandled"), pf.Space, pf.Str("command:")), classes=["2nd-span-class"], ), ), pf.CodeBlock(r"\MLFunctionQuestion{10}{sin(x)}{5}{x}{5}{DS2}"), classes=["div-class"], ) self.assertEqual(to_inline(content1), transformed1) # test if nested inlining works il_content2 = to_inline(content2) self.assertIsInstance(il_content2.content[0], pf.Strong) self.assertEqual(il_content2.content[0].content[0].text, "again") self.assertIsInstance(il_content2.content[2], pf.Emph) # test if class conservation works and advanced nesting il_content3 = to_inline(content3) self.assertEqual(len(il_content3.content), 2) self.assertEqual(len(il_content3.content[0].content), 2) self.assertEqual(il_content3.classes, ["div-class"]) self.assertEqual(il_content3.content[0].content[0].classes, ["1st-span-class"]) self.assertEqual(il_content3.content[0].content[1].classes, ["2nd-span-class"])
def _unknown_command_debug(cmd_name, elem): """Handle unknown latex commands. Output visual feedback about the unknown command. """ classes = ELEMENT_CLASSES["DEBUG_UNKNOWN_CMD"] + [slugify(cmd_name)] msg_prefix = pf.Strong(*destringify("Unhandled command:")) if isinstance(elem, pf.Block): div = pf.Div(classes=classes) div.content.extend([pf.Para(msg_prefix), pf.CodeBlock(elem.text)]) return div # RawInline span = pf.Span(classes=classes) span.content.extend([msg_prefix, pf.Space(), pf.Code(elem.text)]) return span
def handle_mentry(self, cmd_args, elem): r"""Handle ``\MEntry`` command. This command creates an entry for the index. """ if isinstance(elem, pf.Block): log("Warning: Expected Inline for MEntry: {}".format(cmd_args)) text = cmd_args[0] concept = cmd_args[1] for repl in MATH_SUBSTITUTIONS: # can contain LaTeX! concept = re.sub(repl[0], repl[1], concept) strong = pf.Strong() strong.content.extend( parse_fragment(text, elem.doc.metadata["lang"].text)[0].content) span = pf.Span() span.attributes = {INDEX_ATTRIBUTE: concept} span.content = [strong] return block_wrap(span, elem)
def test_all(): md = 'Some *markdown* **text** ~xyz~' c_md = pf.convert_text(md) b_md = [ pf.Para(pf.Str("Some"), pf.Space, pf.Emph(pf.Str("markdown")), pf.Space, pf.Strong(pf.Str("text")), pf.Space, pf.Subscript(pf.Str("xyz"))) ] print("Benchmark MD:") print(b_md) print("Converted MD:") print(c_md) assert repr(c_md) == repr(b_md) with io.StringIO() as f: doc = pf.Doc(*c_md) pf.dump(doc, f) c_md_dump = f.getvalue() with io.StringIO() as f: doc = pf.Doc(*b_md) pf.dump(doc, f) b_md_dump = f.getvalue() assert c_md_dump == b_md_dump # ---------------------- print() tex = r'Some $x^y$ or $x_n = \sqrt{a + b}$ \textit{a}' c_tex = pf.convert_text(tex) b_tex = [ pf.Para(pf.Str("Some"), pf.Space, pf.Math("x^y", format='InlineMath'), pf.Space, pf.Str("or"), pf.Space, pf.Math(r"x_n = \sqrt{a + b}", format='InlineMath'), pf.Space, pf.RawInline(r"\textit{a}", format='tex')) ] print("Benchmark TEX:") print(b_tex) print("Converted TEX:") print(c_tex) assert repr(c_tex) == repr(b_tex) with io.StringIO() as f: doc = pf.Doc(*c_tex) pf.dump(doc, f) c_tex_dump = f.getvalue() with io.StringIO() as f: doc = pf.Doc(*b_tex) pf.dump(doc, f) b_tex_dump = f.getvalue() assert c_tex_dump == b_tex_dump print("\nBack and forth conversions... md->json->md") md = 'Some *markdown* **text** ~xyz~' print("[MD]", md) md2json = pf.convert_text(md, input_format='markdown', output_format='json') print("[JSON]", md2json) md2json2md = pf.convert_text(md2json, input_format='json', output_format='markdown') print("[MD]", md2json2md) assert md == md2json2md print("\nBack and forth conversions... md->panflute->md") md = 'Some *markdown* **text** ~xyz~' print("[MD]", md) md2panflute = pf.convert_text(md, input_format='markdown', output_format='panflute') print("[PANFLUTE]", md2panflute) md2panflute2md = pf.convert_text(md2panflute, input_format='panflute', output_format='markdown') print("[MD]", md2panflute2md) assert md == md2panflute2md print("\nBack and forth conversions... md->panflute(standalone)->md") md = 'Some *markdown* **text** ~xyz~' print("[MD]", md) md2panflute = pf.convert_text(md, input_format='markdown', output_format='panflute', standalone=True) print("[PANFLUTE]", md2panflute) md2panflute2md = pf.convert_text(md2panflute, input_format='panflute', output_format='markdown') print("[MD]", md2panflute2md) assert md == md2panflute2md print( "\nBack and forth conversions... md table -> json(standalone) -> md table" ) md = """lorem --- --- x y --- --- ipsum""" print("[MD]", repr(md)) md2json = pf.convert_text(md, input_format='markdown', output_format='json', standalone=True) print("[json]", md2json) md2json2md = pf.convert_text(md2json, input_format='json', output_format='markdown') print("[MD]", repr(md2json2md)) assert md == md2json2md print( "\nBack and forth conversions... md table -> panflute(standalone) -> md table" ) print("[MD]", repr(md)) md2panflute = pf.convert_text(md, input_format='markdown', output_format='panflute', standalone=True) print("[PANFLUTE]", md2panflute) md2panflute2md = pf.convert_text(md2panflute, input_format='panflute', output_format='markdown') print("[MD]", repr(md2panflute2md)) assert md == md2panflute2md print( "\nBack and forth conversions... gfm table (empty) -> json(standalone) -> gfm table (empty)" ) md = """lorem | x | y | | - | - | ipsum""" print("[MD]", repr(md)) md2json = pf.convert_text(md, input_format='gfm', output_format='json', standalone=True) print("[json]", md2json) md2json2md = pf.convert_text(md2json, input_format='json', output_format='gfm') print("[MD]", repr(md2json2md)) assert md == md2json2md print( "\nBack and forth conversions... gfm table (empty) -> panflute(standalone) -> gfm table (empty)" ) print("[MD]", repr(md)) md2panflute = pf.convert_text(md, input_format='gfm', output_format='panflute', standalone=True) print("[PANFLUTE]", md2panflute) md2panflute2md = pf.convert_text(md2panflute, input_format='panflute', output_format='gfm') print("[MD]", repr(md2panflute2md)) assert md == md2panflute2md
def convert(self): doc = panflute.Doc( api_version=(1, 17, 5), metadata={ 'pagetitle': self.title, }, ) doc.content.append(panflute.Header(panflute.Str(self.title))) lists = {} tables = {} table_rows = {} table_cells = {} for chunk in self._attr_chunks(): self.logger.debug(chunk) container = panflute.Para() cdiv = panflute.Div(container) # Handle lists if 'list_class' in chunk[0]['attrs']: lc = chunk[0]['attrs']['list_class'] check_state = None if lc in ['checked', 'unchecked']: check_state = lc lc = 'checklist' ld = chunk[0]['attrs']['list_depth'] # prune any lists that are lower than us, they're finished for i in list(lists.keys()): if i > ld: lists.pop(i) # non-homogenous list types can be immediately adjacent without # ending up merged if ld in lists and lists[ld]['class'] != lc: lists.pop(ld) # checklists are a special case, they can't contain other lists if lc != 'checklist' and lists and lists[1][ 'class'] == 'checklist': lists = {} # make sure any intermediate lists were created, including # the top level because boxnotes for i in range(1, ld + 1): if i not in lists: lists[i] = self._list(lc, i) if i != ld: lists[i]['pf'].content.append(panflute.ListItem()) lp = lists[i]['pf'] if lc == 'checklist': lp = panflute.Div(lp, classes=['checklist']) if i == 1: doc.content.append(lp) else: lists[i - 1]['pf'].content[-1].content.append(lp) # set the container for the other subchunks container = panflute.Plain() cdiv.content = [container] cdiv.classes.append(lc) if check_state: cdiv.classes.append(check_state) lists[ld]['pf'].content.append(panflute.ListItem(cdiv)) if check_state == 'checked': container.content.append(panflute.Str(CHECKED)) elif check_state == 'unchecked': container.content.append(panflute.Str(UNCHECKED)) elif 'table_id' in chunk[-1]['attrs']: table_id = chunk[-1]['attrs']['table_id'] row_id = chunk[-1]['attrs']['table_row'] cell_id = row_id + chunk[-1]['attrs']['table_col'] if table_id not in tables: # There's some magic in the constructor for panflute tables # that isn't exposed in any other way, so we can't create # the table until we've finished populating the rows. # Instead, use a placeholder div to locate it within the # document. tables[table_id] = { 'div': panflute.Div(), 'rows': [], } doc.content.append(tables[table_id]['div']) if row_id not in table_rows: table_rows[row_id] = panflute.TableRow() tables[table_id]['rows'].append(table_rows[row_id]) if cell_id not in table_cells: cdiv = panflute.Div(panflute.Plain()) table_cells[cell_id] = panflute.TableCell(cdiv) table_rows[row_id].content.append(table_cells[cell_id]) container = table_cells[cell_id].content[0].content[0] else: lists = {} doc.content.append(cdiv) if 'align' in chunk[0]['attrs']: cdiv.attributes['style'] = 'text-align: ' + chunk[0]['attrs'][ 'align'] + ';' for subchunk in chunk: if subchunk['newlines'] > 1: # we've had an extra linebreak, no more adding on to lists lists = {} # don't do anything with markers if subchunk['text'] == '*' and 'lmkr' in subchunk['attrs']: continue scont = container if 'href' in subchunk['attrs']: scont = panflute.Link(url=subchunk['attrs']['href']) container.content.append(scont) if 'image' in subchunk['attrs']: scont.content.append( panflute.Image( url=self._image(subchunk['attrs']['author'], subchunk['attrs']['image']))) continue span = panflute.Span() lines = subchunk['text'].splitlines() while lines: subtext = lines.pop(0) span.content.append(panflute.Str(subtext)) if lines: span.content.append(panflute.LineBreak()) if 'font' in subchunk['attrs']: color = subchunk['attrs']['font'].get('color', '000000') size = subchunk['attrs']['font'].get('size', 'medium') span.classes.append('font-size-' + size) span.classes.append('font-color-' + color) # I don't actually know what the possible colors are and I # don't feel like finding out, so just inject it as an # inline style. if color != '000000': span.attributes['style'] = 'color: #' + color + ';' if subchunk['attrs'].get('underline'): span.classes.append('underline') if subchunk['attrs'].get('bold'): span = panflute.Strong(span) if subchunk['attrs'].get('italic'): span = panflute.Emph(span) if subchunk['attrs'].get('strikethrough'): span = panflute.Strikeout(span) scont.content.append(span) # Actually create the tables for x in tables: tables[x]['div'].content.append(panflute.Table(*tables[x]['rows'])) with io.StringIO() as f: panflute.dump(doc, f) return f.getvalue()
def handle_modstextbf(self, cmd_args, elem): r"""Handle \modstextbf command.""" return pf.Strong(*parse_fragment( cmd_args[0], elem.doc.metadata["lang"].text)[0].content)
def action(elem, doc): if isinstance(elem, pf.CodeBlock): import pdb pdb.set_trace() if isinstance(elem, pf.Emph): return pf.Strong(*elem.content)
def plain(elem: Union[panflute.Div, panflute.Header], doc: panflute.Doc) -> Optional[panflute.Div]: """Assemble the plain text version of the acronym list The base plain text output is a bulleted list of acronyms in the following format:: - {short}: {long} in a new :class:`panflute.Div` with the identifier “acronym-list”. If the given element is a :class:`panflute.Div`, the list is placed under a level 1 header with the text “Acronyms” unless the ``name`` or ``level`` attributes are set in which case, the request is honored. The list is sorted by the short version of the acronyms by default unless the ``sort`` attribute is set to “false” (case insensitive) in which case the order is unspecified. If an attribute cannot be interpreted, it is omitted and a warning is logged. Parameters ---------- elem: :class:`panflute.Div` or :class:`panflute.Header` The element to replace doc: :class:`panflute.Doc` The document under consideration. Returns ------- :class:`panflute.Div`, optional: The replacement for the block. """ logger = logging.getLogger(__name__ + ".plain_text") if "acronyms" not in doc.metadata: return None if isinstance(elem, panflute.Header): header = elem elif isinstance(elem, panflute.Div): header = panflute.Header(panflute.Str( elem.attributes.get("name", "Acronyms")), level=elem.attributes.get("level", 1)) else: cls = type(elem) logger.warning(f"Unknown element type {cls}") return None if "sort" in elem.attributes: sort = elem.attributes["sort"].lower() if sort not in ("true", "false"): sort = "true" logger.warning(f"Unknown 'sort' option '{sort}'") else: sort = "true" if sort == "true": acronyms = sorted(doc.acronyms.values(), key=lambda x: x["short"]) else: acronyms = doc.acronyms.values() acrolist = [ panflute.ListItem( panflute.Plain(panflute.Strong(panflute.Str(acro["short"])), panflute.Str(":"), panflute.Space, *panflute.convert_text(acro["long"])[0].content)) for acro in acronyms if acro["list"] ] return panflute.Div(header, panflute.BulletList(*acrolist), identifier="acronym-list")