Custom markdown parser not working properly

Viewed 48

So I am making my own little script to detect some markdown notations in a variable. Now there are two issues.

  • First of all, my current approach does not work when using the markdown as following (I am referring to the question mark):
**am I working**?
  • Aside from that does the code look quite messy and I am pretty sure that it can be done more efficiently.

I was wondering as to what could be a better way to do what I am trying.
I want to add that I have looked for packages but these wont work in my case as I only want to allow bold, italic, underline and escaping (**bold**, _italic_ and __underline__).
My current code:

description = "**one more test** _just to be sure_ \*hi\* apparently __underline broke__?"

for word in description.split(" "):
    if word.startswith("**"):    
        description = description.replace(word, f'<b>{word.replace("**", "", 1)}')
        word = word.replace("**", "", 1)
    if word.endswith("**") and not word.endswith("\**"):  
        description = description.replace(word, f'{word.replace("**", "", 1)}</b>')
        word = word.replace("**", "", 1)
    if word.startswith("*"):
        description = description.replace(word, f'<i>{word.replace("*", "", 1)}')
        word = word.replace("*", "", 1)
    if word.endswith("*") and not word.endswith("\*"):
        description = description.replace(word, f'{word.replace("*", "", 1)}</i>')   
        word = word.replace("*", "", 1) 
    if word.startswith("__"):
        description = description.replace(word, f'<u>{word.replace("__", "", 1)}')
        word = word.replace("__", "", 1)
    if word.endswith("__") and not word.endswith("\__"):
        description = description.replace(word, f'{word.replace("__", "", 1)}</u>')    
        word = word.replace("__", "", 1)
    if word.startswith("_"):
        description = description.replace(word, f'<i>{word.replace("_", "", 1)}')
        word = word.replace("_", "", 1)
    if word.endswith("_") and not word.endswith("\_"):
        description = description.replace(word, f'{word.replace("_", "", 1)}</i>')     
        word = word.replace("_", "", 1)   

print(description) 
1 Answers

First of all, you should rely on regex expressions to be able to cover the different cases. Also, working with a clean code by defining functions and loops is better for future changes and readability.

So, to solve your special characters issue (like ?, !, ..etc.), we can have the function below:

import re

def _check_match(text, chars, start_or_end="start", excluded_chars='()?!'):
    # check if the text ends or starts with the required characters 
    # but could have any of the excluded characters before (if start) or after (if end)
    if start_or_end == "start":
        pattern = re.compile(rf'([{excluded_chars}]*){re.escape(chars)}(.*)$')
    elif start_or_end == "end":
        pattern = re.compile(rf'(.*[^\\]){re.escape(chars)}([{excluded_chars}]*)$')
    else:
        return False, None, None
    check = re.match(pattern, text)
    if check:
        return True, check.group(1), check.group(2)
    return False, None, None

def check_match_start(text, chars):
    return _check_match(text, chars, "start")

def check_match_end(text, chars):
    return _check_match(text, chars, "end")

Examples:

check_match_start("**test", "**") >> (True, '', 'test')
check_match_start("(**test", "**") >> (True, '(', 'test')
check_match_end("test_?", "_") >> (True, 'test', '?')
check_match_end("test__?", "*") >> (False, None, None)

Then, we can have another function that loops on all the required markdowns to apply:

def markdown_text(text):
    markdown_criteria = [
        ("**", ["<b>", '</b>']),
        ("*", ["<i>", "</i>"]),
        ("__", ["<u>", "</u>"]),
        ("_", ["<i>", "</i>"])
    ]
    for m in markdown_criteria:
        chars = m[0]
        replacement_start = m[1][0]
        replacement_end = m[1][1]
        my_start_check, matched_special_chars, matched_text = check_match_start(text, chars)
        if my_start_check:
            text = f'{matched_special_chars}{replacement_start}{matched_text}'
        my_end_check, matched_text, matched_special_chars = check_match_end(text, chars)
        if my_end_check:
            text = f'{matched_text}{replacement_end}{matched_special_chars}'
    return text

Examples:

markdown_text("test__?") >> 'test</u>?' 
markdown_text("*test") >> '<i>test' 
markdown_text("**test**!") >> '<b>test</b>!'

It is time to execute:

description = "**one more test** _just to be sure_ \*hi\* apparently __underline broke__?"
new_description = []
for word in description.split(" "):
    new_description.append(markdown_text(word))

new_description = " ".join(new_description)
print(new_description) >> <b>one more test</b> <i>just to be sure</i> \*hi\* apparently <u>underline broke</u>?

This solution provides more flexibility to:

  • add any other needed markdowns by simply updating markdown_criteria list (for example, you can add ("***", ["<b><i>", '</i></b>']) to have bold and italic together, but make sure to add it in the beginning of the list not to be overriden by the next bold and italic criteria)
  • exclude more or less special characters by simply updating the parameter excluded_chars in _check_match function

Needed enhancements:

This solution doesn't cover the case for an instance if you have a word starting with ** in your phrase and there is no other word that ends with **, so you will end up having <b> without </b>. This can be solved if you can deal with your phrase also as a bulk in a regex expression instead of splitting the phrase with space and working with each word separately

Related