Beispiel #1
0
def render_html(changelog_txt, version, logging):
    """
    Format an html rendered document from the render_markdown method above.
    """

    rendered = mistletoe.markdown(render_markdown(changelog_txt, version, logging))
    return rendered
Beispiel #2
0
def build_reference():
    doctree = gather_docstrings([
        ROOT_DIR + 'web/filament-js/jsbindings.cpp',
        ROOT_DIR + 'web/filament-js/jsenums.cpp',
        ROOT_DIR + 'web/filament-js/utilities.js',
        ROOT_DIR + 'web/filament-js/wasmloader.js',
        ROOT_DIR + 'web/filament-js/extensions.js',
    ])
    markdown = build_reference_markdown(doctree)
    rendered = mistletoe.markdown(markdown, PygmentsRenderer)
    template = open(SCRIPT_DIR + 'ref_template.html').read()
    rendered = template.replace('$BODY', rendered)
    outfile = os.path.join(OUTPUT_DIR, f'reference.html')
    with open(outfile, 'w') as fout:
        fout.write(rendered)
Beispiel #3
0
def test_imageInLink(capsys):
    '''it should succeed but print error when encountering image in link'''
    #arrange/act
    output = mistletoe.markdown(
        "[![](https://via.placeholder.com/500)](https://cobertos.com)",
        NotionPyRenderer)
    output = output[0]
    captured = capsys.readouterr()

    #assert
    assert isinstance(output,
                      dict)  #Should be a TextBlock, but the image will fail
    assert re.search(r"not support", captured.out,
                     re.I)  #Should print out warning
    assert output['type'] == notion.block.TextBlock
Beispiel #4
0
def test_latex_block():
    output = mistletoe.markdown(r"""
# Test for latex blocks
Text before

$$
f(x) = \sigma(w \cdot x + b)
$$

Text after
""", addLatexExtension(NotionPyRenderer))

    assert output[2] is not None
    assert output[2]['type'] == notion.block.EquationBlock
    assert output[2]['title_plaintext'] == 'f(x) = \\\\sigma(w \\\\cdot x + b)\n'
def pynbParser(indir, outfile, indexWords):
    global OPERATING_PATH
    global CHAPTER_TOP
    if not os.path.exists('./LatexBook/image'):
        os.makedirs('./LatexBook/image')

    OPERATING_PATH = indir + "/"
    fileInDir = [
        indir + "/" + x for x in os.listdir(indir)
        if re.match(r"[\s\S]*.ipynb$", x) and x != "pynbParser.ipynb"
    ]
    fileInDir.sort()

    parsedCells = []
    for infile in fileInDir:
        print(infile)
        pynbRaw = openPynb(infile)

        for cell in pynbRaw:
            if cell["cell_type"] == "markdown":
                if cell["source"][0][:8] != "<a href=":
                    rendered = mistletoe.markdown("".join(cell["source"]),
                                                  LaTeXRenderer)
                    latexRE = r'[\s\S]*\\begin{document}(?P<body>[\s\S]*)\\end{document}'
                    s = re.match(latexRE, rendered).groupdict()
                    s = postParser(s["body"])
                    for word in indexWords:
                        s = s.replace(word, "\\index{" + word + "}")
                    parsedCells.append(s)
            elif cell["cell_type"] == "code":
                res = parseCode("".join(cell["source"]))
                parsedCells.append(res)
                res = parseOutput(cell["outputs"])
                parsedCells.append(res)
            else:
                print("Error: Encountered unknown cell type")
                exit(1)

        parsedCells.append("\n\n")

    if not os.path.exists("./LatexBook/" + indir):
        os.makedirs('./LatexBook/' + indir)

    with open("./LatexBook/" + indir + "/" + outfile, "w") as f:
        f.write("\n".join(parsedCells))

    OPERATING_PATH = ""
    return "\n".join(parsedCells)
Beispiel #6
0
def weave(name):
    with open(SCRIPT_DIR + f'tutorial_{name}.md', 'r') as fin:
        markdown = fin.read()
        if ENABLE_EMBEDDED_DEMO:
            if name == 'triangle':
                markdown = TUTORIAL_PREAMBLE + markdown
            markdown = '<div class="demo_frame">' + \
                f'<iframe src="demo_{name}.html"></iframe>' + \
                f'<a href="demo_{name}.html">&#x1F517;</a>' + \
                '</div>\n' + markdown
        rendered = mistletoe.markdown(markdown, PygmentsRenderer)
    template = open(SCRIPT_DIR + 'tutorial_template.html').read()
    rendered = template.replace('$BODY', rendered)
    outfile = os.path.join(OUTPUT_DIR, f'tutorial_{name}.html')
    with open(outfile, 'w') as fout:
        fout.write(rendered)
