Esempio n. 1
0
def myst_block_plugin(md: MarkdownIt):
    """Parse MyST targets (``(name)=``), blockquotes (``% comment``) and block breaks (``+++``)."""
    md.block.ruler.before(
        "blockquote",
        "myst_line_comment",
        line_comment,
        {
            "alt":
            ["paragraph", "reference", "blockquote", "list", "footnote_def"]
        },
    )
    md.block.ruler.before(
        "hr",
        "myst_block_break",
        block_break,
        {
            "alt":
            ["paragraph", "reference", "blockquote", "list", "footnote_def"]
        },
    )
    md.block.ruler.before(
        "hr",
        "myst_target",
        target,
        {
            "alt":
            ["paragraph", "reference", "blockquote", "list", "footnote_def"]
        },
    )
    md.add_render_rule("myst_target", render_myst_target)
    md.add_render_rule("myst_line_comment", render_myst_line_comment)
Esempio n. 2
0
def myst_block_plugin(md: MarkdownIt):
    md.block.ruler.before(
        "blockquote",
        "myst_line_comment",
        line_comment,
        {
            "alt":
            ["paragraph", "reference", "blockquote", "list", "footnote_def"]
        },
    )
    md.block.ruler.before(
        "hr",
        "myst_block_break",
        block_break,
        {
            "alt":
            ["paragraph", "reference", "blockquote", "list", "footnote_def"]
        },
    )
    md.block.ruler.before(
        "hr",
        "myst_target",
        target,
        {
            "alt":
            ["paragraph", "reference", "blockquote", "list", "footnote_def"]
        },
    )
    md.add_render_rule("myst_target", render_myst_target)
    md.add_render_rule("myst_line_comment", render_myst_line_comment)
Esempio n. 3
0
def texmath_plugin(md: MarkdownIt, **options):
    delimiters = options.get("delimiters", None) or "dollars"
    macros = options.get("macros", {})

    if delimiters in rules:
        for rule_inline in rules[delimiters]["inline"]:
            md.inline.ruler.before("escape", rule_inline["name"],
                                   make_inline_func(rule_inline))

            def render_math_inline(self, tokens, idx, options, env):
                return rule_inline["tmpl"].format(
                    render(tokens[idx].content, False, macros))

            md.add_render_rule(rule_inline["name"], render_math_inline)

        for rule_block in rules[delimiters]["block"]:
            md.block.ruler.before("fence", rule_block["name"],
                                  make_block_func(rule_block))

            def render_math_block(self, tokens, idx, options, env):
                return rule_block["tmpl"].format(
                    render(tokens[idx].content, True, macros),
                    tokens[idx].info)

            md.add_render_rule(rule_block["name"], render_math_block)
Esempio n. 4
0
def amsmath_plugin(md: MarkdownIt):

    md.block.ruler.before(
        "blockquote",
        "amsmath",
        amsmath_block,
        {
            "alt":
            ["paragraph", "reference", "blockquote", "list", "footnote_def"]
        },
    )
    md.add_render_rule("amsmath", render_amsmath_block)
Esempio n. 5
0
def render_html(path):
    """Render html content from markdown file
    """
    try:
        file = open(path, mode='r', encoding='UTF-8')
    except FileNotFoundError:
        return abort(404)

    with file:
        md_content = file.read()

    renderer = MarkdownIt("commonmark", {"highlight": highlighter})
    renderer.add_render_rule("image", render_img)
    return renderer.render(md_content)
Esempio n. 6
0
def amsmath_plugin(md: MarkdownIt):
    """Parses TeX math equations, without any surrounding delimiters,
    only for top-level `amsmath <https://ctan.org/pkg/amsmath>`__ environments:

    .. code-block:: latex

        \\begin{gather*}
        a_1=b_1+c_1\\\\
        a_2=b_2+c_2-d_2+e_2
        \\end{gather*}

    """
    md.block.ruler.before(
        "blockquote",
        "amsmath",
        amsmath_block,
        {"alt": ["paragraph", "reference", "blockquote", "list", "footnote_def"]},
    )
    md.add_render_rule("amsmath", render_amsmath_block)
Esempio n. 7
0
def colon_fence_plugin(md: MarkdownIt):
    """This plugin directly mimics regular fences, but with `:` colons.

    Example::

        :::name
        contained text
        :::

    """

    md.block.ruler.before(
        "fence",
        "colon_fence",
        _rule,
        {
            "alt":
            ["paragraph", "reference", "blockquote", "list", "footnote_def"]
        },
    )
    md.add_render_rule("colon_fence", _render)
