def block_html(self, html): tokens = html.split('\n', 1) if len(tokens) == 2: first_line, rest = html.split('\n', 1) else: first_line = html rest = "" tags = re.match('^<(.+?)>(.*)', html) renderer = WikiRenderer() parser = mistune.Markdown(renderer=renderer) if tags: tag, tag_tail = tags.groups() rest = tag_tail + rest submd = parser.render(unicode(rest)) ret = "<%s>%s" % (tag, submd) else: ret = first_line + parser.render(unicode(rest.lstrip())) return ret
def POST(self): data = web.input() toc = TocRenderer() toc.options = {'escape': True, 'hard_wrap': True, 'use_xhtml': True} md = mistune.Markdown(renderer=toc) toc.reset_toc() dd = md.parse(data['data']) rv = toc.render_toc(level=4) re_itm = re.search(r'<li>', rv) if not (re.search(r'<li>', rv)): rv = re.sub(r'<\/li>', '', rv) try: rr = minidom.parseString(rv) except ExpatError as exc: rv = re.sub(r'<\/ul>\n<\/li>', '', rv) rv = re.sub(r'<ul>', '</li>', rv) data = '<?xml version="1.0" encoding="utf-8" ?>\n<reply>\n<preview>\n<div id="documentView" class="markdown-body">\n' + dd + '\n</div>\n</preview>\n<toc>\n' + rv + '\n</toc>\n<comments>\n</comments>\n<annotations>\n</annotations>\n</reply>' return data
def post(slug): article = ARTICLES_PATH / f'{slug}.md' try: file = open(article, mode='r', encoding='UTF-8') except FileNotFoundError: return abort(404) with file: md_content = file.read() renderer = BlogRenderer() md = mistune.Markdown(renderer=renderer) html_content = md.render(md_content) return render_template( 'post.html', content=html_content, )
def test_embed_images_url(self): class TestImageEmbedder(mei.embed_images._ImageEmbedder): def _get_base64_with_image_url(self, url): return 'png_data', 'image/png' tie = TestImageEmbedder() renderer = mei.embed_images.EmbedImagesRenderer(ie=tie) m = mistune.Markdown(renderer=renderer) markdown = """\ ![alt text](https://some.domain/some_image.png)""" actual = m.render(markdown) expected = '<p><img src="data:image/png;base64,png_data" alt="alt text"></p>\n' self.assertEquals(actual, expected)
def execute(self): self.output('Render %s' % self.__target) renderer = HeaderIdsRenderer() markdown = mistune.Markdown( renderer=renderer, parse_inline_html=True, ) with open(str(self.__source.path()), encoding='utf-8') as input: with open(str(self.__target.path()), 'w', encoding='utf-8') as output: text = input.read() for k, v in self.__replace_before.items(): text = text.replace(k, v) html = markdown(text) for k, v in self.__replace_after.items(): html = html.replace(k, v) output.write(html) return True
def render_markdown_text(text_to_render, **kwargs): """Convert text_to_render to html Renders the markdown as a template first so that any included Jinja template values will be rendered to text before markdown conversion to html. __kwargs__ can contain anything that you want to use in the template context. The following kwargs may also be present: escape: Default to True. If false any included html in the text will be left as-is and not escaped for display """ text_to_render = render_template_string(text_to_render, **kwargs) escape = kwargs.get('escape', True) #Set to False to preserve included html markdown = mistune.Markdown(renderer=mistune.Renderer(escape=escape)) return markdown(text_to_render)
def __init__(self, dialog, username, password, settings): self._debug = any("debug" in arg.lower() for arg in sys.argv) self.username = username self.password = password self.mute = False self.user_list = [] self.channel_list = {} self.active_channel = "" self.default_channel = "" self.last_dm = "" self.loop = asyncio.get_event_loop() self.user_colors = config.Config("qt_user_colors.json", ) self.settings = settings self.ip = self.settings.get("server_ip") self.port = self.settings.get("server_port") renderer = Renderer() lexer = InlineLexer(renderer) lexer.enable_underscore() self.markdown = mistune.Markdown(renderer, inline=lexer) self.history_handler = TextHistoryHandler(self) Ui_MainWindow.__init__(self) self.setupUi(dialog) dialog.setWindowIcon(QtGui.QIcon("./utils/ui/files/icon.png")) self.MainWindow = dialog self.MessageField.installEventFilter(self.history_handler) self.MessageField.setFocus() self.MessageField.returnPressed.connect(self.read_input) self.MessageView.verticalScrollBar().setStyleSheet(SCROLLBAR_STYLE) self.ChannelView.verticalScrollBar().setStyleSheet(SCROLLBAR_STYLE) self.OnlineUsersView.verticalScrollBar().setStyleSheet(SCROLLBAR_STYLE) self.connect_task = self.loop.create_task(self.connect())
def process_markdown(data): """Pre-render GitHub-flavoured Markdown, and syntax-highlight code""" import mistune from pygments import highlight from pygments.lexers import get_lexer_by_name from pygments.formatters import html class HighlightRenderer(mistune.HTMLRenderer): def block_code(self, code, lang=None): if not lang: return '\n<pre><code>%s</code></pre>\n' % \ mistune.escape(code) #print("BLOCK_CODE:\nCODE:%s\nLANG: %s\n======" % (code, lang)) lexer = get_lexer_by_name(lang, stripall=True) formatter = html.HtmlFormatter() return highlight(code, lexer, formatter) md = mistune.Markdown(renderer=HighlightRenderer()) for i in range(0, len(data)): module = data[i] module["desc_gfm"] = md(module["desc"]) module["doc_gfm"] = md(module["doc"]) for item_type in TYPE_NAMES: items = module[item_type] for j in range(0, len(items)): item = items[j] dbg("Preparing template data for: %s" % item["def"]) item["def_gfm"] = strip_paragraph(md(item["def"])) item["doc_gfm"] = md(item["doc"]) if "notes" in item: item["notes_gfm"] = md('\n'.join(item["notes"])) if item_type in ["Function", "Constructor", "Method"]: item["parameters_gfm"] = md('\n'.join(item["parameters"])) item["returns_gfm"] = md('\n'.join(item["returns"])) items[j] = item # Now do the same for the deprecated 'items' list for j in range(0, len(module["items"])): item = module["items"][j] item["def_gfm"] = strip_paragraph(md(item["def"])) item["doc_gfm"] = md(item["doc"]) module["items"][j] = item data[i] = module return data
def ToHTML(file, OutputName, WrongWordList): markdown = mistune.Markdown() htmlpage = markdown(file) htmlpage = AddWrongWord(htmlpage, WrongWordList) try: OutputFile = open(OutputName, 'r+') except FileNotFoundError: OutputFile = open(OutputName, 'w') OutputFile.write("<head>\n") OutputFile.write( "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>" ) OutputFile.write("</head>\n") OutputFile.write(htmlpage)
def get(self, request, pk): article = Article.objects.get(id=int(pk)) article.viewed() mk = mistune.Markdown() output = mk(article.content) #**查找上一篇 previous_article = Article.objects.filter(category=article.category, id__lt=pk).defer('content').order_by('-id')[:1] previous_article = previous_article[0] if len(previous_article) else None #**查找下一篇 next_article = Article.objects.filter(category=article.category, id__gt=pk).defer('content').order_by('id')[:1] next_article = next_article[0] if len(next_article) else None return render(request, 'detail.html', { 'article': article, 'previous_article': previous_article, 'next_article': next_article, 'detail_html': output, })
def __init__(self) -> None: # The whitespace remover doesn't take <pre> into account sanitizer.normalize_overall_whitespace = lambda html, *args, **kw: html sanitizer.normalize_whitespace_in_text_or_tail = \ lambda el, *args, **kw: el # hard_wrap: convert all \n to <br> without required two spaces # escape: escape HTML characters in the input string, e.g. tags self._markdown_to_html = mistune.Markdown( hard_wrap=True, escape=True, inline=MarkdownInlineLexer, renderer=MarkdownRenderer(), ) self._markdown_to_html.block.default_rules = [ rule for rule in self._markdown_to_html.block.default_rules if rule != "block_quote" ]
def generate_react_native_from_markdown(mdfile, images_dir): """Generate react-native code from markdown file.""" rn_renderer = RNRenderer(images_dir=images_dir, warning_prefix='\t\t') # Use log=True to print the actual renderer calls from mistune engine wrapper = RendererWrapper(rn_renderer, log=False) renderer = mistune.Markdown(renderer=wrapper) # Produce react-native code react_native_code = renderer(open(mdfile, 'r').read()) # The following line ensures that all react native code related to images # is flushed from the renderer wrapper (e.g. when a markdown document # terminates with an image stripe with no following text) react_native_code += wrapper.flush_images() # Wrap react-native code inside a container view return ('<View style={{markdown.container}}>\n{}\n</View>' ).format(react_native_code)
def about(): """One-page introduction to Secure Scaffold. This renders Markdown to HTML on-the-fly, trusting the Markdown content can be used to generate <a> tags. Do not do this on production sites! """ # The Anchors renderer trusts the headers in the Markdown file. with open("README-secure-scaffold.md") as fh: m = mistune.Markdown(renderer=Anchors()) readme = m.render(fh.read()) readme = markupsafe.Markup(readme) context = { "page_title": "Secure Scaffold", "readme": readme, } return flask.render_template("about.html", **context)
def markdown(text, escape=True, embed_local_images=False, renderer_options={}, **kwargs): """Render markdown formatted text to html. :param text: markdown formatted text content. :param escape: if set to False, all html tags will not be escaped. :param use_xhtml: output with xhtml tags. :param hard_wrap: if set to True, it will use the GFM line breaks feature. :param parse_block_html: parse text only in block level html. :param parse_inline_html: parse text only in inline level html. """ renderer_options['embed_local_images'] = embed_local_images renderer = EmbedImagesRenderer(**renderer_options) return mistune.Markdown(escape=escape, renderer=renderer, **kwargs)(text)
def on_changed_article_body(target, value, oldvalue, initiator): class Renderer(mistune.Renderer): def __init__(self): super().__init__() self.toc_count = 0 def header(self, text, level, raw=None): rv = '<h%d id="toc-%d">%s</h%d>\n' % ( level, self.toc_count, text, level ) self.toc_count += 1 return rv renderer = Renderer() markdown = mistune.Markdown(renderer=renderer) if Signal.send('should_compile_markdown_when_body_change', page=target): target.body_html = markdown(value) target.body_abstract = RE_HTML_TAGS.sub('', target.body_html)[:200] + '...'
def markdown(value, style, math_engine=None, lazy_load=False): styles = settings.MARKDOWN_STYLES.get(style, settings.MARKDOWN_DEFAULT_STYLE) escape = styles.get('safe_mode', True) nofollow = styles.get('nofollow', True) texoid = TEXOID_ENABLED and styles.get('texoid', False) math = getattr(settings, 'MATHOID_URL') and styles.get('math', False) bleach_params = styles.get('bleach', {}) post_processors = [] if styles.get('use_camo', False) and camo_client is not None: post_processors.append(camo_client.update_tree) if lazy_load: post_processors.append(lazy_load_processor) renderer = AwesomeRenderer(escape=escape, nofollow=nofollow, texoid=texoid, math=math and math_engine is not None, math_engine=math_engine) markdown = mistune.Markdown(renderer=renderer, inline=AwesomeInlineLexer, parse_block_html=1, parse_inline_html=1) result = markdown(value) if post_processors: tree = html.Element('div') try: tree.extend( html.fragments_fromstring( result, parser=html.HTMLParser(recover=True))) except (XMLSyntaxError, ParserError) as e: if result and (not isinstance(e, ParserError) or e.args[0] != 'Document is empty'): logger.exception('Failed to parse HTML string') for processor in post_processors: processor(tree) result = html.tostring(tree, encoding='unicode')[len('<div>'):-len('</div>')] if bleach_params: result = get_cleaner(style, bleach_params).clean(result) return Markup(result)
def markdown_convert(markdown_string) -> str: def _get_contents(text): try: contents = json.loads(text).get('message', '') except json.decoder.JSONDecodeError: contents = text except AttributeError: contents = text return contents class ButtonRenderer(mistune.Renderer): ''' Syntax for MD buttons %%%{JSON.message}%%% For example: %%%%{"message": "Something here"}%%%% Output: Something here ''' def paragraph(self, text): text = _get_contents(text) return f'<p>{text}</p>' class ButtonInlineLexer(mistune.InlineLexer): def enable_md_button(self): self.rules.md_button = re.compile(r'%%%(.*?)%%%') self.default_rules.insert(3, 'md_button') def placeholder(self): pass def output_md_button(self, m): text = m.group(1) return self.renderer.paragraph(text) renderer = ButtonRenderer() inline_lexer = ButtonInlineLexer(renderer) inline_lexer.enable_md_button() md = mistune.Markdown(renderer, inline=inline_lexer) return md(markdown_string).strip()
def parse(text, post=None, clean=True, escape=True, allow_rewrite=False): """ Parses markdown into html. Expands certain patterns into HTML. clean : Applies bleach clean BEFORE mistune escapes unsafe characters. Also removes unbalanced tags at this stage. escape : Escape html originally found in the markdown text. allow_rewrite : Serve images with relative url paths from the static directory. eg. images/foo.png -> /static/images/foo.png """ # Resolve the root if exists. root = post.parent.root if (post and post.parent) else None # Bleach clean the text before handing it over to mistune. # Only clean for non moderators. non_mod = not post.lastedit_user.profile.is_moderator if post else True if clean and non_mod: # strip=True strips all disallowed elements text = bleach.clean(text, tags=ALLOWED_TAGS, styles=ALLOWED_STYLES, attributes=ALLOWED_ATTRIBUTES) # Initialize the renderer # parse_block_html=True ensures '>','<', etc are dealt with without being escaped. renderer = BiostarRenderer(escape=escape, parse_block_html=True) # Initialize the lexer inline = BiostarInlineLexer(renderer=renderer, root=root, allow_rewrite=allow_rewrite) markdown = mistune.Markdown(hard_wrap=True, renderer=renderer, inline=inline) # Create final html. html = markdown(text) return html
def test_files(): bad_files = 0 total_files = 0 bad_links = 0 total_links = 0 for fname in sys.argv[1:]: print("**** Checking:", COLOR_SEQ % WHITE, fname, RESET_SEQ, "****") with open(fname, "r", encoding="utf-8") as f: base_dir = os.path.dirname(fname) data = f.read() renderer = FakeRenderer() parser = mistune.Markdown(renderer) parser(data) file_bad_links = 0 for i in renderer.files_to_check: total_links += 1 result = check_file(base_dir, i) if result == Result.Failed: file_bad_links += 1 print(COLOR_SEQ % RED, " Failed:", RESET_SEQ, i) elif result == Result.Success: print(COLOR_SEQ % GREEN, " Success:", RESET_SEQ, i) elif result == Result.Skipped: print(COLOR_SEQ % YELLOW, " Skipped:", RESET_SEQ, i) total_files += 1 if file_bad_links: bad_files += 1 bad_links += file_bad_links print() print("Check finished, checked %s files with %s links" % (total_files, total_links), sep="") if bad_files: print(COLOR_SEQ % RED, "Errors detected: %s bad files with %s bad links" % (bad_files, bad_links), RESET_SEQ, sep="") return errno.ENOENT else: print(COLOR_SEQ % WHITE, "No errors detected", RESET_SEQ, sep="") return 0
def parse_markdown(md_file): """ Create pretty html from a markdown file. """ f = open(md_file) md = f.read() f.close() md_parser = mistune.Markdown() html = md_parser(md) img_pattern = re.compile("<img ") html = img_pattern.sub("<img class=\"img-fluid\"", html) html = re.sub("<ul>", "<ul class=\"list-group\">", html) html = re.sub("<li>", "<li class=\"list-group-item\">", html) h1_pattern = re.compile("<h1>.*?</h1>") h1_matches = h1_pattern.findall(html) if h1_matches: for m in h1_matches: inner = m.split(">")[1].split("<")[0] inner = inner.lower() inner = re.sub(" ", "-", inner) new_header = f"<h1 id=\"{inner}\">{m[4:]}" html = re.sub(m, new_header, html) hr_pattern = re.compile("\<hr\>") out = [] out.append("<br/><br/>") for section in hr_pattern.split(html): out.append("<div class=\"container bg-light rounded mx-auto\">") out.append("<div class=\"m-3 p-3\">") out.append(section) out.append("</div></div><br/>") return "".join(out)
def get_text_from_wikipedia(link): markdown = mistune.Markdown() response = requests.get(link) unaccented_string = unidecode(str(response.content)).replace("\\n", " ") html = unaccented_string html = markdown(html) soup = BeautifulSoup(html, 'lxml') title = soup.find(id="firstHeading") content = soup.find("div", class_="mw-parser-output") to_remove = content.find(id="External_links") to_remove = content.find(id="Notes") if content.find( id="Notes") is not None else to_remove to_remove = content.find(id="See_also") if content.find( id="See_also") is not None else to_remove to_remove = content.find(id="Gallery") if content.find( id="Gallery") is not None else to_remove to_remove = content.find(id="Selected_bibliography") if content.find( id="Selected_bibliography") is not None else to_remove if to_remove is not None: parent = list(to_remove.parents)[0] for tag in parent.find_next_siblings(): tag.decompose() for tag in content.find_all(["small", "math", "table", "h2", "sup"]): tag.decompose() for tag in content.find_all(True, id=["toc"]): tag.decompose() for tag in content.find_all(True, class_=[ "mw-headline", "IPA", "mw-editsection", "quotebox", "infobox", "vertical-navbox", "navbox", "reference", "reflist", "thumb" ]): tag.decompose() for tag in content.find_all(True, role="note"): tag.decompose() # paren_reg = re.compile("/\(([^()]+)\)/g") # out = paren_reg.sub('', content.get_text()) out = content.get_text().replace("\\", "") out = out.replace("'", "") out = out.replace(";", "") return "{}\n\n{}".format(title.get_text(), out)
def __init__(self, project_name): loader = FileSystemLoader(os.getcwd()) self.project_name = project_name self.jinja = Environment(loader=loader) self.markdown = mistune.Markdown() try: config_dict = {} with open("config.txt", 'r') as config: config = config.readlines() config_dict['root_dir'] = config[2].replace( " ", "").split(":")[1].strip("\n") config_dict['pages_dir'] = config[3].replace( " ", "").split(":")[1].strip("\n") config_dict['headerfile'] = config[4].replace( " ", "").split(":")[1].strip("\n") config_dict['blogsdir'] = config[5].replace( " ", "").split(":")[1].strip("\n") self.config_dict = config_dict except FileNotFoundError: self.config_dict = {}
def html_to_urwid_text_markup(html, excludes=[]): md2urwid = mistune.Markdown(renderer=UrwidMarkdownRenderer()) markdown = html_to_text.handle(html) markup = md2urwid(strip_emoji(markdown)) if excludes: markup = [ item for item in markup for exclude in excludes if not exclude(item) ] # filter out any duplicate line breaks return [markup[0]] + [ b for a, b in pairwise(markup) if a != "\n\n" or a != b ]
def get_content(source_dir, fil=FileFilter('_', ['.html', '.md']), markdown=mistune.Markdown()): logger.debug('Get content from %s', source_dir) source_content = {} for path in source_dir.visit(fil=fil): part_name = path.purebasename.lstrip('_') text = path.read_text(encoding='utf-8') if path.ext == '.html': html = text elif path.ext == '.md': html = markdown(text) else: raise ContentFileError('Invalid file type: {}'.format(path), 2) logger.info("Get content from file: %s", path) source_content[part_name] = html # Make sure there is at lease one file in the content source directory # because git will not track empty directories source_dir.join(KEEP_FILE).ensure(file=1) return source_content
def html_clean(htmlstr): markdown = mistune.Markdown() # 采用bleach来清除不必要的标签,并linkify text tags = [ 'a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'strong', 'ul', 'img', 'table' ] tags.extend([ 'p', 'hr', 'br', 'pre', 'code', 'span', 'h1', 'h2', 'h3', 'h4', 'h5', 'del', 'dl', 'img', 'sub', 'sup', 'u' 'table', 'thead', 'tr', 'th', 'td', 'tbody', 'dd', 'caption', 'blockquote', 'section' ]) attributes = { 'a': ['href', 'title', 'target'], 'img': ['src', 'width', 'height'] } return bleach.linkify( bleach.clean(markdown(htmlstr), tags=tags, attributes=attributes))
def show_page(page_path): """Render custom pages like stats""" renderer = mistune.Renderer(escape=False) markdown = mistune.Markdown(renderer=renderer) pages_folder = os.environ.get('PAGES_PATH', 'src/pages') page_path = os.path.join(pages_folder, f"{page_path}.md") if os.path.exists(page_path): with open(page_path) as page: content = page.read() rendered_content = markdown(content) return render_template('page.html', content=rendered_content) else: return render_template( 'error.html', message='Page not found', details='Did you try to check other pages?'), 404
def includeme(config): """ configures the rendering engines and attaches them to the request Activate this setup using ``config.include('pyragit.markdown')``. """ md_renderer = HighlightRenderer(inlinestyles=False, linenos=False) render_markdown = mistune.Markdown(renderer=md_renderer) renderer_dict = {".md": render_markdown, ".txt": render_text} def get_markup_renderer(filename): name, dot, ext = filename.rpartition(".") complete_extension = dot + ext return renderer_dict.get(complete_extension, None) config.add_request_method( lambda request, filename: get_markup_renderer(filename), "get_markup_renderer", )
def main(): text = sys.stdin.read() front_matter, _, text = text.partition('\n---\n') args = {} for l in front_matter.split('\n'): if ':' in l: key, _, value = l.partition(': ') args[key] = value title = args['title'] author = args['author'] renderer = LaTeXRenderer() parser = mistune.Markdown(renderer=renderer) print(r'''\documentclass[12pt,a4paper]{article} \usepackage{ctex} \usepackage[paper=a4paper,includefoot,margin=54pt]{geometry} \usepackage[colorlinks,linkcolor=black,anchorcolor=black,citecolor=black,unicode]{hyperref} \usepackage{float} \usepackage{listings} \lstset{frame=single,breaklines=true,postbreak=\raisebox{0ex}[0ex][0ex]{\ensuremath{\hookrightarrow\space}}} \renewcommand{\lstlistingname}{程序} \renewcommand{\contentsname}{目录} \renewcommand{\abstractname}{摘要} \renewcommand{\refname}{参考文献} \renewcommand{\indexname}{索引} \renewcommand{\figurename}{图} \renewcommand{\tablename}{表} \renewcommand{\appendixname}{附录} \begin{document} \title{%s} \author{%s} \maketitle \tableofcontents \newpage ''' % (title, author)) print(parser(text)) print('\\end{document}')
def parse(text, post=None, clean=True, escape=True, allow_rewrite=False): """ Parses markdown into html. Expands certain patterns into HTML. clean : Applies bleach clean BEFORE mistune escapes unsafe characters. Also removes unbalanced tags at this stage. escape : Escape html originally found in the markdown text. allow_rewrite : Serve images with relative url paths from the static directory. eg. images/foo.png -> /static/images/foo.png """ # Resolve the root if exists. root = post.parent.root if (post and post.parent) else None # Initialize the renderer renderer = BiostarRenderer(escape=escape) # Initialize the lexer inline = BiostarInlineLexer(renderer=renderer, root=root, allow_rewrite=allow_rewrite) markdown = mistune.Markdown(hard_wrap=True, renderer=renderer, inline=inline) html = safe(markdown, text=text) # Bleach clean the html. if clean: html = safe(bleach.clean, text=html, tags=ALLOWED_TAGS, styles=ALLOWED_STYLES, attributes=ALLOWED_ATTRIBUTES) # Embed sensitive links into html html = safe(linkify, text=html) return html
def api_get_blog(*, id): blog = yield from Blog.get(id=id) if blog: blog.read_total += 1 yield from blog.update() # comments = get_comments(id) comments = yield from Comment.filter(blog_id=id) if comments is None: raise APIResourceNotFoundError('Comment') parent_comments = list() child_comments = list() for item in comments: for c in comments: c['html_content'] = text2html(c['content']) if item['parent_id'] == '': parent_comments.append(item) else: child_comments.append(item) for x in parent_comments: child_comments_list = list() for y in child_comments: if x.get('id') == y.get('parent_id'): child_comments_list.append(y) x.setdefault('child_comments', child_comments_list) #input_file = codecs.open('test.md', mode="r", encoding="utf-8") #text = input_file.read() #html = markdown.markdown(text) #blog.html_content = markdown.markdown( blog.content ) #blog.html_content = markdown.markdown(text, safe_mode="escape") markdown = mistune.Markdown() blog.html_content = markdown(blog.content) return { '__template__': 'blogs.html', 'blog': blog, 'comments': parent_comments }