def test_imageBlockText():
    '''it should render an image in bold text'''
    #arrange/act
    output = mistletoe.markdown(
        "**texttext![](https://via.placeholder.com/500)texttext**",
        NotionPyRenderer)

    #assert
    assert len(output) == 2
    assert isinstance(output[0], dict)
    assert output[0]['type'] == notion.block.TextBlock
    assert output[0][
        'title'] == "**texttexttexttext**"  #Should extract the image
    assert isinstance(
        output[1], dict
    )  #The ImageBlock can't be inline with anything else so it comes out
    assert output[1]['type'] == notion.block.ImageBlock
def test_imageInLink():
    '''it should render an image in a link, but separately because notion doesnt support that'''
    #arrange/act
    output = mistletoe.markdown(
        "[![](https://via.placeholder.com/500)](https://cobertos.com)",
        NotionPyRenderer)

    #assert
    assert len(output) == 2
    assert isinstance(output[0], dict)
    assert output[0]['type'] == notion.block.TextBlock
    assert output[0][
        'title'] == "[](https://cobertos.com)"  #Should extract the image
    assert isinstance(
        output[1], dict
    )  #The ImageBlock can't be in a link in Notion, so we get it outside
    assert output[1]['type'] == notion.block.ImageBlock
Beispiel #9
0
 def save(self, *args, **kwargs):
     self.content_html = mistletoe.markdown(self.content)
     self.slug = slugify(self.title)[:50]
     super().save(*args, **kwargs)
     # Also post it to Elasticsearch
     if not self.isdeleted and self.id:
         payload = {
                 'title': self.title,
                 'summary': self.summary,
                 'content': self.content,
                 'tags': [tag.elasticname for tag in self.tags.all()],
                 'timeposted': self.timeposted.timestamp(),
                 'author': self.author.id
                 }
         r = requests.put(f'http://localhost:9200/snips/doc/{self.id}', json=payload)
         if r.status_code != 200:
             print(r.json())
Beispiel #10
0
def test_imageInHtml():
    '''it should render an image that is mentioned in the html <img> tag'''
    #arrange/act
    output = mistletoe.markdown(
        "head<img src=\"https://via.placeholder.com/500\" />tail",
        addHtmlImgTagExtension(NotionPyRenderer))

    #assert
    assert len(output) == 2
    assert isinstance(output[0], dict)
    assert output[0]['type'] == notion.block.TextBlock
    assert output[0]['title'] == "headtail"  #Should extract the image
    assert isinstance(
        output[1], dict
    )  #The ImageBlock can't be inline with anything else so it comes out
    assert output[1]['type'] == notion.block.ImageBlock
    assert output[1]['caption'] is None
Beispiel #11
0
def api_comment(v):

    parent_submission = base36decode(request.form.get("submission"))
    parent_fullname = request.form.get("parent_fullname")

    #sanitize
    body = request.form.get("body", "")
    body_md = mistletoe.markdown(body)
    body_html = sanitize(body_md, linkgen=True)

    #check existing
    existing = db.query(Comment).filter_by(
        author_id=v.id,
        body=body,
        parent_fullname=parent_fullname,
        parent_submission=parent_submission).first()
    if existing:
        return redirect(existing.permalink)

    parent_id = int(parent_fullname.split("_")[1], 36)
    if parent_fullname.startswith("t2"):
        parent = db.query(Submission).filter_by(id=parent_id).first()
    elif parent_fullname.startswith("t3"):
        parent = db.query(Comment).filter_by(id=parent_id).first()

    if parent.is_banned:
        abort(403)

    c = Comment(author_id=v.id,
                body=body,
                body_html=body_html,
                parent_submission=parent_submission,
                parent_fullname=parent_fullname,
                parent_author_id=parent.author.id
                if parent.author.id != v.id else None)

    db.add(c)
    db.commit()

    vote = CommentVote(user_id=v.id, comment_id=c.id, vote_type=1)

    db.add(vote)
    db.commit()

    return redirect(f"{c.post.permalink}#comment-{c.base36id}")