Esempio n. 8
0
def dollarmath_plugin(
    md: MarkdownIt,
    allow_labels: bool = True,
    allow_space: bool = True,
    allow_digits: bool = True,
    double_inline: bool = False,
) -> None:
    """Plugin for parsing dollar enclosed math,
    e.g. inline: ``$a=1$``, block: ``$$b=2$$``

    This is an improved version of ``texmath``; it is more performant,
    and handles ``\\`` escaping properly and allows for more configuration.

    :param allow_labels: Capture math blocks with label suffix, e.g. ``$$a=1$$ (eq1)``
    :param allow_space: Parse inline math when there is space
        after/before the opening/closing ``$``, e.g. ``$ a $``
    :param allow_digits: Parse inline math when there is a digit
        before/after the opening/closing ``$``, e.g. ``1$`` or ``$2``.
        This is useful when also using currency.
    :param double_inline: Search for double-dollar math within inline contexts

    """

    md.inline.ruler.before(
        "escape",
        "math_inline",
        math_inline_dollar(allow_space, allow_digits, double_inline),
    )
    md.add_render_rule("math_inline", render_math_inline)

    md.block.ruler.before("fence", "math_block",
                          math_block_dollar(allow_labels))
    md.add_render_rule("math_block", render_math_block)
    md.add_render_rule("math_block_eqno", render_math_block_eqno)
Esempio n. 9
0
def texmath_plugin(md: MarkdownIt, delimiters="dollars", macros: Optional[dict] = None):
    """Plugin ported from
    `markdown-it-texmath <https://github.com/goessner/markdown-it-texmath>`__.

    It parses TeX math equations set inside opening and closing delimiters:

    .. code-block:: md

        $\\alpha = \\frac{1}{2}$

    :param delimiters: one of: brackets, dollars, gitlab, julia, kramdown

    """
    macros = macros or {}

    if delimiters in rules:
        for rule_inline in rules[delimiters]["inline"]:
            md.inline.ruler.before(
                "escape", rule_inline["name"], make_inline_func(rule_inline)
            )

            def render_math_inline(self, tokens, idx, options, env):
                return rule_inline["tmpl"].format(
                    render(tokens[idx].content, False, macros)
                )

            md.add_render_rule(rule_inline["name"], render_math_inline)

        for rule_block in rules[delimiters]["block"]:
            md.block.ruler.before(
                "fence", rule_block["name"], make_block_func(rule_block)
            )

            def render_math_block(self, tokens, idx, options, env):
                return rule_block["tmpl"].format(
                    render(tokens[idx].content, True, macros), tokens[idx].info
                )

            md.add_render_rule(rule_block["name"], render_math_block)
Esempio n. 10
0
def amsmath_plugin(md: MarkdownIt,
                   *,
                   renderer: Optional[Callable[[str], str]] = None):
    """Parses TeX math equations, without any surrounding delimiters,
    only for top-level `amsmath <https://ctan.org/pkg/amsmath>`__ environments:

    .. code-block:: latex

        \\begin{gather*}
        a_1=b_1+c_1\\\\
        a_2=b_2+c_2-d_2+e_2
        \\end{gather*}

    :param renderer: Function to render content, by default escapes HTML

    """
    md.block.ruler.before(
        "blockquote",
        "amsmath",
        amsmath_block,
        {
            "alt":
            ["paragraph", "reference", "blockquote", "list", "footnote_def"]
        },
    )

    if renderer is None:
        _renderer = lambda content: escapeHtml(content)
    else:
        _renderer = renderer

    def render_amsmath_block(self, tokens, idx, options, env):
        content = _renderer(str(tokens[idx].content))
        return f'<div class="math amsmath">\n{content}\n</div>\n'

    md.add_render_rule("amsmath", render_amsmath_block)
Esempio n. 11
0
def dollarmath_plugin(md: MarkdownIt,
                      allow_labels=True,
                      allow_space=True,
                      allow_digits=True):
    """Plugin for parsing dollar enclosed math, e.g. ``$a=1$``.

    :param allow_labels: Capture math blocks with label suffix, e.g. ``$$a=1$$ (eq1)``
    :param allow_space: Parse inline math when there is space
        after/before the opening/closing ``$``, e.g. ``$ a $``
    :param allow_digits: Parse inline math when there is a digit
        before/after the opening/closing ``$``, e.g. ``1$`` or ``$2``.
        This is useful when also using currency.
    """

    md.inline.ruler.before("escape", "math_inline",
                           math_inline_dollar(allow_space, allow_digits))
    md.add_render_rule("math_inline", render_math_inline)

    md.block.ruler.before("fence", "math_block",
                          math_block_dollar(allow_labels))
    md.add_render_rule("math_block", render_math_block)
    md.add_render_rule("math_block_eqno", render_math_block_eqno)
