Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
catcombo committed Mar 11, 2021
2 parents 41a6604 + bd9add9 commit d906dfb
Show file tree
Hide file tree
Showing 20 changed files with 902 additions and 233 deletions.
54 changes: 53 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
# jira2markdown
# Overview

`jira2markdown` is a text converter from [JIRA markup](https://jira.atlassian.com/secure/WikiRendererHelpAction.jspa?section=all) to [YouTrack Markdown](https://www.jetbrains.com/help/youtrack/standalone/youtrack-markdown-syntax-issues.html) using parsing expression grammars. The Markdown implementation in YouTrack follows the [CommonMark specification](https://spec.commonmark.org/0.29/) with extensions. Thus, `jira2markdown` can be used to convert text to any Markdown syntax with minimal modifications.

# Table of Contents

* [Prerequisites](#prerequisites)
* [Installation](#installation)
* [Usage](#usage)
* [Conversion tables](#conversion-tables)
* [Headings](#headings)
* [Text Effects](#text-effects)
* [Text Breaks](#text-breaks)
* [Links](#links)
* [Lists](#lists)
* [Images](#images)
* [Tables](#tables)
* [Advanced Formatting](#advanced-formatting)
* [Customization](#customization)

# Prerequisites

- Python 3.6+
Expand Down Expand Up @@ -296,3 +312,39 @@ Some text with a title
</td>
</tr>
</table>

# Customization

To customize the list of markup elements send it as an optional argument to `convert`:
```python
from jira2markdown import convert
from jira2markdown.elements import MarkupElements
from jira2markdown.markup.links import Link
from jira2markdown.markup.text_effects import Bold

# Only bold and link tokens will be converted here
elements = MarkupElements([Link, Bold])
convert("Some Jira text here", elements=elements)
```

Keep in mind that the order of markup elements is important! Elements are matching first from top to bottom in the list.

To override some elements in the default element list use `insert_after`/`replace` methods:
```python
from jira2markdown import convert
from jira2markdown.elements import MarkupElements
from jira2markdown.markup.base import AbstractMarkup
from jira2markdown.markup.links import Link
from jira2markdown.markup.text_effects import Color

class CustomColor(Color):
...

class MyElement(AbstractMarkup):
...

elements = MarkupElements()
elements.replace(Color, CustomColor)
elements.insert_after(Link, MyElement)
convert("Some Jira text here", elements=elements)
```
67 changes: 67 additions & 0 deletions jira2markdown/elements.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from typing import Iterable, Type

from pyparsing import Forward, MatchFirst, ParseExpression

from jira2markdown.markup.advanced import Code, Noformat, Panel
from jira2markdown.markup.base import AbstractMarkup
from jira2markdown.markup.headings import Headings
from jira2markdown.markup.images import Image
from jira2markdown.markup.links import Attachment, Link, MailTo, Mention
from jira2markdown.markup.lists import OrderedList, UnorderedList
from jira2markdown.markup.tables import Table
from jira2markdown.markup.text_breaks import LineBreak, Mdash, Ndash, Ruler
from jira2markdown.markup.text_effects import BlockQuote, Bold, Color, EscSpecialChars, InlineQuote, Monospaced, \
Quote, Strikethrough, Subscript, Superscript, Underline


class MarkupElements(list):
def __init__(self, seq: Iterable = ()):
super().__init__(seq or [
UnorderedList,
OrderedList,
Code,
Noformat,
Monospaced,
Mention,
MailTo,
Attachment,
Link,
Image,
Table,
Headings,
Quote,
BlockQuote,
Panel,
Bold,
Ndash,
Mdash,
Ruler,
Strikethrough,
Underline,
InlineQuote,
Superscript,
Subscript,
Color,
LineBreak,
EscSpecialChars,
])

def insert_after(self, element: Type[AbstractMarkup], new_element: Type[AbstractMarkup]):
index = self.index(element)
self.insert(index + 1, new_element)

def replace(self, old_element: Type[AbstractMarkup], new_element: Type[AbstractMarkup]):
index = self.index(old_element)
self[index] = new_element

def expr(
self,
inline_markup: Forward,
markup: Forward,
usernames: dict,
elements: Iterable[Type[AbstractMarkup]],
) -> ParseExpression:
return MatchFirst([
element(inline_markup=inline_markup, markup=markup, usernames=usernames).expr
for element in elements
])
25 changes: 14 additions & 11 deletions jira2markdown/markup/advanced.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from pyparsing import Combine, FollowedBy, Forward, Group, Literal, OneOrMore, Optional, ParseResults, ParserElement, \
from pyparsing import Combine, FollowedBy, Group, Literal, OneOrMore, Optional, ParseResults, ParserElement, \
QuotedString, SkipTo, Suppress, Word, alphanums, alphas

from jira2markdown.markup.base import AbstractMarkup

class Noformat:

class Noformat(AbstractMarkup):
def action(self, tokens: ParseResults) -> str:
text = tokens[0].strip("\n")
return f"```\n{text}\n```"
Expand All @@ -12,7 +14,7 @@ def expr(self) -> ParserElement:
return QuotedString("{noformat}", multiline=True).setParseAction(self.action)


class Code:
class Code(AbstractMarkup):
def action(self, tokens: ParseResults) -> str:
lang = tokens.lang or "Java"
text = tokens.text.strip("\n")
Expand All @@ -33,18 +35,19 @@ def expr(self) -> ParserElement:
).setParseAction(self.action)


class Panel:
def __init__(self, markup: Forward):
self.markup = markup

class Panel(AbstractMarkup):
def action(self, tokens: ParseResults) -> str:
text = self.markup.transformString(tokens.text.strip())

for param, value in tokens.get("params", []):
if param.lower() == "title":
text = f"**{value}**\n{text}"
prefix = f"> **{value}**\n"
break
else:
prefix = ""

return "\n".join([f"> {line.lstrip()}" for line in text.splitlines()])
text = self.markup.transformString("\n".join([
line.lstrip() for line in tokens.text.strip().splitlines()
]))
return prefix + "\n".join([f"> {line}" for line in text.splitlines()])

@property
def expr(self) -> ParserElement:
Expand Down
15 changes: 15 additions & 0 deletions jira2markdown/markup/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from pyparsing import Forward, ParserElement


class AbstractMarkup:
is_inline_element: bool = True

def __init__(self, inline_markup: Forward, markup: Forward, usernames: dict):
self.inline_markup = inline_markup
self.markup = markup
self.usernames = usernames
self.init_kwargs = dict(inline_markup=inline_markup, markup=markup, usernames=usernames)

@property
def expr(self) -> ParserElement:
raise NotImplementedError
17 changes: 12 additions & 5 deletions jira2markdown/markup/headings.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
from pyparsing import Combine, ParseResults, ParserElement, StringStart, Word
from pyparsing import Combine, LineEnd, ParseResults, ParserElement, SkipTo, StringEnd, StringStart, Word

from jira2markdown.markup.base import AbstractMarkup


class Headings(AbstractMarkup):
is_inline_element = False

class Headings:
def action(self, tokens: ParseResults) -> str:
return "#" * int(tokens[0][1]) + " "
return "#" * int(tokens.level[1]) + " " + self.inline_markup.transformString(tokens.text)

@property
def expr(self) -> ParserElement:
return ("\n" | StringStart()) \
+ Combine(Word("h", "123456", exact=2) + ". ").setParseAction(self.action)
return ("\n" | StringStart()) + Combine(
Word("h", "123456", exact=2).setResultsName("level")
+ ". "
+ SkipTo(LineEnd() | StringEnd()).setResultsName("text"),
).setParseAction(self.action)
4 changes: 3 additions & 1 deletion jira2markdown/markup/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
from pyparsing import Combine, Optional, ParseResults, ParserElement, PrecededBy, Regex, SkipTo, StringStart, Word, \
printables

from jira2markdown.markup.base import AbstractMarkup

class Image:

class Image(AbstractMarkup):
def action(self, tokens: ParseResults) -> str:
return f"![{tokens.url}]({tokens.url})"

Expand Down
56 changes: 32 additions & 24 deletions jira2markdown/markup/links.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,59 @@
from string import punctuation

from pyparsing import CaselessLiteral, Char, Combine, FollowedBy, Forward, Optional, ParseResults, ParserElement, \
from pyparsing import CaselessLiteral, Char, Combine, FollowedBy, Optional, ParseResults, ParserElement, \
PrecededBy, SkipTo, StringEnd, StringStart, Suppress, White, Word, alphanums

from jira2markdown.markup.base import AbstractMarkup

class MailTo:

class MailTo(AbstractMarkup):
def action(self, tokens: ParseResults) -> str:
return f"<{tokens.email}>"
alias = self.markup.transformString(getattr(tokens, "alias", ""))
email = tokens.email

if (alias == email) or (len(alias.strip()) == 0):
return f"<{email}>"
else:
return f"[{alias}](mailto:{tokens.email})"

@property
def expr(self) -> ParserElement:
return Combine(
"["
+ Optional(
SkipTo("|", failOn="]") + Suppress("|"),
)
+ Optional(SkipTo("|", failOn="]").setResultsName("alias") + "|")
+ "mailto:"
+ Word(alphanums + "@.-").setResultsName("email")
+ Word(alphanums + "@.-_").setResultsName("email")
+ "]",
).setParseAction(self.action)


class Link:
def __init__(self, markup: Forward):
self.markup = markup
class Link(AbstractMarkup):
URL_PREFIXES = ["http", "ftp"]

def action(self, tokens: ParseResults) -> str:
alias = getattr(tokens, "alias", "")
alias = self.markup.transformString(getattr(tokens, "alias", ""))
url = tokens.url

if len(alias) > 0:
alias = self.markup.transformString(alias)
return f"[{alias}]({url})"
else:
return f"<{url}>"
if url.lower().startswith("www."):
url = f"https://{url}"

if not any(map(url.lower().startswith, self.URL_PREFIXES)):
url = self.markup.transformString(url)
return fr"[{alias}\|{url}]" if alias else f"[{url}]"

return f"[{alias}]({url})" if len(alias) > 0 else f"<{url}>"

@property
def expr(self) -> ParserElement:
ALIAS_LINK = SkipTo("|", failOn="]").setResultsName("alias") + "|" + SkipTo("]").setResultsName("url")
LINK = Combine("http" + SkipTo("]")).setResultsName("url")
return Combine("[" + (LINK ^ ALIAS_LINK) + "]").setParseAction(self.action)
return Combine(
"["
+ Optional(SkipTo("|", failOn="]").setResultsName("alias") + "|")
+ SkipTo("]").setResultsName("url")
+ "]",
).setParseAction(self.action)


class Attachment:
class Attachment(AbstractMarkup):
def action(self, tokens: ParseResults) -> str:
return f"[{tokens.filename}]({tokens.filename})"

Expand All @@ -51,10 +62,7 @@ def expr(self) -> ParserElement:
return Combine("[^" + SkipTo("]").setResultsName("filename") + "]").setParseAction(self.action)


class Mention:
def __init__(self, usernames: dict):
self.usernames = usernames

class Mention(AbstractMarkup):
def action(self, tokens: ParseResults) -> str:
username = self.usernames.get(tokens.accountid)
return f"@{tokens.accountid}" if username is None else f"@{username}"
Expand Down
Loading

0 comments on commit d906dfb

Please sign in to comment.