def test_code_block_without_language():
    '''it should render a fenced code block with no language specified'''
    #arrange/act
    raw =\
"""\
```
(f_ my_made_up_language a b)!
```"""
    expected = "(f_ my_made_up_language a b)!\n"
    output = mistletoe.markdown(raw, NotionPyRenderer)

    #assert
    assert len(output) == 1
    output = output[0]
    assert isinstance(output, dict)
    assert output['type'] == notion.block.CodeBlock
    assert output['title_plaintext'] == expected
    assert output['language'] == "Plain Text"
Beispiel #13
0
def build_page(name):
    if not os.path.isdir(name):
        raise Exception(f"No page named '{name}'")
    else:
        os.chdir(name)
        page_data = toml.load("page.cfg")

        if not os.path.isfile("page.md"):
            raise FileNotFoundError("No page markdown file found!")

        with open("page.md") as f:
            md = f.read()
            f.close()

        with open("page.html", "w+") as f:
            html = mistletoe.markdown(md)
            f.write(html)
            f.close()
Beispiel #14
0
 def testplan_table(self, fmt="pipe"):
     '''Generate testplan table from hjson entries in the format specified
     by the 'fmt' arg.
     '''
     table = [["Milestone", "Name", "Description", "Tests"]]
     colalign = ("center", "center", "left", "left")
     for entry in self.entries:
         tests = ""
         for test in entry.tests:
             tests += test + "<br>\n"
         desc = entry.desc.strip()
         if fmt == "html":
             desc = mistletoe.markdown(desc)
         table.append([entry.milestone, entry.name, desc, tests])
     return tabulate(table,
                     headers="firstrow",
                     tablefmt=fmt,
                     colalign=colalign)
Beispiel #15
0
def edit_comment(v):

    comment_id = request.form.get("id")
    body = request.form.get("comment", "")
    body_md = mistletoe.markdown(body)
    body_html = sanitize(body_md, linkgen=True)

    c = db.query(Comment).filter_by(id=comment_id, author_id=v.id).first()

    if not c:
        abort(404)

    c.body = body
    c.body_html = body_html
    c.edited_timestamp = time.time()

    db.add(c)
    db.commit()
def test_header(capsys, headerLevel):
    '''it renders a range of headers, warns if it cant render properly'''
    #arrange/act
    output = mistletoe.markdown(f"{'#'*headerLevel} Owo what's this?", NotionPyRenderer)
    captured = capsys.readouterr()

    #assert
    assert len(output) == 1
    output = output[0]
    assert isinstance(output, dict)
    if headerLevel > 3: #Should print error
        assert re.search(r"not support", captured.out, re.I) #Should print out warning
    if headerLevel == 1:
        assert output['type'] == notion.block.HeaderBlock
    elif headerLevel == 2:
        assert output['type'] == notion.block.SubheaderBlock
    else:
        assert output['type'] == notion.block.SubsubheaderBlock
    assert output['title'] == "Owo what's this?"
def test_code_block_with_language():
    '''it should render a fenced code block with explicit language'''
    #arrange/act
    raw =\
"""\
```python
def get_favorite_fruit():
    return Watermelon
```"""
    expected = "def get_favorite_fruit():\n    return Watermelon\n"
    output = mistletoe.markdown(raw, NotionPyRenderer)

    #assert
    assert len(output) == 1
    output = output[0]
    assert isinstance(output, dict)
    assert output['type'] == notion.block.CodeBlock
    assert output['title_plaintext'] == expected
    assert output['language'] == 'Python'
def md(target: Path, parent: Resource) -> Optional[Document]:
    """ Read the frontmatter and markdown """

    file_content = target.open().read()
    if '---\n' in file_content:
        yaml_string, markdown_string = file_content.split('---\n')

        # Handle the frontmatter
        frontmatter = (load(yaml_string, Loader=Loader) or {})

        # Parse the Markdown into HTML
        body = markdown(markdown_string)

        # Make and return a resource. If the name is 'index', make a
        # Folder.
        name = str(target.stem)
        resource = Document(name=name, parent=parent, body=body, **frontmatter)

        return resource