Esempio n. 12
0
def container_plugin(md: MarkdownIt, name, **options):
    """Second param may be useful,
    if you decide to increase minimal allowed marker length
    """

    def validateDefault(params: str, *args):
        return params.strip().split(" ", 2)[0] == name

    def renderDefault(self, tokens, idx, _options, env):
        # add a class to the opening tag
        if tokens[idx].nesting == 1:
            tokens[idx].attrJoin("class", name)

        return self.renderToken(tokens, idx, _options, env)

    min_markers = 3
    marker_str = options.get("marker", ":")
    marker_char = charCodeAt(marker_str, 0)
    marker_len = len(marker_str)
    validate = options.get("validate", validateDefault)
    render = options.get("render", renderDefault)

    def container_func(state: StateBlock, startLine: int, endLine: int, silent: bool):

        auto_closed = False
        start = state.bMarks[startLine] + state.tShift[startLine]
        maximum = state.eMarks[startLine]

        # Check out the first character quickly,
        # this should filter out most of non-containers
        if marker_char != charCodeAt(state.src, start):
            return False

        # Check out the rest of the marker string
        pos = start + 1
        while pos <= maximum:
            if marker_str[(pos - start) % marker_len] != state.src[pos]:
                break
            pos += 1

        marker_count = floor((pos - start) / marker_len)
        if marker_count < min_markers:
            return False
        pos -= (pos - start) % marker_len

        markup = state.src[start:pos]
        params = state.src[pos:maximum]
        if not validate(params, markup):
            return False

        # Since start is found, we can report success here in validation mode
        if silent:
            return True

        # Search for the end of the block
        nextLine = startLine

        while True:
            nextLine += 1
            if nextLine >= endLine:
                # unclosed block should be autoclosed by end of document.
                # also block seems to be autoclosed by end of parent
                break

            start = state.bMarks[nextLine] + state.tShift[nextLine]
            maximum = state.eMarks[nextLine]

            if start < maximum and state.sCount[nextLine] < state.blkIndent:
                # non-empty line with negative indent should stop the list:
                # - ```
                #  test
                break

            if marker_char != charCodeAt(state.src, start):
                continue

            if state.sCount[nextLine] - state.blkIndent >= 4:
                # closing fence should be indented less than 4 spaces
                continue

            pos = start + 1
            while pos <= maximum:
                if marker_str[(pos - start) % marker_len] != state.src[pos]:
                    break
                pos += 1

            # closing code fence must be at least as long as the opening one
            if floor((pos - start) / marker_len) < marker_count:
                continue

            # make sure tail has spaces only
            pos -= (pos - start) % marker_len
            pos = state.skipSpaces(pos)

            if pos < maximum:
                continue

            # found!
            auto_closed = True
            break

        old_parent = state.parentType
        old_line_max = state.lineMax
        state.parentType = "container"

        # this will prevent lazy continuations from ever going past our end marker
        state.lineMax = nextLine

        token = state.push(f"container_{name}_open", "div", 1)
        token.markup = markup
        token.block = True
        token.info = params
        token.map = [startLine, nextLine]

        state.md.block.tokenize(state, startLine + 1, nextLine)

        token = state.push(f"container_{name}_close", "div", -1)
        token.markup = state.src[start:pos]
        token.block = True

        state.parentType = old_parent
        state.lineMax = old_line_max
        state.line = nextLine + (1 if auto_closed else 0)

        return True

    md.block.ruler.before(
        "fence",
        "container_" + name,
        container_func,
        {"alt": ["paragraph", "reference", "blockquote", "list"]},
    )
    md.add_render_rule(f"container_{name}_open", render)
    md.add_render_rule(f"container_{name}_close", render)
Esempio n. 13
0
def dollarmath_plugin(
    md: MarkdownIt,
    *,
    allow_labels: bool = True,
    allow_space: bool = True,
    allow_digits: bool = True,
    double_inline: bool = False,
    label_normalizer: Optional[Callable[[str], str]] = None,
    renderer: Optional[Callable[[str, Dict[str, Any]], str]] = None,
    label_renderer: Optional[Callable[[str], str]] = None,
) -> None:
    """Plugin for parsing dollar enclosed math,
    e.g. inline: ``$a=1$``, block: ``$$b=2$$``

    This is an improved version of ``texmath``; it is more performant,
    and handles ``\\`` escaping properly and allows for more configuration.

    :param allow_labels: Capture math blocks with label suffix, e.g. ``$$a=1$$ (eq1)``
    :param allow_space: Parse inline math when there is space
        after/before the opening/closing ``$``, e.g. ``$ a $``
    :param allow_digits: Parse inline math when there is a digit
        before/after the opening/closing ``$``, e.g. ``1$`` or ``$2``.
        This is useful when also using currency.
    :param double_inline: Search for double-dollar math within inline contexts
    :param label_normalizer: Function to normalize the label,
        by default replaces whitespace with `-`
    :param renderer: Function to render content: `(str, {"display_mode": bool}) -> str`,
        by default escapes HTML
    :param label_renderer: Function to render labels, by default creates anchor

    """
    if label_normalizer is None:
        label_normalizer = lambda label: re.sub(r"\s+", "-", label)

    md.inline.ruler.before(
        "escape",
        "math_inline",
        math_inline_dollar(allow_space, allow_digits, double_inline),
    )
    md.block.ruler.before("fence", "math_block",
                          math_block_dollar(allow_labels, label_normalizer))

    # TODO the current render rules are really just for testing
    # would be good to allow "proper" math rendering,
    # e.g. https://github.com/roniemartinez/latex2mathml

    if renderer is None:
        _renderer = lambda content, _: escapeHtml(content)
    else:
        _renderer = renderer

    if label_renderer is None:
        _label_renderer = (
            lambda label:
            f'<a href="#{label}" class="mathlabel" title="Permalink to this equation">¶</a>'  # noqa: E501
        )
    else:
        _label_renderer = label_renderer

    def render_math_inline(self, tokens, idx, options, env) -> str:
        content = _renderer(
            str(tokens[idx].content).strip(), {"display_mode": False})
        return f'<span class="math inline">{content}</span>'

    def render_math_inline_double(self, tokens, idx, options, env) -> str:
        content = _renderer(
            str(tokens[idx].content).strip(), {"display_mode": True})
        return f'<div class="math inline">{content}</div>'

    def render_math_block(self, tokens, idx, options, env) -> str:
        content = _renderer(
            str(tokens[idx].content).strip(), {"display_mode": True})
        return f'<div class="math block">\n{content}\n</div>\n'

    def render_math_block_label(self, tokens, idx, options, env) -> str:
        content = _renderer(
            str(tokens[idx].content).strip(), {"display_mode": True})
        _id = tokens[idx].info
        label = _label_renderer(tokens[idx].info)
        return f'<div id="{_id}" class="math block">\n{label}\n{content}\n</div>\n'

    md.add_render_rule("math_inline", render_math_inline)
    md.add_render_rule("math_inline_double", render_math_inline_double)

    md.add_render_rule("math_block", render_math_block)
    md.add_render_rule("math_block_label", render_math_block_label)
