# -*- coding: utf-8 -*-
#
# Copyright © 2017 markdown-link-style contributors.
#
# This file is part of markdown-link-style.
#
# markdown-link-style is free software: you can redistribute it
# and/or modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation, either version 3 of
# the License, or (at your option) any later version.
#
# markdown-link-style is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with markdown-link-style (see COPYING). If not, see
# <http://www.gnu.org/licenses/>.
import argparse
import re
from mistune import BlockGrammar, BlockLexer, InlineLexer, Renderer, Markdown
from markdown_link_style.logging import MDLSLogger
from markdown_link_style._version import __version__
# Initialize logger for this module.
logger = MDLSLogger(__name__)
# from mistune
_inline_tags = [
"a",
"em",
"strong",
"small",
"s",
"cite",
"q",
"dfn",
"abbr",
"data",
"time",
"code",
"var",
"samp",
"kbd",
"sub",
"sup",
"i",
"b",
"u",
"mark",
"ruby",
"rt",
"rp",
"bdi",
"bdo",
"span",
"br",
"wbr",
"ins",
"del",
"img",
"font",
]
_valid_end = r"(?!:/|[^\w\s@]*@)\b"
_block_tag = r"(?!(?:%s)\b)\w+%s" % ("|".join(_inline_tags), _valid_end)
def _pure_pattern(regex):
"""Function from mistune."""
pattern = regex.pattern
if pattern.startswith("^"):
pattern = pattern[1:]
return pattern
class LSBlockGrammar(BlockGrammar):
def __init__(self):
# remove list_block and block_quote from paragraph
self.paragraph = re.compile(
r"^((?:[^\n]+\n?(?!"
r"%s|%s|%s|%s|%s|%s|%s"
r"))+)\n*"
% (
_pure_pattern(self.fences).replace(r"\1", r"\2"),
_pure_pattern(self.hrule),
_pure_pattern(self.heading),
_pure_pattern(self.lheading),
_pure_pattern(self.def_links),
_pure_pattern(self.def_footnotes),
"<" + _block_tag,
)
)
class LSBlockLexer(BlockLexer):
"""Link Style Block Lexer.
"""
grammar_class = LSBlockGrammar
def __init__(self, rules=None, **kwargs):
super(LSBlockLexer, self).__init__(rules, **kwargs)
# Only parse these block rules.
self.default_rules = ["def_links", "paragraph", "text"]
class LSInlineLexer(InlineLexer):
"""Link Style Inline Lexer.
"""
def __init__(self, renderer, rules=None, **kwargs):
super(LSInlineLexer, self).__init__(renderer, rules, **kwargs)
# Only parse these inline rules
self.default_rules = ["autolink", "link", "reflink", "text"]
class LSRenderer(Renderer):
"""Link Style Renderer.
"""
def __init__(self, **kwargs):
super(LSRenderer, self).__init__(**kwargs)
# Link style is either 'inline' or 'footnote'.
self.link_style = self.options.get("link_style")
self.fn_lnk_num = 0 # footnote style link number
self.fn_lnk_refs = [] # footnote style link refs
def text(self, text):
return text
def autolink(self, link, is_email=False):
return "<{}>".format(link)
def paragraph(self, text):
p = text
fn_refs = self._pop_fn_refs()
if fn_refs:
# Insert footnote refs, if any, after paragraph.
return "\n{}\n\n{}".format(p, fn_refs)
return "\n{}\n".format(p)
def link(self, link, title, text):
link_text = self._stylize_link(link, title, text)
return link_text
def image(self, src, title, text):
# Markup for images are same as links, except it is prefixed
# with a bang (!).
return "{}{}".format("!", self.link(src, title, text))
def _stylize_link(self, link, title, text):
if self.link_style == "inline":
return self._gen_inline_link(link, title, text)
else:
return self._gen_footnote_link(link, title, text)
def _gen_inline_link(self, link, title, text):
if title:
return '[{}]({} "{}")'.format(text, link, title)
else:
return "[{}]({})".format(text, link)
def _gen_footnote_link(self, link, title, text):
fn_num = self._st_fn_ref(link, title)
return "[{}][{}]".format(text, fn_num)
def _st_fn_ref(self, link, title):
"""Store footnote link reference.
"""
fn_num = self._get_fn_lnk_num()
if title:
fn_ref = "[{}]: {} ({})".format(fn_num, link, title)
else:
fn_ref = "[{}]: {}".format(fn_num, link)
self.fn_lnk_refs.append(fn_ref)
return fn_num
def _get_fn_lnk_num(self):
"""Get footnote link number.
"""
fn_num = self.fn_lnk_num
self.fn_lnk_num = self.fn_lnk_num + 1
return fn_num
def _pop_fn_refs(self):
"""Pop all footnote refs and return them as a string.
"""
refs = ""
for ref in self.fn_lnk_refs:
refs += "{}\n".format(ref)
# Empty fn_lnk_refs
self.fn_lnk_refs = []
return refs
class LSMarkdown(Markdown):
"""Link Style Markdown parser.
"""
def __init__(self, renderer=None, inline=None, block=None, **kwargs):
link_style = kwargs.get("link_style") or "inline"
if not renderer:
renderer = LSRenderer(link_style=link_style)
if not inline:
inline = LSInlineLexer(renderer)
if not block:
block = LSBlockLexer()
super(LSMarkdown, self).__init__(renderer, inline, block, **kwargs)
def parse(self, text):
# Reset footnote link variables.
self.renderer.fn_lnk_num = 0
self.renderer.fn_lnk_refs = []
# Parse text.
out = super(LSMarkdown, self).parse(text)
# Spit out.
return out.lstrip("\n")
class LinkStyler(object):
"""Markdown Link Styler.
"""
def __init__(self, link_style="inline"):
self.style = link_style
def __call__(self, file_):
return self._link_stylize(file_)
def _link_stylize(self, file_):
text = file_.read()
md = LSMarkdown(link_style=self.style)
return md(text)
def _write_to(file_, content):
"""Write `content` to `file_`.
`file_` is expected to be a sub-class of `io.TextIOBase`.
"""
file_.truncate(0)
file_.seek(0)
file_.write(content)
file_.flush()
file_.close()
def _mdl_stylize(args):
ls = LinkStyler(args.link_style)
stylized_content = ls(args.in_file)
if args.out_file:
args.in_file.close()
_write_to(open(args.out_file, "wt"), stylized_content)
else:
_write_to(args.in_file, stylized_content)
def _get_args(args=None):
parser = argparse.ArgumentParser()
parser.add_argument("--version", action="version", version=__version__)
parser.add_argument(
"link_style", choices=["inline", "footnote"], help="markdown link style."
)
parser.add_argument(
"in_file", type=argparse.FileType("rt+"), help="path to markdown file."
)
parser.add_argument(
"out_file",
nargs="?",
type=str,
default=None,
help=" ".join(
[
"path to output file.",
"if it is not given, the output is",
"directly written to the original",
"in_file.",
]
),
)
return parser.parse_args(args)
def main():
args = _get_args()
_mdl_stylize(args)