Beispiel #19
0
def print_multiversion_format(obj, outfile):
    # Sort the revision list based on the version field.
    # TODO: If minor version goes up gte than 10?
    revisions = sorted(obj["revisions"], key=lambda x: x["version"])
    latest_rev = len(revisions) - 1
    outstr = ""
    for i, rev in enumerate(revisions):
        outstr += "      <tr>\n"

        # If only one entry in `revisions`, no need of `rowspan`.
        if len(revisions) == 1:
            outstr += "        <td class='fixleft'>"
            outstr += get_linked_design_spec(obj) + "</td>\n"
            outstr += "        <td class='dv-doc'>"
            outstr += get_linked_dv_doc(obj) + "</td>\n"
        # Print out the module name in the first entry only
        elif i == 0:
            outstr += "        <td class='fixleft' rowspan='{}'>".format(
                len(revisions))
            outstr += get_linked_design_spec(obj) + "</td>\n"
            outstr += "        <td class='hw-stage' rowspan='{}'>".format(
                len(revisions))
            outstr += get_linked_dv_doc(obj) + "</td>\n"

        # Version
        outstr += "        <td class=\"version\">"
        outstr += get_linked_version(rev) + "</td>\n"

        # Development Stage
        for stage_html in get_development_stage(obj, rev, (i == latest_rev)):
            outstr += "        <td class=\"hw-stage\"><span class='hw-stage'>"
            outstr += stage_html
            outstr += "</span></td>\n"

        # Notes
        if 'notes' in rev and rev['notes'] != '':
            outstr += "        <td>" + mk.markdown(
                rev['notes']).rstrip() + "</td>\n"
        else:
            outstr += "        <td><p>&nbsp;</p></td>\n"
        outstr += "      </tr>\n"

    genout(outfile, outstr)
Beispiel #20
0
def md_results_to_html(title, css_path, md_text):
    '''Convert results in md format to html. Add a little bit of styling.
    '''
    html_text = "<!DOCTYPE html>\n"
    html_text += "<html lang=\"en\">\n"
    html_text += "<head>\n"
    if title != "":
        html_text += "  <title>{}</title>\n".format(title)
    if css_path != "":
        html_text += "  <link rel=\"stylesheet\" type=\"text/css\""
        html_text += " href=\"{}\"/>\n".format(css_path)
    html_text += "</head>\n"
    html_text += "<body>\n"
    html_text += "<div class=\"results\">\n"
    html_text += mistletoe.markdown(md_text)
    html_text += "</div>\n"
    html_text += "</body>\n"
    html_text += "</html>\n"
    return html_text
Beispiel #21
0
def print_version1_format(obj, outfile):
    life_stage = obj['life_stage']
    life_stage_mapping = convert_stage(obj['life_stage'])

    # yapf: disable
    genout(outfile, "      <tr>\n")
    genout(outfile, "        <td class=\"fixleft\">" +
                    html.escape(obj['name']) + "</td>\n")
    genout(outfile, "        <td class=\"hw-stage\">" +
                    html.escape(obj['version']) + "</td>\n")
    genout(outfile, "        <td class=\"hw-stage\"><span class='hw-stage' title='" +
                    html.escape(life_stage_mapping) + "'>" +
                    html.escape(life_stage) + "</span></td>\n")
    if life_stage != 'L0' and 'design_stage' in obj:
        design_stage_mapping = convert_stage(obj['design_stage'])
        genout(outfile,
                    "        <td class=\"hw-stage\"><span class='hw-stage' title='" +
                    html.escape(design_stage_mapping) + "'>" +
                    html.escape(obj['design_stage']) + "</span></td>\n")
    else:
        genout(outfile,
                    "        <td>&nbsp;</td>\n")
    if life_stage != 'L0' and 'verification_stage' in obj:
        verification_stage_mapping = convert_stage(obj['verification_stage'])
        genout(outfile,
                    "        <td class=\"hw-stage\"><span class='hw-stage' title='" +
                    html.escape(verification_stage_mapping) + "'>" +
                    html.escape(obj['verification_stage']) + "</span></td>\n")
    else:
        genout(outfile,
                    "        <td>&nbsp;</td>\n")

    # Empty commit ID
    genout(outfile, "        <td>&nbsp;</td>\n")

    if 'notes' in obj:
        genout(outfile,
                    "        <td>" + mk.markdown(obj['notes']).rstrip() + "</td>\n")
    else:
        genout(outfile,
                    "        <td><p>&nbsp;</p></td>\n")
    genout(outfile, "      </tr>\n")