Esempio n. 14
0
class Zettelkasten(object):

    """This class acts as the container for all notes in the Zettelkasten.
    Methods to work with the notes and associated graph are provided. This
    class mostly provides callers with generators to requested information
    about the Zettelkasten or contained notes.
    """

    def __init__(self, zettelkasten):
        """Constructor.

        :zettelkasten: pathname of directory where the notes are stored

        """
        if not isdir(zettelkasten):
            raise ValueError('Invalid Zettelkasten directory provided')
        self.zettelkasten = zettelkasten
        self.G = None
        # Set up Markdown parser
        self.md = MarkdownIt('default').use(footnote_plugin)
        self.md.add_render_rule('link_open', Zettelkasten.render_link_open)

    @staticmethod
    def render_link_open(instance, tokens, idx, options, env):
        """Change any links to include '.html'. """
        ai = tokens[idx].attrIndex('target')
        try:
            # If the target is an int, convert to point to an HTML file
            target = '{t}.html'.format(t=int(tokens[idx].attrs[ai][1]))
            tokens[idx].attrs[ai][1] = target
        except ValueError:
            # Use target as-is (don't break other links)
            pass
        return instance.renderToken(tokens, idx, options, env)

    def get_note(self, v):
        """Get a single Note instance by id.

        :v: id of Note to retrieve
        :returns: Note

        """
        if not self.exists(v):
            raise ValueError(f'No Note for ID: "{v}" found')
        # Filter by ident attribute
        g = self.get_graph()
        return [n for n, d in g.nodes(data=True) if d[NOTE_ID] == v].pop()

    def get_notes(self):
        """Get all available Notes from the directory.

        :returns: generator of tuple (id, Note)

        """
        # Process all Notes in Zettelkasten
        for root, dirs, files in walk(self.zettelkasten):
            for name in sorted(files):
                try:
                    # Skip filenames which do not contain an int
                    v = int(splitext(name)[0])
                except ValueError:
                    continue
                yield(Note(self.md, v, join(root, name)))

    def get_notes_to(self, s, t):
        """Get all Notes that refer to Note v. If list t is provided, paths are
        resolved between vertex v and each vertex listed in t. The result is
        added to the output.

        :s:
        :t:
        :returns: tuple of tuple (ref, Note), dictionary of paths

        """
        exit_notes = [v for _, v in self._exit_notes()]
        if s in exit_notes:
            return []
        G = self.get_graph()
        # Add direct predecessors, if a predecessor is not an exit Note
        notes_to = [(G.in_degree(n), n, []) for n in G.predecessors(s)
                    if n not in exit_notes]
        for n in t:
            # If no path exists between the source and target, skip
            if not nx.has_path(G, n, s):
                continue
            # Add all paths to target notes
            paths = []
            for path in nx.all_shortest_paths(G, n, s, weight='weight'):
                # TODO: don't generate html filename here, use template
                paths.append(['%2F{}.html'.format(p.get_id())
                              for p in path[::-1]])
            notes_to.append((G.in_degree(n), n, paths))
        return sorted(notes_to, key=itemgetter(0), reverse=True)

    def get_notes_date(self, date, attr=NOTE_MDATE):
        """ """
        G = self.get_graph()
        # Get all notes from specified data till now
        notes = [n for n, d in G.nodes(data=True) if d[attr] > date]
        return self._get_notes(notes)

    def get_filename(self, v):
        """Create a filename based on Note ID. """
        return(join(self.zettelkasten, f'{v}.{EXT_NOTE}'))

    def get_graph(self):
        """Create a directed graph, using each Note as a vertex and the
        Markdown links between Notes as edges. The graph is used to find
        clusters of Notes and to create a visual representation of the
        Zettelkasten.

        :returns: Graph containing all Notes in the Zettelkasten

        """
        # Cached version available?
        if self.G is not None:
            return self.G

        # Add all available Notes to the graph
        self.G = nx.DiGraph()
        self.G.add_nodes_from([(n, {NOTE_ID: n.get_id(),
                                    NOTE_MDATE: n.get_mdate()})
                               for n in self.get_notes()])
        # Get all Notes
        for note in self.G.nodes():
            # Add edges
            for text, v in note.get_links():
                try:
                    # Add edge, include link text for reference in register
                    self.G.add_edge(note, self.get_note(v), text=text)
                except ValueError as e:
                    log.error(f'While processing note "{note.get_id()}": {e}')
        # Update edges to combined in_degree as weight
        for s, t in self.G.edges():
            weight = self.G.in_degree(s) + self.G.in_degree(t)
            self.G.add_edge(s, t, weight=weight)
        # Delete hidden Notes and associated Notes
        notes = set()
        for n in [n for n in self.G.nodes() if n.is_hidden()]:
            notes |= set(self._explore(n, self.G.predecessors))
        log.debug('Deleted note: "{n}"'.format(n='", "'.join(map(str, notes))))
        self.G.remove_nodes_from(notes)
        # Return the populated graph
        return self.G

    def get_stats(self):
        """ Get information about the Zettelkasten. """
        G = self.get_graph()
        stats = {}
        # Number of notes
        stats['nr_vertices'] = len(G.nodes())
        # Number of links between notes
        stats['nr_edges'] = len(G.edges())
        edges = [G.degree(n) for n in G.nodes()]
        stats['avg_edges'] = int(mean(edges))
        stats['min_edges'] = int(min(edges))
        stats['max_edges'] = int(max(edges))
        # Average word count
        wc = [n.get_word_count() for n in G.nodes()]
        stats['word_count'] = sum(wc)
        stats['avg_word_count'] = int(mean(wc))
        # Entry & exit notes
        stats['nr_entry'] = len(self._entry_notes())
        stats['nr_entry_perc'] = int((stats['nr_entry'] /
                                      stats['nr_vertices']) * 100)
        stats['nr_exit'] = len(self._exit_notes())
        stats['nr_exit_perc'] = int((stats['nr_exit'] /
                                     stats['nr_vertices']) * 100)
        # Statistics
        return stats

    def create_note(self, title='', body=''):
        """Create a new Note using a template. Does not write the note to disk.

        :title: title for note
        :body: body for note
        :returns: Note

        """
        G = self.get_graph()
        # Get largest Note ID, increment by 1
        notes = [d[NOTE_ID] for n, d in G.nodes(data=True)]
        v = max(notes) + 1 if len(notes) > 0 else 1
        # Compose Note
        contents = Note.render('new.md.tpl', title=title, body=body,
                               date=datetime.utcnow().isoformat())
        return Note(self.md, v, self.get_filename(v), contents)

    def exists(self, v):
        """Check if a Note with the provided id exists in the graph.

        :v: id of Note to retrieve
        :returns: True if Note exists, False if not

        """
        G = self.get_graph()
        return G.has_node(v)

    def _top_notes(self):
        """Get a top 10 of most referred notes. Based on PageRank. """
        G = self.get_graph()
        pr = nx.pagerank(G)
        notes = [n for n, r in sorted(pr.items(), key=itemgetter(1),
                 reverse=True)]
        return self._get_notes(notes[:10])

    def _get_notes(self, nodes, sort=True):
        """Create a list of notes n for use in templates.

        :vertices: list of vertex IDs

        """
        G = self.get_graph()
        notes = [(G.in_degree(n), n) for n in nodes]
        return (sorted(notes, key=itemgetter(0), reverse=True)
                if sort else notes)

    def _exit_notes(self):
        """Get a list of exit notes. Exit notes have no incoming edges, which
        makes them the ending point for a train of thought.

        :returns: list of tuple(b, Note)

        """
        G = self.get_graph()
        return self._get_notes([v for v, d in G.in_degree() if d == 0])

    def _entry_notes(self):
        """Get a list of entry notes. Entry notes have no outgoing edges, which
        makes them the starting point for a train of thought by following the
        back links.

        :returns: list of tuple(b, Note)

        """
        G = self.get_graph()
        n = [(n, G.in_degree(n), G.out_degree(n)) for n in G.nodes()]
        # Get all notes without outgoing links
        notes = set([note for note, in_degree, out_degree in n
                     if in_degree != 0 and out_degree == 0])
        # Add any note that is marked as an entry note
        notes |= set([v for v in G.nodes() if v.is_entry()])
        return self._get_notes(notes)

    def _inbox(self):
        """List of notes that have neither predecessors or successors.

        :returns: list of tuple(b, Note)

        """
        G = self.get_graph()
        notes = [(n, G.in_degree(n), G.out_degree(n)) for n in G.nodes()]
        return self._get_notes([note for note, in_degree, out_degree in notes
                                if in_degree == 0 and out_degree == 0])

    def index(self):
        """Create a markdown representation of the index of notes.

        :returns: Note

        """
        exit_notes = [u for b, u in self._exit_notes()]
        contents = Note.render('index.md.tpl', top_notes=self._top_notes(),
                               entry_notes=self._entry_notes(),
                               exit_notes=exit_notes,
                               inbox=self._inbox(),
                               date=datetime.utcnow().isoformat())
        return Note(self.md, 0, contents=contents, display_id=False)

    def _register(self):
        """Collect all notes and group by first leter of note title.

        :returns: Tuple of notes sorted by first letter

        """
        def get_predecessors(G, n):
            # Find all predecessors for a note by incoming edge
            p = [(u, data['text']) for u, v, data in G.in_edges(n, data=True)]
            return sorted(p, key=itemgetter(0))
        G = self.get_graph()
        # Get all notes and sort by first letter
        notes = sorted([(n.get_title()[0].upper(), G.in_degree(n), n,
                        get_predecessors(G, n)) for n in G.nodes()],
                       key=itemgetter(0))
        # Group all notes by first letter, yield each group
        for k, group in groupby(notes, key=itemgetter(0)):
            yield((k, sorted(group, key=lambda x: x[2].get_title().upper())))

    def register(self):
        """Create a registry of all notes, sorted by first letter of note
        title.

        :returns Note with register

        """
        exit_notes = [n for b, n in self._exit_notes()]
        entry_notes = [n for b, n in self._entry_notes()]
        contents = Note.render('register.md.tpl', notes=self._register(),
                               stats=self.get_stats(), exit_notes=exit_notes,
                               entry_notes=entry_notes,
                               date=datetime.utcnow().isoformat())
        return Note(self.md, 0, 'Register', contents=contents,
                    display_id=False)

    def _tags(self):
        """Collect all notes and group by tag.

        :returns: Tuple of tag, notes

        """
        G = self.get_graph()
        tags = {}
        for note in G.nodes():
            # Tag the note for each associated tag
            for tag in note.get_tags():
                if tag not in tags:
                    tags[tag] = []
                tags[tag].append(note)
        for k, v in tags.items():
            yield((k, self._get_notes(v)))

    def tags(self):
        """Create an overview of all tags, grouping notes by tag.

        :returns: Note with tags

        """
        contents = Note.render('tags.md.tpl', notes=self._tags(),
                               date=datetime.utcnow().isoformat())
        return Note(self.md, 0, 'Register', contents=contents,
                    display_id=False)

    def _explore(self, s, nodes, depth=None):
        """Find a "train of thought", starting at the note with the provided
        id. Finds all notes from the starting point to any endpoints, by
        following backlinks.

        :s: Note to use as starting point
        :nodes: Iterator to use to traverse the graph. See explore() for an
                example on how to use this
        :returns: generator of Note

        """
        explored = []
        need_visit = set()
        need_visit.add(s)
        while need_visit:
            u = need_visit.pop()
            explored.append(u)
            for v in nodes(u):
                if v not in explored:
                    need_visit.add(v)
        return explored

    def predecessors(self, s, depth=99):
        """Find a "train of thought" across all predecessors (parents).

        :s: id of Note to use as starting point
        :depth: radius of nodes from center to include
        :returns: Note for all collected notes

        """
        s = self.get_note(s)
        G = nx.ego_graph(self.get_graph().reverse(), s, depth, center=False)
        return self.create_note(s.get_title(), Note.render('collected.md.tpl',
                                notes=[s] + list(G)))

    def successors(self, s, depth=99):
        """Find a "train of thought" across all successors (children).

        :s: id of Note to use as starting point
        :depth: radius of nodes from center to include
        :returns: Note for all collected notes

        """
        s = self.get_note(s)
        G = nx.ego_graph(self.get_graph(), s, depth, center=False)
        return self.create_note(s.get_title(), Note.render('collected.md.tpl',
                                notes=[s] + list(G)))

    def _suggestions(self, days):
        """Generate a list of suggestions for recently modified notes.
        Suggested notes are not part of the (multiple) train of thought(s) the
        sampled notes are a part of.

        :days: number of days in the past to look for modified notes
        :returns: list((references, note), [suggested notes])

        :todo: a possible optimalisation is to limited the nodes for review to
               only include exit notes. This reduces the number of notes to
               check to ~30% of the number of notes in the Zettelkasten.
        """
        G = self.get_graph()
        suggestions = []
        for b, t in self.get_notes_date(date.today() - timedelta(days=days)):
            # Find all entry notes that have a path to the sampled note
            entry_notes = [s for _, s in self._entry_notes()
                           if nx.has_path(G, t, s) and s is not t]
            review = list(G.nodes())
            for n in entry_notes:
                for s in G.nodes():
                    # Remove all notes that have a path to the entry note
                    if nx.has_path(G, s, n) and s in review:
                        review.remove(s)
            log.info('{t} has {x} candidates'.format(x=len(review), t=t))
            suggestions.append(((b, t), self._get_notes(sample(review, 3))))
        return suggestions

    def today(self, birthday, days=3):
        """Create an overview of today.

        :birthday: date with birthday
        :days: number of days in the past to look for modified notes
        :returns: Note containing an overview of today

        """
        t = date.today()
        # How many days have I been alive?
        days_from = (t - birthday).days
        # How many days until I reach next [age] milestone?
        age = t.year - birthday.year
        milestone = age if age % 10 == 0 else (age + 10) - (age % 10)
        next_birthday = birthday.replace(year=birthday.year + milestone)
        days_to = (next_birthday - t).days
        # Days since COVID started in NL
        days_covid = (t - date.fromisoformat('2020-02-27')).days
        # Days till Hacker Hotel 2022
        days_hh = (date.fromisoformat('2022-02-11') - t).days
        # Suggestions
        contents = Note.render('today.md.tpl', days_from=days_from,
                               days_to=days_to, milestone=milestone,
                               days_covid=days_covid, days_hh=days_hh,
                               stats=self.get_stats(),
                               inbox=len(self._inbox()),
                               suggestions=self._suggestions(days), days=days,
                               entry_notes=self._entry_notes(),
                               date=datetime.utcnow().isoformat())
        return Note(self.md, 0, contents=contents, display_id=False)

    def lattice(self, v):
        """Create a lattice from any entry note that has a path to Note v. If
        multiple lattices are found, all lattices are displayed.

        :v: ID of Note to find lattices for
        :returns: Note containing lattices

        """
        s = self.get_note(v)
        G = self.get_graph()

        lattice = []
        for t in [t for _, t in self._entry_notes()]:
            if nx.has_path(G, s, t) and s is not t:
                path = self._get_notes(list(nx.all_shortest_paths(
                       G, s, t, weight='weight'))[0][::-1], False)
                lattice.append((t, path))
        contents = Note.render('lattice.md.tpl', source=s, lattice=lattice,
                               date=datetime.utcnow().isoformat())
        return Note(self.md, 0, contents=contents, display_id=False)

    def render(self):
        """Get all Notes in the Zettelkasten, including a list of referring
        Notes for each Note.

        :output: output directory, must exist

        """
        G = self.get_graph()
        exit_notes = [u for b, u in self._exit_notes()]
        # Write all Notes to disk
        for n in G.nodes():
            notes_to = self.get_notes_to(n, exit_notes)
            # Render contents with references as a new Note
            note = Note(self.md, n.get_id(), contents=Note.render(
                        'note.md.tpl', ident=n.get_id(),
                        contents=n.get_contents(), notes_to=notes_to,
                        exit_notes=exit_notes))
            yield(note)
