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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
def myst_role_plugin(md: MarkdownIt): md.inline.ruler.before("backticks", "myst_role", myst_role) md.add_render_rule("myst_role", render_myst_role)
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)
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)
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)
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)
def markdownify(content): md = MarkdownIt("commonmark", {"typographer": True}) md.enable(["smartquotes"]) md.add_render_rule("link_open", render_blank_link) return md.render(content)