Beispiel #22
0
def test_nested_list():
    '''it should render nested lists'''
    #arrange/act
    output = mistletoe.markdown(\
"""
* Awoo
    * Hewwo
""", NotionPyRenderer)
    output = output[0]

    #assert
    assert isinstance(output, dict)
    assert output['type'] == notion.block.BulletedListBlock
    assert output['title'] == 'Awoo'

    assert len(output['children']) == 1
    outputChild = output['children'][0]
    assert isinstance(outputChild, dict)
    assert outputChild['type'] == notion.block.BulletedListBlock
    assert outputChild['title'] == 'Hewwo'
Beispiel #23
0
def interactive(renderer):
    """
    Parse user input, dump to stdout, rinse and repeat.
    Python REPL style.
    """
    _import_readline()
    _print_heading(renderer)
    contents = []
    more = False
    while True:
        try:
            prompt, more = ('... ', True) if more else ('>>> ', True)
            contents.append(input(prompt) + '\n')
        except EOFError:
            print('\n' + mistletoe.markdown(contents, renderer), end='')
            more = False
            contents = []
        except KeyboardInterrupt:
            print('\nExiting.')
            break
def test_imageInHtmlBlock():
    '''it should render an image that is mentioned in the html block'''
    output = mistletoe.markdown(\
"""
<div><img src="https://via.placeholder.com/500" alt="ImCaption"/>text in div</div>

tail
""", addHtmlImgTagExtension(NotionPyRenderer))

    #assert
    assert len(output) == 3
    assert isinstance(output[0], dict)
    assert output[0]['type'] == notion.block.TextBlock
    assert output[0]['title'] == "<div>text in div</div>" #Should extract the image
    assert isinstance(output[1], dict)
    assert output[1]['type'] == notion.block.ImageBlock
    assert output[1]['caption'] == "ImCaption"
    assert isinstance(output[2], dict)
    assert output[2]['type'] == notion.block.TextBlock
    assert output[2]['title'] == "tail"
Beispiel #25
0
def api_comment(v):

    body = request.form.get("text")
    parent_fullname = request.form.get("parent_fullname")

    #sanitize
    body = request.form.get("body")
    body_html = mistletoe.markdown(body_md)
    body_html = sanitize(body_html, linkgen=True)

    #check existing
    existing = db.query(Comment).filter_by(
        author_id=v.id, body=body, parent_fullname=parent_fullname).first()
    if existing:
        return redirect(existing.permalink)

    c = Comment(author_id=v.id, body=body, body_html=body_html)

    db.add(c)
    db.commit()
Beispiel #26
0
    def get_testplan_table(self, fmt="md"):
        """Generate testplan table from hjson testplan.

        fmt is either 'md' or 'html'.
        """
        assert fmt in ["md", "html"]

        if self.testpoints:
            text = "### Testpoints\n\n"
            header = ["Milestone", "Name", "Tests", "Description"]
            colalign = ("center", "center", "left", "left")
            table = []
            for tp in self.testpoints:
                desc = tp.desc.strip()
                tests = "\\\n".join(tp.tests)
                table.append([tp.milestone, tp.name, tests, desc])
            text += tabulate(table,
                             headers=header,
                             tablefmt=fmt,
                             colalign=colalign)
            text += "\n"

        if self.covergroups:
            text += "\n### Covergroups\n\n"
            header = ["Name", "Description"]
            colalign = ("center", "left")
            table = []
            for covergroup in self.covergroups:
                desc = covergroup.desc.strip()
                table.append([covergroup.name, desc])
            text += tabulate(table,
                             headers=header,
                             tablefmt="pipe",
                             colalign=colalign)
            text += "\n"

        if fmt == "html":
            text = self.get_dv_style_css() + mistletoe.markdown(text)
            text = text.replace("<table>", "<table class=\"dv\">")

        return text