Esempio n. 15
0
def myst_role_plugin(md: MarkdownIt):
    md.inline.ruler.before("backticks", "myst_role", myst_role)
    md.add_render_rule("myst_role", render_myst_role)
Esempio n. 16
0
def myst_role_plugin(md: MarkdownIt):
    """Parse ``{role-name}`content```"""
    md.inline.ruler.before("backticks", "myst_role", myst_role)
    md.add_render_rule("myst_role", render_myst_role)
Esempio n. 17
0
def footnote_plugin(md: MarkdownIt):
    """Plugin ported from
    `markdown-it-footnote <https://github.com/markdown-it/markdown-it-footnote>`__.

    It is based on the
    `pandoc definition <http://johnmacfarlane.net/pandoc/README.html#footnotes>`__:

    .. code-block:: md

        Normal footnote:

        Here is a footnote reference,[^1] and another.[^longnote]

        [^1]: Here is the footnote.

        [^longnote]: Here's one with multiple blocks.

            Subsequent paragraphs are indented to show that they
        belong to the previous footnote.

    """
    md.block.ruler.before("reference", "footnote_def", footnote_def,
                          {"alt": ["paragraph", "reference"]})
    md.inline.ruler.after("image", "footnote_inline", footnote_inline)
    md.inline.ruler.after("footnote_inline", "footnote_ref", footnote_ref)
    md.core.ruler.after("inline", "footnote_tail", footnote_tail)

    md.add_render_rule("footnote_ref", render_footnote_ref)
    md.add_render_rule("footnote_block_open", render_footnote_block_open)
    md.add_render_rule("footnote_block_close", render_footnote_block_close)
    md.add_render_rule("footnote_open", render_footnote_open)
    md.add_render_rule("footnote_close", render_footnote_close)
    md.add_render_rule("footnote_anchor", render_footnote_anchor)

    # helpers (only used in other rules, no tokens are attached to those)
    md.add_render_rule("footnote_caption", render_footnote_caption)
    md.add_render_rule("footnote_anchor_name", render_footnote_anchor_name)
