WMarkDown/Python/WMarkDown.py

958 lines
42 KiB
Python

#!/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'(?<![a-z0-9_\-])(class=")([^"]+)(")')
re_white_spaces:REPattern = RECompile(r'[ \t]+')
re_line_marks:REPattern = RECompile(r'[\*#~\-_]+|\[[ \-x]\]|\([ \-x]\)', RE_IGNORE_CASE)
re_integer:REPattern = RECompile(r'^[0-9]+$')
re_code_doc:REPattern = RECompile(r'^(\[([^\]]+)\][ \t]+)?([\!\?])?(([^\(\[\]\?\!]+)([\:\.]))?([^\(\.\:\[\]\=]+)(\(((.+|[\r\n]+)*)\)|[ \t]*=[ \t]*((.+|[\r\n]+)*))?')
re_code_doc_arguments:REPattern = RECompile(r'([\!\?]{0,2})([^=]+)[ \t]+([^=]+)(=(.+))?')
re_code_doc_subarguments: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'(?<!\\)\*{3}((?:\\\*|\*{1,2}[^\*]|[^*\r\n]|[\r\n])*)\*{3}')),
# "bold" : (WMarkDown.SUBITEM | WMarkDown.LINKED, RECompile(r'\*{2}(([^\*]+|\*[^\*]|[\r\n]+|\\\*)*)\*{2}')),
"bold" : (WMarkDown.SUBITEM | WMarkDown.LINKED, RECompile(r'(?<!\\)\*{2}((?:\\\*|\*[^\*]|[^*\r\n]|[\r\n]|(?!\*{2}[^\*])\*)*)\*{2}')),
# "italic" : (WMarkDown.SUBITEM | WMarkDown.LINKED, RECompile(r'(?<!\\)\*(([^\*]+|[\r\n]+|\\\*)*)\*')),
"italic" : (WMarkDown.SUBITEM | WMarkDown.LINKED, RECompile(r'(?<!\\)\*((?:\\\*|[^*\r\n]|[\r\n]|\*{2})*)\*')),
"underline" : (WMarkDown.SUBITEM | WMarkDown.LINKED, RECompile(r'__(((?!__).)+)__')),
"strike" : (WMarkDown.SUBITEM | WMarkDown.LINKED, RECompile(r'~~(((?!~~).)+)~~')),
"code_item" : (WMarkDown.SUBITEM | WMarkDown.LINKED, RECompile(r'``(((?!``).)+)``')),
"checkbox" : (WMarkDown.SUBITEM | WMarkDown.LINKED, RECompile(r'\[((\-)|(x)|( ))\]', RE_IGNORE_CASE)),
"radio" : (WMarkDown.SUBITEM | WMarkDown.LINKED, RECompile(r'\(((\-)|(x)|( ))\)', RE_IGNORE_CASE)),
"tick" : (WMarkDown.SUBITEM | WMarkDown.LINKED, RECompile(r'\{((w)|(v)|(x))\}', RE_IGNORE_CASE)),
"color_sample" : (WMarkDown.SUBITEM | WMarkDown.LINKED, RECompile(r'\[{2}((\#[a-f0-9]{3,8})|color[ \t]+([^\[\]\t\r\n]+))\]{2}', RE_IGNORE_CASE)),
"exclude" : (WMarkDown.SUBITEM, RECompile(r'\[{2}\!(([^\]]+|\][^\]]|[\r\n])*)\]{2}')),
"link" : (WMarkDown.SUBITEM, RECompile(r'(\[([^\[\]]+)\])?\((([^\(\) \t]+)[ \t]+("(([^\\\\"]+|\\\\.|[\r\n]+)*)"|\'(([^\\\\\']+|\\\\.|[\r\n]+)*)\'|([^\(\)]+)?))[ \t]*\)|\[(\+?[0-9 ]{5,22}|[^ \]]+)([\t ]+([^\]]*))?\]|([a-z0-9]{3,8}\:\/{2}[^ \(\[\)\]>]+|[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 ('<h' + level + ' class="wmd-title" id="' + id + '" title="' + content + '">' +
'<span>' + self.analyse(content, language, WMarkDown.SUBITEM, path) + '</span>' +
'<a href="#' + id + '" target="_blank">' +
'<span data-icon="link"></span>' +
'</a>' +
'</h' + level + '>')
return None
def module_paragraph(self, matches:REMatch, language:Optional[int] = HTML, path:Optional[str] = None) -> str|None:
if language & WMarkDown.HTML:
return '<p>' + self.analyse(matches.group(0).strip(), language, WMarkDown.SUBITEM, path) + '</p>'
return None
def module_bold_italic(self, matches:REMatch, language:Optional[int] = HTML, path:Optional[str] = None) -> str|None:
if language & WMarkDown.HTML:
return '<b><i>' + self.analyse(matches.group(1), language, WMarkDown.SUBITEM, path) + '</i></b>'
return None
def module_bold(self, matches:REMatch, language:Optional[int] = HTML, path:Optional[str] = None) -> str|None:
if language & WMarkDown.HTML:
return '<b>' + self.analyse(matches.group(1), language, WMarkDown.SUBITEM, path) + '</b>'
return None
def module_italic(self, matches:REMatch, language:Optional[int] = HTML, path:Optional[str] = None) -> str|None:
if language & WMarkDown.HTML:
return '<i>' + self.analyse(matches.group(1), language, WMarkDown.SUBITEM, path) + '</i>'
return None
def module_strike(self, matches:REMatch, language:Optional[int] = HTML, path:Optional[str] = None) -> str|None:
if language & WMarkDown.HTML:
return '<del>' + self.analyse(matches.group(1), language, WMarkDown.SUBITEM, path) + '</del>'
return None
def module_underline(self, matches:REMatch, language:Optional[int] = HTML, path:Optional[str] = None) -> str|None:
if language & WMarkDown.HTML:
return '<u>' + self.analyse(matches.group(1), language, WMarkDown.SUBITEM, path) + '</u>'
return None
def module_code_item(self, matches:REMatch, language:Optional[int] = HTML, path:Optional[str] = None) -> str|None:
if language & WMarkDown.HTML:
return '<code>' + self.analyse(matches.group(1), language, WMarkDown.SUBITEM, path) + '</code>'
return None
@staticmethod
def __check_html_module(_type:str, matches:REMatch) -> str:
is_radio:bool = _type == "radio"
return ('<label class="wmd-icon ' + ("radio-button" if is_radio else _type) + '">' +
'<input type="' + ("checkbox" if _type == "tick" else _type) + '" readonly onclick="return false;"' + (
' disabled' if matches.group(2) else
' checked' if matches.group(3) else
'') + ' />' +
'<span data-icon="' + ("radio_button" if is_radio else _type) + '"></span>' +
'</label>')
@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 += ('</li>' 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
'') + '>') + '<li>' + string
elif levels[l][0] < separator:
l += 1
levels += [[separator, "u" if unordered else "o", subtype]]
html += '<' + 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
'') + '><li>' + string
else:
while levels[l][0] > separator:
html += '</li></' + levels.pop(l)[1] + 'l>'
l -= 1
if not l:
break
html += '</li><li>' + string
else:
html += ' ' + string
last_mark = subtype
while len(levels):
html += '</li></' + levels.pop(len(levels) - 1)[1] + 'l>'
return html
return None
@classmethod
def set_class(self, html:str, _class:str|list|tuple) -> str:
if not isinstance(_class, (list, tuple)):
_class = (_class,)
if 'class="' not in html:
html = (' ' if html else '') + 'class="' + " ".join(_class) + '"'
def callback(matches:REMatch):
attribute:str
classes_str:str
closer:str
classes:list[str]
class_key:str
attribute, classes_str, closer = matches.groups()
classes = WMarkDown.re_white_spaces.split(classes_str)
return attribute + classes_str + (' ' if classes_str else '') + " ".join([
class_key for class_key in _class if class_key not in classes
]) + closer
return WMarkDown.re_class_attribute.sub(callback, html)
def module_table(self, matches:REMatch, language:Optional[int] = HTML, path:Optional[str] = None) -> str|None:
if language & WMarkDown.HTML:
html:dict[str, str] = {
"thead" : '',
"tbody" : '',
"tfoot" : ''
}
attributes:str = (matches.group(1) or "").strip()
data:str = matches.group(2).strip()
while True:
line_matches:REMatch = WMarkDown.re_table_line.search(data)
if not line_matches:
break
line:str = line_matches.group(0)
cell_type:str = 'h' if line[1] in ('^', '_', '=') else 'd'
tags:tuple[str] = (
("thead",) if line[1] == '^' else
("tfoot",) if line[1] == '_' else
("thead", "tfoot") if line[1] == '=' else
("tbody",))
tag:str
row:str = '<tr>'
if cell_type == 'h':
line = line[0] + line[2:]
while True:
cell_matches:REMatch = WMarkDown.re_table_item.search(line)
if not cell_matches:
break
column_span_i:int = len(cell_matches.group(1))
column_span:str = ' colspan="' + str(column_span_i) + '"' if column_span_i > 1 else ''
content:str = (cell_matches.group(2) or "").strip()
row += '<t' + cell_type + column_span + '>' + self.analyse(
content[1:-1] if content and content[0] + content[-1] in ("''", '""') else content,
WMarkDown.HTML,
WMarkDown.SUBITEM,
path
) + '</t' + cell_type + '>'
line = line[cell_matches.span()[1]:]
row += '</tr>'
for i, tag in enumerate(tags):
if i:
html[tag] = row + html[tag]
else:
html[tag] += row
data = data[line_matches.span()[1]:]
return ('<div class="wmd-table">' +
'<table ' + attributes + '>' + "".join([
'<' + tag + '>' + content + '</' + tag + '>' if content else '' for tag, content in html.items()
]) + '</table>' +
'</div>')
return None
@classmethod
def module_exclude(self, matches:REMatch, language:Optional[int] = HTML, path:Optional[str] = None) -> str|None:
if language & WMarkDown.HTML:
return '<span class="wmd-excluded">' + (matches.group(1) or "") + '</span>'
return None
def module_link(self, matches:REMatch, language:Optional[int] = HTML, path:Optional[str] = None) -> str|None:
if language & WMarkDown.HTML:
raw_url:str = (matches.group(4) or matches.group(11) or matches.group(14) or "").strip()
url = (
"tel:" + raw_url.replace(" ", "") if WMarkDown.re_phone_number.search(raw_url) else
"mailto:" + raw_url if WMarkDown.re_email_address.search(raw_url) else
raw_url)
text:str = matches.group(2) or matches.group(13) or raw_url
protocol_matches:REMatch|None = WMarkDown.re_protocol.search(url)
return '<a href="' + url + '" target="_' + ("blank" if "://" in url else "self") + '" title="' + WMarkDown.re_line_marks.sub(r'', (
matches.group(6) or matches.group(8) or matches.group(10) or text
).strip()) + '" data-protocol="' + (
(protocol_matches.group(1) if protocol_matches else "http").lower()
) + '">' + self.analyse(text.strip(), language, WMarkDown.SUBITEM | self.LINKED, path) + '</a>'
return None
def module_quote(self, matches:REMatch, language:Optional[int] = HTML, path:Optional[str] = None) -> str|None:
if language & WMarkDown.HTML:
_type:str|None = matches.group(2)
type_text:str|None = matches.group(4)
if _type in WMarkDown.quote_special:
_type = WMarkDown.quote_special[_type]
return ('<blockquote class="wmd-quote"' + (' data-quote-type="' + _type + '"' if _type else '') + '>' +
(('<div class="quote-type">' + ((
'<span data-avatar="' + _type[1:].lower() + '"></span>' +
'<span class="user">' + _type[1:] + '</span>'
) if _type[0] == "@" else (
'<span data-icon="' + _type + '"></span>' +
('<span class="quote-type-name">' + type_text + '</span>' if type_text else '')
)) + '</div>') if _type else '') +
'<div class="text">' + self.analyse(matches.group(5), language, WMarkDown.SUBITEM, path) + '</div>' +
'</blockquote>')
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 ('<li data-i18n="' + key + '" data-i18n-without="true" title="' + key + '">' +
'<span data-icon="' + key + '"></span>' +
'<span data-i18n="' + key + '">' + key + '</span>' +
'<span class="value">' + str(value) + '</span>' +
'</li>')
return None
@classmethod
def filter_html_special_characters(self, string:str) -> str:
l:int = len(WMarkDown.html_special_characters)
_:int
index:list[int] = [-1 for _ in range(l)]
response:str = ''
characters:tuple[str] = tuple(WMarkDown.html_special_characters.keys())
while True:
j:int
character:str
i:int|None = None
for j, character in enumerate(characters):
if index[j] == None:
continue
if index[j] < 0:
try:
index[j] = string.index(character)
except:
index[j] = None
continue
if i == None or index[i] > index[j]:
i = j
if i == None:
response += string
break
length:int = index[i]
response += string[:length] + '&' + WMarkDown.html_special_characters[characters[i]] + ';'
string = string[length + 1:]
for j in range(l):
if index[j] != None:
index[j] -= length + 1
return response
@classmethod
def module_code_block(self, matches:REMatch, language:Optional[int] = HTML, path:Optional[str] = None) -> str|None:
if language & WMarkDown.HTML:
_type:str
content:str
i:int
for i in range(6):
j:int = 2 + i * 4
_type = matches.group(j)
content = matches.group(j + 1)
if _type or content:
_type = (_type or "unamed").strip().lower()
content = (content or "")
break
if _type:
lines:list[str] = WMarkDown.re_new_line.split(content)[2:]
_:str
return ('<div class="wmd-code-block" data-type="' + _type + '" data-processed="false">'
'<ul class="data">' +
'<li><span data-icon="' + _type + '"></span></li>' +
WMarkDown.code_block_data("type", _type, language) +
WMarkDown.code_block_data("characters", len(content), language) +
WMarkDown.code_block_data("lines", len(lines), language) +
'</ul>' +
'<div class="code">' +
'<ol class="lines">' + ''.join(['<li></li>' for _ in lines]) + '</ol>' +
'<pre class="content">' + WMarkDown.filter_html_special_characters(content) + '</pre>' +
'</div>' +
'</div>')
return 'UNKNOWN_BLOCK'
return None
def module_special_characters(self, matches:REMatch, language:Optional[int] = HTML, path:Optional[str] = None) -> str|None:
if language & WMarkDown.HTML:
return matches.group(1)
return None
@staticmethod
def load_file(path:str) -> str|None:
if path_exists(path):
try:
with open(path, "r") as opened:
return opened.read()
except:
pass
return None
def module_include(self, matches:REMatch, language:Optional[int] = HTML, path:Optional[str] = None) -> str|None:
if language & WMarkDown.HTML:
relative_path:str = matches.group(1)
new_path:str = path + relative_path
extension:str = WMarkDown.re_extension.search(new_path).groups()[1:]
content:str|None = WMarkDown.load_file(new_path)
directory:str = directory_name(new_path) + "/"
results:str|None = (
None if content == None else
self.process(content, language, directory)[0] if ".".join(extension) == "w.md" else
self.analyse(content, language, WMarkDown.RAW, directory) if extension[-1] == "md" else
content)
return ('<section data-path="' + relative_path + '" data-include="' + ("false" if results == None else "true") + '">' +
'<div class="link"><a href="' + relative_path + '" target="_self" title="' + relative_path + '">' + relative_path + '</a></div>' +
(results or "") +
'</section>')
return None
@staticmethod
def build_html_image(links:list[str]|tuple[str], _type:Optional[str] = "image", title:Optional[str] = None) -> str:
attributes:str = ' title="' + title + '" alt="' + title + '"' if title else ''
return ('<span class="wmd-media wmd-image" data-type="' + _type + '" data-status="unprocessed">' +
'<noscript>' +
''.join(['<img src="' + link + '"' + attributes + ' />' for link in links]) +
'</noscript>' +
'<img src="/images/WMarkDown.png"' + attributes + ' data-sources="' + base64_encode(json_encode(links).encode("utf-8")).decode("utf-8") + '" data-i="0" />' +
'<span class="image"></span>' +
('<span class="text">' + title + '</span>' if title else '') +
'</span>')
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 ('<span class="wmd-media wmd-video" data-type="' + _type + '" data-status="unprocessed">' +
'<noscript></noscript>' +
(
"<b>YOUTUBE</b>" if _type == "youtube" else
"<b>VIMEO</b>" if _type == "vimeo" else
'<video src="' + links[0] + '"' + (' poster="' + links[1] + '"' if len(links) > 1 else '') + '></video>') +
'</span>')
if _type in ("sound", "audio"):
if "soundcloud.com" in links[0]:
_type = "soundcloud"
return ('<span class="wmd-media wmd-video" data-type="' + _type + '" data-status="unprocessed">' +
'<noscript></noscript>' +
(
"<b>SOUNDCLOUD</b>" if _type == "soundcloud" else
'<audio src="' + links[0] + '" controls></audio>') +
'</span>')
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 = ('<tr>' +
'<th data-i18n="name" title="Name">Name</th>' +
'<th data-i18n="required" title="Required">Required</th>' +
'<th data-i18n="nullable" title="Nullable">Nullable</th>' +
'<th data-i18n="typed" title="Typed">Typed</th>' +
'<th data-i18n="default_value" title="Default Value">Default Value</th>' +
'</tr>')
fragments:list[str] = []
matches:REMatch
arguments_l:int = 0
if access == "?":
access = "protected"
elif access == "!":
access = "private"
if environment == ":":
environment = "static"
elif environment == ".":
environment = "object"
if parameters:
while True:
matches = WMarkDown.re_code_doc_subarguments.search(parameters)
if matches:
parameters = WMarkDown.mark_replace(parameters, matches, fragments)
else:
break
else:
parameters = ""
for parameter in parameters.split(","):
matches = WMarkDown.re_code_doc_arguments.search(parameter.strip())
if matches:
scopes:str = matches.group(1) or ""
required:bool = "!" in scopes
nullish:bool = "?" in scopes
typed:str = WMarkDown.__doc_typed_format(WMarkDown.restore_marks(matches.group(2), fragments))
name:str = matches.group(3).strip()
default_value:str = WMarkDown.filter_html_special_characters(WMarkDown.restore_marks((matches.group(5) or "").strip(), fragments))
arguments += ('<span class="argument">' +
('<span class="required" data-i18n="required" title="Required">Required</span>' if required else '') +
('<span class="nullish" data-i18n="nullish" title="Nullish">Nullish</span>' if nullish else '') +
'<span class="typed" data-i18n="typed" data-i18n-without="true" title="Typed">' + typed + '</span>' +
'<span class="name" data-i18n="name" data-i18n-without="true" title="Name">' + name + '</span>' +
('<span class="default_value" data-i18n="default_value" title="Default value">' + default_value + '</span>' if default_value else '') +
'</span>')
arguments_definition += ('<tr>' +
'<td data-i18n="name" data-i18n-without="true" title="Name">' + name + '</td>' +
'<td data-i18n="required" data-i18n-without="true" title="Required">' + ('True' if required else 'False') + '</td>' +
'<td data-i18n="nullish" data-i18n-without="true" title="Nullish">' + ('True' if nullish else 'False') + '</td>' +
'<td data-i18n="typed" data-i18n-without="true" title="Typed">' + typed + '</td>' +
'<td data-i18n="default_value" data-i18n-without="true" title="Default value">' + default_value + '</td>' +
'</tr>')
arguments_l += 1
return ('<fieldset class="wmd-code-doc" data-arguments-l="' + str(arguments_l) + '">' +
'<legend>' + full_method + '</legend>' +
'<div class="description">' +
'<span class="return-type" data-i18n="return" data-i18n-without="true" title="Return">' + return_type + '</span>' +
'<span class="environment" data-i18n="environment" data-i18n-without="true" title="Environment">' + environment + '</span>' +
'<span class="access" data-i18n="access" data-i18n-without="true" title="Access">' + access + '</span>' +
'<span class="full-method" data-i18n="method" data-i18n-without="true" title="Method">' + full_method + '</span>' +
('<span class="arguments" data-i18n="arguments" data-i18n-without="true" title="Arguments">' + arguments + '</span>' if has_parameters else '') +
('<span class="default-value" data-i18n="default_value" data-i18n-without="true" title="Default value">' + default_value + '</span>' if default_value else '') +
'</div>' +
'<div class="wmd-table definition">' +
'<table>' +
'<thead>' + header + '</thead>' +
'<tbody>' + arguments_definition + '</tbody>' +
'<tfoot>' + header + '</tfoot>' +
'</table>' +
'</div>' +
'</fieldset>')
return None
def module_color_sample(self, matches:REMatch, language:Optional[int] = HTML, path:Optional[str] = None) -> str|None:
if language & WMarkDown.HTML:
color:str = matches.group(2) or matches.group(3)
return ('<span class="wmd-color-sample" style="background-color:' + color + '" title="' + color + '">' +
'<span data-sample="' + color + '"></span>' +
'<span class="color">' + color + '</span>' +
'</span>')
return None
def module_presentation_links(self, matches:REMatch, language:Optional[int] = HTML, path:Optional[str] = None) -> str|None:
if language & WMarkDown.HTML:
items:list = []
i:int = -1
title:str
avatars:list[str]
links:list[str]
html_avatars:str = ''
html_list:str = ''
for line in WMarkDown.re_new_lines.split(matches.group(1)):
has_spaces:bool = WMarkDown.re_start_with_white_spaces.search(line) != None
data:str = line.strip()
if not data:
continue
if has_spaces:
if i == -1:
continue
if data[0] == "*":
items[i][1] += [data[1:]]
if ".k3y.pw/" in data:
items[i][1] += [data[1:].replace(".k3y.pw/", ".local/")]
else:
items[i][2] += [data]
else:
i += 1
items += [[line.strip(), [], []]]
for i, (title, avatars, links) in enumerate(items):
link:str
avatar:str
html_avatars += ('<li title="' + title + '" data-i="' + str(i) + '" data-links="' + str(len(links)) + '">' +
('<a href="' + links[0] + '" target="_blank">' +
WMarkDown.build_html_image(avatars) +
'</a>' if len(links) else WMarkDown.build_html_image(avatars)) +
''.join(['<a href="' + link + '" target="_blank" class="wmd-favicon" style="background-image:' + (
'url(\'' + WMarkDown.re_domain.search(link).group(0) + '/favicon.ico\')' +
(',url(\'' + WMarkDown.re_domain.search(link.replace(".k3y.pw/", ".local/")).group(0) + '/favicon.ico\')' if ".k3y.pw/" in link else '')
) + ';"></a>' for link in links]) +
'</li>')
html_list += ('<li class="name">' +
'<span>' + title + '</span>' +
'<ul class="links">' +
''.join(['<li class="link"><a href="' + link + '" target="_blank" title="' + link + '">' + link + '</a></li>' for link in links]) +
'</ul>' +
'</li>')
return ('<div class="wmd-presentation-links">' +
'<ul class="wmd-avatars">' + html_avatars + '</ul>' +
'<ul class="wmd-list">' + html_list + '</ul>' +
'</div>')
return None
@classmethod
def remove_default_tabulations(self, data:str) -> str:
line:str
lines:list[str] = WMarkDown.re_new_lines.split(data)
spaces:int = len(data)
def callback(matches:REMatch):
return matches.group(0)[spaces:]
for line in lines:
if not line:
continue
white_spaces:int = len(WMarkDown.re_started_white_spaces.search(line).group(0))
if white_spaces < spaces:
spaces = white_spaces
return WMarkDown.re_lines.sub(callback, data)
def __build(self, data:str, language:int, path:str|None) -> str:
return ('<div class="wmd wmarkdown" data-dictionary-processed="false" data-menu-processed="false">' +
self.analyse(WMarkDown.remove_default_tabulations(data), language, WMarkDown.RAW, path) +
'<div class="wmd-process-and-loaded"></div>' +
'</div>')
def process(self, data:str, language:Optional[int] = HTML, path:Optional[str] = None) -> str:
results:str = ""
variables:dict[str, str] = {}
options:REMatch = WMarkDown.re_options.search(data)
if options:
_from:int
_to:int
options_data:str = options.group(3)
_from, _to = options.span()
while True:
option:REMatch = WMarkDown.re_option.search(options_data)
if not option:
break
variables[option.group(1).strip()] = option.group(2).strip()
options_data = options_data[option.span()[1]:]
data = data[:_from] + data[_to:]
while True:
matches:REMatch = WMarkDown.re_block_mark.search(data)
if not matches:
results += data
break
open_from:int
open_to:int
mark:str = matches.group(1)
re_close:REPattern = RECompile(r'^\s*' + mark, RE_MULTILINE) if mark else WMarkDown.re_block_html
open_from, open_to = matches.span()
results += data[:open_from]
data = data[open_to:]
close_matches:REMatch = re_close.search(data)
if close_matches:
close_from:int
close_to:int
close_from, close_to = close_matches.span()
results += self.__build(data[:close_from], language, path)
data = data[close_to:]
else:
results += self.__build(data, language, path)
break
return results, variables
def analyse(self, data:str, language:Optional[int] = HTML, mode:Optional[int] = RAW, path:Optional[str] = None) -> str:
response:str = ""
item_mode:int
items:dict[str, list[bool, int, int, REMatch]] = {key : [True, -1, -1, None] for key, (item_mode, pattern) in self.modules.items() if mode == WMarkDown.RAW or mode | item_mode == item_mode}
# j:int = 0
# m:int = 10
while len(items):
key:str
exists:bool
_from:int
_to:int
matches:REMatch
l:int = len(data)
selected:str|None = None
for key, (exists, _from, _to, matches) in items.items():
if not exists:
continue
pattern:REPattern = self.modules[key][1]
if _from < 0:
items[key][3] = matches = pattern.search(data)
if matches:
_from, _to = items[key][1], items[key][2] = matches.span()
else:
items[key][0] = False
continue
if _from < l:
l = _from
selected = key
if selected == None:
response += data
break
_from, _to, matches = items[selected][1:]
response += data[:_from] + getattr(self, "module_" + selected)(matches, language, path)
data = data[_to:]
for key in items.keys():
if items[key][3]:
items[key][1] -= _to
items[key][2] -= _to
# j += 1
# if j >= m:
# break
return response
def to_html(self, data:str, path:Optional[str] = None) -> str:
return self.analyse(data, WMarkDown.HTML, WMarkDown.RAW, path)