#!/usr/bin/env python
# -*- coding: utf-8 -*-
from re import compile as RECompile
from re import Pattern as REPattern
from re import Match as REMatch
from re import MULTILINE as RE_MULTILINE
from re import IGNORECASE as RE_IGNORE_CASE
from typing import Callable, Optional
from os.path import exists as path_exists
from os.path import dirname as directory_name
from base64 import b64encode as base64_encode
from json import dumps as json_encode
class WMarkDown:
# Languages
HTML:int = 1 << 0
# Analyse modes
RAW:int = 0
SUBITEM:int = 1 << 0
LINKED:int = 1 << 1
html_special_characters:dict[str, str] = {
"<" : "lt",
">" : "gt",
"&" : "amp"
}
quote_special:dict[str, str] = {
"?" : "ask",
"!" : "warning",
">" : "answer",
"Q" : "comment",
"#" : "note"
}
item_mark:tuple[str, str, REPattern] = ("###@&&_", "_&&@###", RECompile(r'\#{3}\@\&{2}_([0-9]+)_\&{2}\@\#{3}'))
re_block_html_pattern:str = r'^[ \t]*<\!\-{2}[ \t]*\[{2}[\t ]*(wmd|wmarkdown)[ \t]*\]{2}[\t ]*\-{2}>'
re_block_html:REPattern = RECompile(re_block_html_pattern, RE_MULTILINE)
re_block_mark:REPattern = RECompile(r'^[ \t]*((" ?){3}|(\' ?){3}|(` ?){3})[ \t]*(wmd|wmarkdown)|' + re_block_html_pattern, RE_MULTILINE)
re_new_lines:REPattern = RECompile(r'[\r\n]+')
re_new_line:REPattern = RECompile(r'\n|\r\n|\r')
re_started_white_spaces:REPattern = RECompile(r'^[ \t]*')
re_lines:REPattern = RECompile(r'^[^\r\n]+', RE_MULTILINE)
re_list_line:REPattern = RECompile(r'^([ \t]*)(([\-\*#\+]+)|([0-9]+|[a-z]+)\.)?(.*)$', RE_IGNORE_CASE | RE_MULTILINE)
re_table_line:REPattern = RECompile(r'^\|([^\r\n"\']+|"([^"\\\\]+|\\\\(.|[\r\n])|[\r\n]+)*"|\'([^\'\\\\]+|\\\\(.|[\r\n])|[\r\n]+)*\')*[\r\n]*', RE_MULTILINE)
re_table_item:REPattern = RECompile(r'(\|+)(([^\|\'\"]+|"([^"\\\\]+|\\\\(.|[\r\n])|[\r\n]+)*"|\'([^\'\\\\]+|\\\\(.|[\r\n])|[\r\n]+)*\')*)')
re_options:REPattern = RECompile(r'`{3}[ \t]*(wmd|wmarkdown)\-options[ \t]*(\n|\r\n|\r)(([^\r\n`]+|[\r\n]+|`{1,2}[^`])*)`{3}')
re_option:REPattern = RECompile(r'^([^=\r\n]+)=([^\r\n]*)$', RE_MULTILINE)
re_extension:REPattern = RECompile(r'(([^\.\/\\\\]+)\.)?([^\.\/\\\\]+)$')
re_characters_no_id:REPattern = RECompile(r'[^a-z0-9]+', RE_IGNORE_CASE)
re_phone_number:REPattern = RECompile(r'^\+?[0-9 ]{5,22}$')
re_email_address:REPattern = RECompile(r'^[a-z\.0-9_\-]+@[a-z\.0-9_\-]+\.[a-z0-9]+$')
re_class_attribute:REPattern = RECompile(r'(?]+)>|("[^"]*"|\'[^\']*\')')
re_start_with_white_spaces:REPattern = RECompile(r'^[ \t]', RE_MULTILINE)
re_domain:REPattern = RECompile(r'^[^\:]+\:\/{2}[^\/]+')
re_protocol:REPattern = RECompile(r'^([a-z0-9_\-]+)\:', RE_IGNORE_CASE)
def __init__(self) -> None:
self.modules:dict[str, tuple[int, REPattern]] = {
"special_characters" : (WMarkDown.SUBITEM, RECompile(r'\\([\(\{\[\*\\])')),
"title" : (WMarkDown.RAW, RECompile(r'^((#{1,6})[\t ]*([^\r\n]+)|(={1,6})[\t ]*([^\r\n=]+)={1,6})(\n|\r\n|\r){2,}', RE_MULTILINE)),
"list" : (WMarkDown.RAW, RECompile(r'^([\-\*#\+]+|([0-9]+|[a-z]+)\.)[^\r\n]*(\n|\r\n|\r)([\t ]*(([\-\*#\+]+|([0-9]+|[a-z]+)\.)[^\r\n]*)(\n|\r\n|\r)?)*', RE_IGNORE_CASE | RE_MULTILINE)),
"code_block" : (WMarkDown.RAW, RECompile(r'^("{3}([^\r\n]*)(((?!"{3})(.|[\r\n]))+)"{3}|" " "([^\r\n]*)(((?!" " ")(.|[\r\n]))+)" " "|\'{3}([^\r\n]*)(((?!\'{3})(.|[\r\n]))+)\'{3}|\' \' \'([^\r\n]*)(((?!\' \' \')(.|[\r\n]))+)\' \' \'|`{3}([^\r\n]*)(((?!`{3})(.|[\r\n]))+)`{3}|` ` `([^\r\n]*)(((?!` ` `)(.|[\r\n]))+)` ` `)', RE_MULTILINE)),
"table" : (WMarkDown.RAW, RECompile(r'^\[\|([^\r\n]*)[\r\n]+((^\|([^\]]([^\r\n"\']|"([^"\\\\]+|\\\\(.|[\r\n])|[\r\n]+)*"|\'([^\'\\\\]+|\\\\(.|[\r\n])|[\r\n]+)*\')*)?[\r\n]+)*)^\|\]', RE_MULTILINE)),
"quote" : (WMarkDown.RAW, RECompile(r'^>[ \t]*(\[\![\t ]*([^\] \t]+)([ \t]+([^\]]+))?\])?(([^\r\n]+(\r\n|[\r\n])?)*)', RE_MULTILINE)),
"include" : (WMarkDown.RAW, RECompile(r'^\[{2}include[ \t]+([^\]]+)\]{2}', RE_MULTILINE)),
"media" : (WMarkDown.SUBITEM | WMarkDown.LINKED, RECompile(r'\({2}\!(image|icon|video|audio|sound|picture)[ \t]+("(([^"]+|[\r\n]+))"|\'(([^\']+|[\r\n]+))\'|([^ \t\)]+))([ \t]*|[ \t]+("(([^\\\\"]+|\\\\.|[\r\n]+)*)"|\'(([^\\\\\']+|\\\\.|[\r\n]+)*)\'|([^\)]+)))\){2}', RE_IGNORE_CASE)),
"code_doc" : (WMarkDown.RAW, RECompile(r'^\[{2}@(([^\]]+|\][^\]])+)\]{2}', RE_MULTILINE)),
"presentation_links" : (WMarkDown.RAW, RECompile(r'^\[{2}"{3}(([^"]+|[\r\n]+|"{1,2}[^"])*)"{3}\]{2}', RE_MULTILINE)),
"paragraph" : (WMarkDown.RAW, RECompile(r'^(([^\r\n]+(\n|\r\n|\r)?)+)', RE_MULTILINE)),
# "bold_italic" : (WMarkDown.SUBITEM | WMarkDown.LINKED, RECompile(r'\*{3}(([^\*]+|\*{1,2}[^\*]|[\r\n]+|\\\*)*)\*{3}')),
# "bold_italic" : (WMarkDown.SUBITEM | WMarkDown.LINKED, RECompile(r'(?]+|[a-z\.0-9_\-]+@[a-z\.0-9_\-]+\.[a-z0-9]+)', RE_IGNORE_CASE))
}
self.__ids_cache:tuple[str] = tuple()
def __id(self, string:str) -> str:
id:str = WMarkDown.re_characters_no_id.sub(r'-', string).lower()
if id[0] == '-':
id = id[1:]
if id[-1] == '-':
id = id[:-1]
if id in self.__ids_cache:
i:int = 2
while id + "-" + str(i) in self.__ids_cache:
i += 1
id += "-" + str(i)
self.__ids_cache += (id,)
return id
def reset_ids(self):
self.__ids_cache = tuple()
def module_title(self, matches:REMatch, language:Optional[int] = HTML, path:Optional[str] = None) -> str|None:
if language & WMarkDown.HTML:
level:str = str(len(matches.group(2) or matches.group(4)))
content:str = (matches.group(3) or matches.group(5)).strip()
id:str = self.__id(content)
return ('
' + self.analyse(matches.group(0).strip(), language, WMarkDown.SUBITEM, path) + '
' return None def module_bold_italic(self, matches:REMatch, language:Optional[int] = HTML, path:Optional[str] = None) -> str|None: if language & WMarkDown.HTML: return '' + self.analyse(matches.group(1), language, WMarkDown.SUBITEM, path) + '' return None def module_bold(self, matches:REMatch, language:Optional[int] = HTML, path:Optional[str] = None) -> str|None: if language & WMarkDown.HTML: return '' + self.analyse(matches.group(1), language, WMarkDown.SUBITEM, path) + '' return None def module_italic(self, matches:REMatch, language:Optional[int] = HTML, path:Optional[str] = None) -> str|None: if language & WMarkDown.HTML: return '' + self.analyse(matches.group(1), language, WMarkDown.SUBITEM, path) + '' return None def module_strike(self, matches:REMatch, language:Optional[int] = HTML, path:Optional[str] = None) -> str|None: if language & WMarkDown.HTML: return '' + self.analyse(matches.group(1), language, WMarkDown.SUBITEM, path) + ''
return None
@staticmethod
def __check_html_module(_type:str, matches:REMatch) -> str:
is_radio:bool = _type == "radio"
return ('')
@classmethod
def module_checkbox(self, matches:REMatch, language:Optional[int] = HTML, path:Optional[str] = None) -> str|None:
if language & WMarkDown.HTML:
return WMarkDown.__check_html_module("checkbox", matches)
return None
@classmethod
def module_radio(self, matches:REMatch, language:Optional[int] = HTML, path:Optional[str] = None) -> str|None:
if language & WMarkDown.HTML:
return WMarkDown.__check_html_module("radio", matches)
return None
@classmethod
def module_tick(self, matches:REMatch, language:Optional[int] = HTML, path:Optional[str] = None) -> str|None:
if language & WMarkDown.HTML:
return WMarkDown.__check_html_module("tick", matches)
return None
@staticmethod
def __list_deployed(level:list[int, str, str|None], last_mark:str|None, l:int) -> str:
_type:str = last_mark if l and last_mark else level[2]
deployable:bool = l and _type and _type in "+-"
deployed:str = (
"null" if not deployable else
"false" if _type == '-' else
"true")
return ' data-deployed="' + deployed + '"' + (' data-list-unprocessed="true"' if deployable else '')
@classmethod
def __list_start(self, unordered:bool, _type:str) -> str:
if not unordered:
_type = _type[:-1]
if WMarkDown.re_integer.search(_type):
return ' start="' + _type + '"'
return ''
def module_list(self, matches:REMatch, language:Optional[int] = HTML, path:Optional[str] = None) -> str|None:
if language & WMarkDown.HTML:
html:str = ''
lines:list[str] = WMarkDown.re_new_line.split(matches.group(0))
levels:list[list[int, str, str|None]] = [[0, 'u', None]]
l:int = 0
line:str
last_mark:str|None = None
subtype:str|None = None
for line in lines:
matches:REMatch = WMarkDown.re_list_line.search(line)
if not matches:
continue
separator_by_marks:int = len(matches.group(3) or "")
separator:int = separator_by_marks if separator_by_marks > 1 else len(matches.group(1))
_type:str = matches.group(2)
key:str = matches.group(4)
string:str = self.analyse(matches.group(5), language, WMarkDown.SUBITEM, path).strip()
unordered:bool = _type and _type[-1] in ('-', '*', '+')
subtype = _type[-1] if _type else None
if _type:
if levels[l][0] == separator:
levels[l][1] = 'u' if unordered else 'o'
levels[l][2] = subtype
html += ('' if html else '<' + str(levels[l][1]) + 'l class="wmd-list"' + WMarkDown.__list_start(unordered, _type) + WMarkDown.__list_deployed(levels[l - 1], last_mark, l) + (
' type="' + key[0] + '"' if key and not unordered and key[0] in ('a', 'A', 'i', 'I') else
'') + '>') + '' + (('') return None @classmethod def code_block_data(self, key:str, value:str|int, language:Optional[int] = HTML, path:Optional[str] = None) -> str|None: if language & WMarkDown.HTML: return ('' + (( '' + '' + _type[1:] + '' ) if _type[0] == "@" else ( '' + ('' + type_text + '' if type_text else '') )) + '') if _type else '') + '' + self.analyse(matches.group(5), language, WMarkDown.SUBITEM, path) + '' + '
' + WMarkDown.filter_html_special_characters(content) + '' + '
' +
'' +
('' + title + '' if title else '') +
'')
def module_media(self, matches:REMatch, language:Optional[int] = HTML, path:Optional[str] = None) -> str|None:
if language & WMarkDown.HTML:
_type:str = matches.group(1).lower()
links:list[str] = WMarkDown.re_white_spaces.split(matches.group(3) or matches.group(5) or matches.group(7))
text:str = (matches.group(10) or matches.group(12) or matches.group(14) or '').strip()
if _type in ("image", "picture", "icon"):
return WMarkDown.build_html_image(links, _type, text)
if _type in ("video",):
if "youtube.com" in links[0] or "youtu.be" in links[0]:
_type = "youtube"
elif "vimeo.com" in links[0]:
_type = "vimeo"
return ('' +
'' +
(
"YOUTUBE" if _type == "youtube" else
"VIMEO" if _type == "vimeo" else
'') +
'')
if _type in ("sound", "audio"):
if "soundcloud.com" in links[0]:
_type = "soundcloud"
return ('' +
'' +
(
"SOUNDCLOUD" if _type == "soundcloud" else
'') +
'')
return None
return None
@classmethod
def mark_replace(self, string:str, matches:REMatch, fragments:list[str]) -> str:
if matches:
start:int
end:int
i:int = len(fragments)
start, end = matches.span()
string = string[:start] + WMarkDown.item_mark[0] + str(len(fragments)) + WMarkDown.item_mark[1] + string[end:]
fragments += [matches.group(0)]
return string
@classmethod
def restore_marks(self, string:str, fragments:list[str]) -> str:
while True:
matches:REMatch = WMarkDown.item_mark[2].search(string)
if not matches:
break
i:int = int(matches.group(1))
_from:int
_to:int
_from, _to = matches.span()
string = string[:_from] + (fragments[i] if i >= 0 and i < len(fragments) else "") + string[_to:]
return string
@classmethod
def __doc_typed_format(self, typed:str) -> str:
return WMarkDown.filter_html_special_characters(typed.replace(' ', "").replace(',', ", "))
def module_code_doc(self, matches:REMatch, language:Optional[int] = HTML, path:Optional[str] = None) -> str|None:
if language & WMarkDown.HTML:
# print(matches.groups())
data:str = matches.group(1).strip()
base:REMatch = WMarkDown.re_code_doc.search(data)
return_type:str = WMarkDown.__doc_typed_format(base.group(2) or "void")
access:str = base.group(3) or "public"
name_space:str|None = base.group(5)
environment:str = base.group(6) or "global"
method:str = base.group(7)
parameters:str|None = base.group(9)
has_parameters:bool = parameters != None
default_value:str|None = base.group(11)
full_method:str = (name_space + "." if name_space else "") + method.strip()
arguments:str = ''
arguments_definition:str = ''
header:str = ('