Esempio n. 18
0
def footnote_plugin(md: MarkdownIt):

    md.block.ruler.before("reference", "footnote_def", footnote_def,
                          {"alt": ["paragraph", "reference"]})
    md.inline.ruler.after("image", "footnote_inline", footnote_inline)
    md.inline.ruler.after("footnote_inline", "footnote_ref", footnote_ref)
    md.core.ruler.after("inline", "footnote_tail", footnote_tail)

    md.add_render_rule("footnote_ref", render_footnote_ref)
    md.add_render_rule("footnote_block_open", render_footnote_block_open)
    md.add_render_rule("footnote_block_close", render_footnote_block_close)
    md.add_render_rule("footnote_open", render_footnote_open)
    md.add_render_rule("footnote_close", render_footnote_close)
    md.add_render_rule("footnote_anchor", render_footnote_anchor)

    # helpers (only used in other rules, no tokens are attached to those)
    md.add_render_rule("footnote_caption", render_footnote_caption)
    md.add_render_rule("footnote_anchor_name", render_footnote_anchor_name)
Esempio n. 19
0
def container_plugin(
    md: MarkdownIt,
    name: str,
    marker: str = ":",
    validate: Optional[Callable[[str, str], bool]] = None,
    render=None,
):
    """Plugin ported from
    `markdown-it-container <https://github.com/markdown-it/markdown-it-container>`__.

    It is a plugin for creating block-level custom containers:

    .. code-block:: md

        :::: name
        ::: name
        *markdown*
        :::
        ::::

    :param name: the name of the container to parse
    :param marker: the marker character to use
    :param validate: func(marker, param) -> bool, default matches against the name
    :param render: render func

    """
    def validateDefault(params: str, *args):
        return params.strip().split(" ", 2)[0] == name

    def renderDefault(self, tokens, idx, _options, env):
        # add a class to the opening tag
        if tokens[idx].nesting == 1:
            tokens[idx].attrJoin("class", name)

        return self.renderToken(tokens, idx, _options, env)

    min_markers = 3
    marker_str = marker
    marker_char = charCodeAt(marker_str, 0)
    marker_len = len(marker_str)
    validate = validate or validateDefault
    render = render or renderDefault

    def container_func(state: StateBlock, startLine: int, endLine: int,
                       silent: bool):

        auto_closed = False
        start = state.bMarks[startLine] + state.tShift[startLine]
        maximum = state.eMarks[startLine]

        # Check out the first character quickly,
        # this should filter out most of non-containers
        if marker_char != state.srcCharCode[start]:
            return False

        # Check out the rest of the marker string
        pos = start + 1
        while pos <= maximum:
            try:
                character = state.src[pos]
            except IndexError:
                break
            if marker_str[(pos - start) % marker_len] != character:
                break
            pos += 1

        marker_count = floor((pos - start) / marker_len)
        if marker_count < min_markers:
            return False
        pos -= (pos - start) % marker_len

        markup = state.src[start:pos]
        params = state.src[pos:maximum]
        assert validate is not None
        if not validate(params, markup):
            return False

        # Since start is found, we can report success here in validation mode
        if silent:
            return True

        # Search for the end of the block
        nextLine = startLine

        while True:
            nextLine += 1
            if nextLine >= endLine:
                # unclosed block should be autoclosed by end of document.
                # also block seems to be autoclosed by end of parent
                break

            start = state.bMarks[nextLine] + state.tShift[nextLine]
            maximum = state.eMarks[nextLine]

            if start < maximum and state.sCount[nextLine] < state.blkIndent:
                # non-empty line with negative indent should stop the list:
                # - ```
                #  test
                break

            if marker_char != state.srcCharCode[start]:
                continue

            if state.sCount[nextLine] - state.blkIndent >= 4:
                # closing fence should be indented less than 4 spaces
                continue

            pos = start + 1
            while pos <= maximum:
                try:
                    character = state.src[pos]
                except IndexError:
                    break
                if marker_str[(pos - start) % marker_len] != character:
                    break
                pos += 1

            # closing code fence must be at least as long as the opening one
            if floor((pos - start) / marker_len) < marker_count:
                continue

            # make sure tail has spaces only
            pos -= (pos - start) % marker_len
            pos = state.skipSpaces(pos)

            if pos < maximum:
                continue

            # found!
            auto_closed = True
            break

        old_parent = state.parentType
        old_line_max = state.lineMax
        state.parentType = "container"

        # this will prevent lazy continuations from ever going past our end marker
        state.lineMax = nextLine

        token = state.push(f"container_{name}_open", "div", 1)
        token.markup = markup
        token.block = True
        token.info = params
        token.map = [startLine, nextLine]

        state.md.block.tokenize(state, startLine + 1, nextLine)

        token = state.push(f"container_{name}_close", "div", -1)
        token.markup = state.src[start:pos]
        token.block = True

        state.parentType = old_parent
        state.lineMax = old_line_max
        state.line = nextLine + (1 if auto_closed else 0)

        return True

    md.block.ruler.before(
        "fence",
        "container_" + name,
        container_func,
        {"alt": ["paragraph", "reference", "blockquote", "list"]},
    )
    md.add_render_rule(f"container_{name}_open", render)
    md.add_render_rule(f"container_{name}_close", render)
Esempio n. 20
0
def markdownify(content):
    md = MarkdownIt("commonmark", {"typographer": True})
    md.enable(["smartquotes"])
    md.add_render_rule("link_open", render_blank_link)
    return md.render(content)