kivyblocks/kivyblocks/markdown.py
2022-10-14 13:50:29 +08:00

442 lines
10 KiB
Python

from kivy.properties import StringProperty, NumericProperty, ListProperty
from kivy.factory import Factory
from kivy.clock import Clock
from kivy.logger import Logger
from kivy.uix.scrollview import ScrollView
from kivy.uix.label import Label
from kivy.uix.image import AsyncImage
from .baseWidget import ScrollWidget, getDataHandler, VBox
from .utils import CSize
import re
SOURCE_DELIMITER = "\n```\n"
VIDEO_LEAD = "![@"
AUDIO_LEAD = "![#"
IMAGE_LEAD = "!["
LINK_LEAD = "["
REFER_SPLIT = "]("
REFER_END = ")"
class MarkdownParser(object):
mdkeys = [
SOURCE_DELIMITER,
VIDEO_LEAD,
AUDIO_LEAD,
IMAGE_LEAD
]
def __init__(self, mdtext):
self.mdtext = mdtext
self.result = []
self.text = ""
self.mdkey_handler = {
SOURCE_DELIMITER:self.handle_source_delimiter,
VIDEO_LEAD:self.lead_video,
AUDIO_LEAD:self.lead_audio,
IMAGE_LEAD:self.lead_image,
}
def handle_source_delimiter(self):
x = self.mdtext.split(SOURCE_DELIMITER,1)
mk_p = MarkdownParser(x[0])
d = {
'source':x[0]
}
self.result.append(d)
if len(x) > 1:
self.mdtext = x[1]
def lead_video(self):
print('lead_video() ..., mdtext=', self.mdtext)
x = self.mdtext.split(REFER_SPLIT,1)
if len(x)<2:
print('lead_video() return here', REFER_SPLIT, '---', self.mdtext)
self.text = f'{VIDEO_LEAD}'
return
title = x[0]
y = x[1].split(REFER_END,1)
if len(y) < 2:
print('lead_video() return here', REFER_END)
self.text = f'{VIDEO_LEAD}'
return
url = y[0]
self.mdtext = y[1]
d = {
"video":{
"title":title,
"url":url
}
}
self.result.append(d)
def lead_audio(self):
x = self.mdtext.split(REFER_SPLIT,1)
if len(x)<2:
self.text = f'{AUDIO_LEAD}'
return
title = x[0]
y = x[1].split(REFER_END,1)
if len(y) < 2:
self.text = f'{AUDIO_LEAD}'
return
url = y[0]
d = {
"audio":{
"title":"title",
"url":url
}
}
self.result.append(d)
l = len(title) + 2 + len(url)
self.mdtext = self.mdtext[l:]
def lead_image(self):
x = self.mdtext.split(REFER_SPLIT,1)
if len(x)<2:
self.text = f'{IMAGE_LEAD}'
return
title = x[0]
y = x[1].split(REFER_END,1)
if len(y) < 2:
self.text = f'{IMAGE_LEAD}'
return
url = y[0]
d = {
"image":{
"title":"title",
"url":url
}
}
self.result.append(d)
self.mdtext = y[1]
def check_key(self,t):
for k in self.mdkeys:
if t.startswith(k):
return k
return None
def parse(self):
"""
parser parse mdtext, recognize bbtext, source, img, audio, video
part text
"""
while len(self.mdtext) > 0:
k = self.check_key(self.mdtext)
if k is None:
self.text = f'{self.text}{self.mdtext[0]}'
self.mdtext = self.mdtext[1:]
else:
if len(self.text) > 0:
self.result.append({'pure_md':self.text})
self.text = ""
self.mdtext = self.mdtext[len(k):]
self.mdkey_handler[k]()
if len(self.text) > 0:
self.result.append({'pure_md':self.text})
self.text = ''
return self.result
class MDImage(AsyncImage):
parent_width=NumericProperty(None)
image_size = ListProperty(None)
def __init__(self, **kw):
super().__init__(**kw)
self.image_size = None
self.parent_width = None
self.bind(texture=self.image_loaded)
def image_loaded(self,o, *args):
self.image_size = self.texture.size
def resize(self, *args):
ps = [0,0,0,0]
if hasattr(self.parent, 'padding'):
ps = self.parent.padding
self.parent_width = self.parent.width - ps[0] - ps[2]
if self.texture:
if self.texture.size:
self.image_size = self.texture.size
Logger.info('MDImage:resize called, %d', self.parent_width)
def on_image_size(self, *args):
if self.parent_width:
Logger.info('MDImage:on_iage_size %d, %s', self.parent_width,
self.image_size)
self.set_image_size()
def on_parent_width(self, *args):
if self.image_size:
Logger.info('MDImage:on_oarent_width %d, %s', self.parent_width,
self.image_size)
self.set_image_size()
def set_image_size(self):
self.width = self.parent_width
self.height = self.parent_width * self.image_size[1] \
/ self.image_size[0]
Logger.info('MDImage:set_image_size %d, %d', self.width,
self.height)
class MarkdownBody(VBox):
def __init__(self, md_obj=None, padding=[10,10,10,10], **kw):
self.part_handlers = {
"pure_md":self.build_pure_md,
"source":self.build_source,
"image":self.build_image,
"video":self.build_video,
"audio":self.build_audio
}
self.md_obj = md_obj
super().__init__(**kw)
self.padding=padding
self.size_hint = 1,None
self.bind(parent=self.resize)
self.resize()
def show_mdtext(self, mdtext):
mdp = MarkdownParser(mdtext)
parts = mdp.parse()
self.clear_widgets()
for p in parts:
for k,v in p.items():
f = self.part_handlers.get(k)
f(v)
def resize(self, *args):
return
Logger.info('MDBody:resize called')
if self.parent:
ps = [0,0,0,0]
if hasattr(self.parent, 'padding'):
ps = self.parent.padding
h = 0
for c in self.children:
if hasattr(c, 'resize'):
c.resize()
h += c.height
self.width = self.parent.width - ps[0] - ps[2]
self.height = h
else:
Logger.info('resize:parent is null')
def build_source(self,source_desc):
w = MarkdownBody(md_obj=self.md_obj,
csscls=self.md_obj.second_css, size_hint_y=None)
w.show_mdtext(source_desc)
self.add_widget(w)
w.resize()
def build_pure_md(self, mdtext):
for l in mdtext.split('\n'):
self.parse_line(l)
def build_image(self,img_desc):
w = MDImage(source=img_desc['url'],
allow_stretch=True,
size_hint_y=None,
height=CSize(10),
keep_ratio=True
)
self.add_widget(w)
w.resize()
def build_video(self, video_desc):
w = Factory.NewVideo(source=video_desc['url'],
keep_ratio=True,
play=False,
allow_stretch = True,
size_hint_y=None
)
w.height=self.width * 10 / 16
w.state = 'pause'
def f1(x):
x.state = 'play'
def f2(x):
x.state = 'stop'
w.bind(on_enter_focus=f1)
w.bind(on_leave_focus=f2)
self.add_widget(w)
w.resize()
def build_audio(self, audio_desc):
w = Factory.APlayer(source=audio_desc.url)
w.bind(minimum_height=w.setter('height'))
self.add_widget(w)
def parse_title(self, txt, level):
w = Factory.Blocks().widgetBuild({
"widgettype":f"Title{level}",
"options":{
"text":txt,
"size_hint_x":None,
"width":self.width,
"size_hint_y":None,
"markup":True,
"wrap":True,
"halign":"left",
"valign":"middle"
}
})
if not w:
return
w1,h1 = w.get_wraped_size()
clen = CSize(1)
if h1 is None or h1 < clen:
h1 = clen
w.height = h1
w.bind(on_ref_press=self.md_obj.open_new_md)
self.add_widget(w)
def parse_line(self, l):
if l.startswith('###### '):
t = self.mktext_bbtext(l[7:])
return self.parse_title(t,6)
if l.startswith('##### '):
t = self.mktext2bbtext(l[6:])
return self.parse_title(t,5)
if l.startswith('#### '):
t = self.mktext2bbtext(l[5:])
return self.parse_title(t,4)
if l.startswith('### '):
t = self.mktext2bbtext(l[4:])
return self.parse_title(t,3)
if l.startswith('## '):
t = self.mktext2bbtext(l[3:])
return self.parse_title(t,2)
if l.startswith('# '):
t = self.mktext2bbtext(l[2:])
return self.parse_title(t,1)
t = self.mktext2bbtext(l)
print('markdown.py, t=', t, 'len(t)=', len(t), 'type(t)=', type(t))
w = Factory.Blocks().widgetBuild({
"widgettype":"Text",
"options":{
"text":t,
"wrap":True,
"size_hint_x":None,
"width":self.width,
"markup":True,
"valign":"middle",
"halign":"left"
}
})
if not w:
return
_,h = w.get_wraped_size()
clen = CSize(1)
if h is None or h < clen:
h = clen
w.height = h
w.bind(on_ref_press=self.md_obj.open_new_md)
self.add_widget(w)
def mktext2bbtext(self,mdtext):
"""
convert markdown text to bbtag text kivy Label Widget recognized
markdown syntax
*XXX* Italic
**XXX** Bold
***XXX*** Bold + Italic
> XXXX reference
>> XXXX reference inside reference
[-*] XXX list
[0-9]*[.] XXX list
*** split line
--- split line
___ split line
[.*](url) link
![.*](url) image
[![.*](url)](url) image link
!v[.*](url) video* plan to extend
!a[.*](url) audio* plan to extend
bb tag syntax
[b]XXX[/b] Bold
[i]XXX[/i] Italic
[sub]XXX[/sub]
"""
mdtext = re.sub('\*\*\*(.*)\*\*\*', \
lambda x: '[b][i]'+x.group(1)+'[/i][/b]', \
mdtext)
mdtext = re.sub('\*\*(.*)\*\*', \
lambda x: '[b]'+x.group(1)+'[/b]', \
mdtext)
mdtext = re.sub('\*(.*)\*', \
lambda x: '[i]'+x.group(1)+'[/i]', \
mdtext)
mdtext = re.sub('([^!]?)\[(.*)\]\((.*)\)', \
lambda x:x.group(1) + '[ref='+x.group(3)+'][color=00ff00]'+ x.group(2)+'[/color][/ref]', \
mdtext)
return mdtext
class Markdown(ScrollView):
"""
# Markdown
MArkdown widget using to render a markdown file and show it
description file format
{
"widgettype":"Markdown",
"options":{
"source": the markdown file
"first_css":,
"second_css":
other options
}
}
"""
source = StringProperty(None)
first_css = StringProperty("default")
second_css = StringProperty("default")
def __init__(self, **kw):
super(Markdown, self).__init__(**kw)
self.root_body = MarkdownBody(md_obj=self,
csscls=self.first_css,
size_hint_y=None
)
self.root_body.bind(minimum_height=self.root_body.setter('height'))
self.add_widget(self.root_body)
self.source = kw.get('source')
if self.source:
self.load_text()
self.bind(size=self.root_body.resize)
self.bind(source=self.load_text)
def check_parent_window(self, *args):
pw = self.get_parent_window()
rw = self.get_root_window()
print('MD: pw=%s,rw=%s', pw,rw)
def stop_media(self, *args):
self.root_body.stp_video()
def load_text(self, *args):
h = getDataHandler(self.source)
h.bind(on_success=self.update)
h.bind(on_error=self.show_error)
h.handle()
def show_error(self, o, e):
print('load_text(), error=',e)
def update(self, o, text):
print('text=',text, type(text))
if not text:
return
text = ''.join(text.split('\r'))
self.root_body.show_mdtext(text)
self.scroll_y = 1
def open_new_md(self, o, value):
print(value,'is it a link')
self.source = value