Beispiel #27
0
def items():
    # 返回插入sql的数据
    for filename in now_archives:
        with open(os.path.join(archive_path, filename), 'r',
                  encoding='utf-8') as f:
            if f.readline().strip() == "---":
                flag = 1
                headlist = []
                while flag:
                    newline = f.readline()
                    if newline.count("-") >= 3:
                        flag = 0
                        break
                    else:
                        if newline.count(":") >= 1:
                            key = newline[:newline.index(':')].strip()
                            value = newline[newline.index(':') + 1:].strip()
                            headlist.append([key, value])
                        elif '-' in newline and newline[0] == '-':
                            headlist[-1][-1] = headlist[-1][-1].strip(
                            ) + ' ' + newline[1:].strip()
                            headlist[-1][-1] = headlist[-1][-1].strip()
                abstract = ''
                l = False
                text = f.read()
                text = text.replace("](/img/archive_img",
                                    '](/static/img/archive_img')
                m = markdown.markdown(text[:200])
                for i in m:
                    if i == "<":
                        l = True
                    elif i == ">":
                        l = False
                        continue
                    if l or i in "\n*`":
                        continue
                    abstract += i
                headlist.append(["text", mistletoe.markdown(text)])
                headlist.append(["abstract", abstract[:80]])
                headlist.append(['filename', InitSqlFilename(filename)])
                yield dict(headlist)
Beispiel #28
0
def print_version1_format(obj, outfile):
    # yapf: disable
    genout(outfile, "      <tr>\n")
    genout(outfile, "        <td class=\"fixleft\">" +
                    get_linked_design_spec(obj) + "</td>\n")
    genout(outfile, "        <td class=\"dv-doc\">" +
                    get_linked_dv_doc(obj) + "</td>\n")
    genout(outfile, "        <td class=\"version\">" +
                    get_linked_version(obj) + "</td>\n")

    for stage_html in get_development_stage(obj, obj):
        genout(outfile,
               "        <td class=\"hw-stage\">" + stage_html + "</td>\n")

    if 'notes' in obj:
        genout(outfile,
               "        <td>" + mk.markdown(obj['notes']).rstrip() + "</td>\n")
    else:
        genout(outfile,
               "        <td><p>&nbsp;</p></td>\n")
    genout(outfile, "      </tr>\n")
Beispiel #29
0
def md_results_to_html(title, css_file, md_text):
    '''Convert results in md format to html. Add a little bit of styling.
    '''
    html_text = "<!DOCTYPE html>\n"
    html_text += "<html lang=\"en\">\n"
    html_text += "<head>\n"
    if title != "":
        html_text += "  <title>{}</title>\n".format(title)
    html_text += "</head>\n"
    html_text += "<body>\n"
    html_text += "<div class=\"results\">\n"
    html_text += mistletoe.markdown(md_text)
    html_text += "</div>\n"
    html_text += "</body>\n"
    html_text += "</html>\n"
    html_text = htmc_color_pc_cells(html_text)
    # this function converts css style to inline html style
    html_text = transform(html_text,
                          external_styles=css_file,
                          cssutils_logging_level=log.ERROR)
    return html_text
def test_list():
    '''it should render a GFM list item'''
    #arrange/act
    output = mistletoe.markdown(\
"""
* [] Really
* [ ] big
* [x] uwu
""", NotionPyRenderer)

    #assert
    assert len(output) == 3
    assert isinstance(output[0], dict)
    assert output[0]['type'] == notion.block.BulletedListBlock
    assert output[0]['title'] == '[] Really'
    assert isinstance(output[1], dict)
    assert output[1]['type'] == notion.block.TodoBlock
    assert output[1]['title'] == 'big'
    assert output[1]['checked'] == False
    assert isinstance(output[2], dict)
    assert output[2]['type'] == notion.block.TodoBlock
    assert output[2]['title'] == 'uwu'
    assert output[2]['checked'] == True
Beispiel #31
0
def print_version1_format(obj, outfile):
    life_stage = obj['life_stage']
    life_stage_mapping = convert_stage(obj['life_stage'])

    # yapf: disable
    genout(outfile, "      <tr>\n")
    name = html.escape(obj['name'])
    genout(outfile, "        <td class=\"fixleft\">" +
                    get_linked_design_spec(obj) + "</td>\n")
    genout(outfile, "        <td class=\"hw-stage\">" +
                    get_linked_dv_plan(obj) + "</td>\n")
    genout(outfile, "        <td class=\"hw-stage\">" +
                    get_linked_version(obj) + "</td>\n")
    genout(outfile, "        <td class=\"hw-stage\"><span class='hw-stage'>" +
                    get_development_stage(obj, obj) + "</span></td>\n")

    if 'notes' in obj:
        genout(outfile,
                    "        <td>" + mk.markdown(obj['notes']).rstrip() + "</td>\n")
    else:
        genout(outfile,
                    "        <td><p>&nbsp;</p></td>\n")
    genout(outfile, "      </tr>\n")