forked from openlp/openlp
updated renderer comments/docs
This commit is contained in:
parent
daed497a9e
commit
0086d102bd
@ -51,22 +51,14 @@ FOOTER = [u'Arky Arky (Unknown)', u'Public Domain', u'CCLI 123456']
|
|||||||
|
|
||||||
class Renderer(object):
|
class Renderer(object):
|
||||||
"""
|
"""
|
||||||
Class to pull all Renderer interactions into one place. The plugins will
|
Class to pull all Renderer interactions into one place. The plugins will call helper methods to do the rendering but
|
||||||
call helper methods to do the rendering but this class will provide
|
this class will provide display defense code.
|
||||||
display defense code.
|
|
||||||
"""
|
"""
|
||||||
log.info(u'Renderer Loaded')
|
log.info(u'Renderer Loaded')
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""
|
"""
|
||||||
Initialise the renderer.
|
Initialise the renderer.
|
||||||
|
|
||||||
``image_manager``
|
|
||||||
A image_manager instance which takes care of e. g. caching and
|
|
||||||
resizing images.
|
|
||||||
|
|
||||||
``theme_manager``
|
|
||||||
The theme_manager instance, used to get the current theme details.
|
|
||||||
"""
|
"""
|
||||||
log.debug(u'Initialisation started')
|
log.debug(u'Initialisation started')
|
||||||
self.screens = ScreenList()
|
self.screens = ScreenList()
|
||||||
@ -99,19 +91,17 @@ class Renderer(object):
|
|||||||
|
|
||||||
def update_theme(self, theme_name, old_theme_name=None, only_delete=False):
|
def update_theme(self, theme_name, old_theme_name=None, only_delete=False):
|
||||||
"""
|
"""
|
||||||
This method updates the theme in ``_theme_dimensions`` when a theme
|
This method updates the theme in ``_theme_dimensions`` when a theme has been edited or renamed.
|
||||||
has been edited or renamed.
|
|
||||||
|
|
||||||
``theme_name``
|
``theme_name``
|
||||||
The current theme name.
|
The current theme name.
|
||||||
|
|
||||||
``old_theme_name``
|
``old_theme_name``
|
||||||
The old theme name. Has only to be passed, when the theme has been
|
The old theme name. Has only to be passed, when the theme has been renamed. Defaults to *None*.
|
||||||
renamed. Defaults to *None*.
|
|
||||||
|
|
||||||
``only_delete``
|
``only_delete``
|
||||||
Only remove the given ``theme_name`` from the ``_theme_dimensions``
|
Only remove the given ``theme_name`` from the ``_theme_dimensions`` list. This can be used when a theme is
|
||||||
list. This can be used when a theme is permanently deleted.
|
permanently deleted.
|
||||||
"""
|
"""
|
||||||
if old_theme_name is not None and old_theme_name in self._theme_dimensions:
|
if old_theme_name is not None and old_theme_name in self._theme_dimensions:
|
||||||
del self._theme_dimensions[old_theme_name]
|
del self._theme_dimensions[old_theme_name]
|
||||||
@ -144,20 +134,17 @@ class Renderer(object):
|
|||||||
Set up the theme to be used before rendering an item.
|
Set up the theme to be used before rendering an item.
|
||||||
|
|
||||||
``override_theme_data``
|
``override_theme_data``
|
||||||
The theme data should be passed, when we want to use our own theme
|
The theme data should be passed, when we want to use our own theme data, regardless of the theme level. This
|
||||||
data, regardless of the theme level. This should for example be used
|
should for example be used in the theme manager. **Note**, this is **not** to be mixed up with the
|
||||||
in the theme manager. **Note**, this is **not** to be mixed up with
|
``set_item_theme`` method.
|
||||||
the ``set_item_theme`` method.
|
|
||||||
"""
|
"""
|
||||||
# Just assume we use the global theme.
|
# Just assume we use the global theme.
|
||||||
theme_to_use = self.global_theme_name
|
theme_to_use = self.global_theme_name
|
||||||
# The theme level is either set to Service or Item. Use the service
|
# The theme level is either set to Service or Item. Use the service theme if one is set. We also have to use the
|
||||||
# theme if one is set. We also have to use the service theme, even when
|
# service theme, even when the theme level is set to Item, because the item does not necessarily have to have a
|
||||||
# the theme level is set to Item, because the item does not necessarily
|
# theme.
|
||||||
# have to have a theme.
|
|
||||||
if self.theme_level != ThemeLevel.Global:
|
if self.theme_level != ThemeLevel.Global:
|
||||||
# When the theme level is at Service and we actually have a service
|
# When the theme level is at Service and we actually have a service theme then use it.
|
||||||
# theme then use it.
|
|
||||||
if self.service_theme_name:
|
if self.service_theme_name:
|
||||||
theme_to_use = self.service_theme_name
|
theme_to_use = self.service_theme_name
|
||||||
# If we have Item level and have an item theme then use it.
|
# If we have Item level and have an item theme then use it.
|
||||||
@ -206,8 +193,7 @@ class Renderer(object):
|
|||||||
|
|
||||||
def set_item_theme(self, item_theme_name):
|
def set_item_theme(self, item_theme_name):
|
||||||
"""
|
"""
|
||||||
Set the item-level theme. **Note**, this has to be done for each item we
|
Set the item-level theme. **Note**, this has to be done for each item we are rendering.
|
||||||
are rendering.
|
|
||||||
|
|
||||||
``item_theme_name``
|
``item_theme_name``
|
||||||
The item theme's name.
|
The item theme's name.
|
||||||
@ -238,9 +224,8 @@ class Renderer(object):
|
|||||||
serviceItem.raw_footer = FOOTER
|
serviceItem.raw_footer = FOOTER
|
||||||
# if No file do not update cache
|
# if No file do not update cache
|
||||||
if theme_data.background_filename:
|
if theme_data.background_filename:
|
||||||
self.image_manager.add_image(theme_data.background_filename,
|
self.image_manager.add_image(
|
||||||
ImageSource.Theme,
|
theme_data.background_filename, ImageSource.Theme, QtGui.QColor(theme_data.background_border_color))
|
||||||
QtGui.QColor(theme_data.background_border_color))
|
|
||||||
theme_data, main, footer = self.pre_render(theme_data)
|
theme_data, main, footer = self.pre_render(theme_data)
|
||||||
serviceItem.themedata = theme_data
|
serviceItem.themedata = theme_data
|
||||||
serviceItem.main = main
|
serviceItem.main = main
|
||||||
@ -278,22 +263,21 @@ class Renderer(object):
|
|||||||
if u'[---]' in text:
|
if u'[---]' in text:
|
||||||
while True:
|
while True:
|
||||||
slides = text.split(u'\n[---]\n', 2)
|
slides = text.split(u'\n[---]\n', 2)
|
||||||
# If there are (at least) two occurrences of [---] we use
|
# If there are (at least) two occurrences of [---] we use the first two slides (and neglect the last
|
||||||
# the first two slides (and neglect the last for now).
|
# for now).
|
||||||
if len(slides) == 3:
|
if len(slides) == 3:
|
||||||
html_text = expand_tags(u'\n'.join(slides[:2]))
|
html_text = expand_tags(u'\n'.join(slides[:2]))
|
||||||
# We check both slides to determine if the optional split is
|
# We check both slides to determine if the optional split is needed (there is only one optional
|
||||||
# needed (there is only one optional split).
|
# split).
|
||||||
else:
|
else:
|
||||||
html_text = expand_tags(u'\n'.join(slides))
|
html_text = expand_tags(u'\n'.join(slides))
|
||||||
html_text = html_text.replace(u'\n', u'<br>')
|
html_text = html_text.replace(u'\n', u'<br>')
|
||||||
if self._text_fits_on_slide(html_text):
|
if self._text_fits_on_slide(html_text):
|
||||||
# The first two optional slides fit (as a whole) on one
|
# The first two optional slides fit (as a whole) on one slide. Replace the first occurrence
|
||||||
# slide. Replace the first occurrence of [---].
|
# of [---].
|
||||||
text = text.replace(u'\n[---]', u'', 1)
|
text = text.replace(u'\n[---]', u'', 1)
|
||||||
else:
|
else:
|
||||||
# The first optional slide fits, which means we have to
|
# The first optional slide fits, which means we have to render the first optional slide.
|
||||||
# render the first optional slide.
|
|
||||||
text_contains_split = u'[---]' in text
|
text_contains_split = u'[---]' in text
|
||||||
if text_contains_split:
|
if text_contains_split:
|
||||||
try:
|
try:
|
||||||
@ -343,8 +327,7 @@ class Renderer(object):
|
|||||||
self.width = screen_size.width()
|
self.width = screen_size.width()
|
||||||
self.height = screen_size.height()
|
self.height = screen_size.height()
|
||||||
self.screen_ratio = float(self.height) / float(self.width)
|
self.screen_ratio = float(self.height) / float(self.width)
|
||||||
log.debug(u'_calculate default %s, %f' % (screen_size,
|
log.debug(u'_calculate default %s, %f' % (screen_size, self.screen_ratio))
|
||||||
self.screen_ratio))
|
|
||||||
# 90% is start of footer
|
# 90% is start of footer
|
||||||
self.footer_start = int(self.height * 0.90)
|
self.footer_start = int(self.height * 0.90)
|
||||||
|
|
||||||
@ -369,12 +352,10 @@ class Renderer(object):
|
|||||||
The theme data.
|
The theme data.
|
||||||
"""
|
"""
|
||||||
if not theme_data.font_footer_override:
|
if not theme_data.font_footer_override:
|
||||||
return QtCore.QRect(10, self.footer_start, self.width - 20,
|
return QtCore.QRect(10, self.footer_start, self.width - 20, self.height - self.footer_start)
|
||||||
self.height - self.footer_start)
|
|
||||||
else:
|
else:
|
||||||
return QtCore.QRect(theme_data.font_footer_x,
|
return QtCore.QRect(theme_data.font_footer_x,
|
||||||
theme_data.font_footer_y, theme_data.font_footer_width - 1,
|
theme_data.font_footer_y, theme_data.font_footer_width - 1, theme_data.font_footer_height - 1)
|
||||||
theme_data.font_footer_height - 1)
|
|
||||||
|
|
||||||
def _set_text_rectangle(self, theme_data, rect_main, rect_footer):
|
def _set_text_rectangle(self, theme_data, rect_main, rect_footer):
|
||||||
"""
|
"""
|
||||||
@ -397,9 +378,8 @@ class Renderer(object):
|
|||||||
if theme_data.font_main_shadow:
|
if theme_data.font_main_shadow:
|
||||||
self.page_width -= int(theme_data.font_main_shadow_size)
|
self.page_width -= int(theme_data.font_main_shadow_size)
|
||||||
self.page_height -= int(theme_data.font_main_shadow_size)
|
self.page_height -= int(theme_data.font_main_shadow_size)
|
||||||
# For the life of my I don't know why we have to completely kill the
|
# For the life of my I don't know why we have to completely kill the QWebView in order for the display to work
|
||||||
# QWebView in order for the display to work properly, but we do. See
|
# properly, but we do. See bug #1041366 for an example of what happens if we take this out.
|
||||||
# bug #1041366 for an example of what happens if we take this out.
|
|
||||||
self.web = None
|
self.web = None
|
||||||
self.web = QtWebKit.QWebView()
|
self.web = QtWebKit.QWebView()
|
||||||
self.web.setVisible(False)
|
self.web.setVisible(False)
|
||||||
@ -425,10 +405,9 @@ class Renderer(object):
|
|||||||
|
|
||||||
def _paginate_slide(self, lines, line_end):
|
def _paginate_slide(self, lines, line_end):
|
||||||
"""
|
"""
|
||||||
Figure out how much text can appear on a slide, using the current
|
Figure out how much text can appear on a slide, using the current theme settings.
|
||||||
theme settings.
|
**Note:** The smallest possible "unit" of text for a slide is one line. If the line is too long it will be cut
|
||||||
**Note:** The smallest possible "unit" of text for a slide is one line.
|
off when displayed.
|
||||||
If the line is too long it will be cut off when displayed.
|
|
||||||
|
|
||||||
``lines``
|
``lines``
|
||||||
The text to be fitted on the slide split into lines.
|
The text to be fitted on the slide split into lines.
|
||||||
@ -444,8 +423,8 @@ class Renderer(object):
|
|||||||
html_lines = map(expand_tags, lines)
|
html_lines = map(expand_tags, lines)
|
||||||
# Text too long so go to next page.
|
# Text too long so go to next page.
|
||||||
if not self._text_fits_on_slide(separator.join(html_lines)):
|
if not self._text_fits_on_slide(separator.join(html_lines)):
|
||||||
html_text, previous_raw = self._binary_chop(formatted,
|
html_text, previous_raw = self._binary_chop(
|
||||||
previous_html, previous_raw, html_lines, lines, separator, u'')
|
formatted, previous_html, previous_raw, html_lines, lines, separator, u'')
|
||||||
else:
|
else:
|
||||||
previous_raw = separator.join(lines)
|
previous_raw = separator.join(lines)
|
||||||
formatted.append(previous_raw)
|
formatted.append(previous_raw)
|
||||||
@ -454,18 +433,15 @@ class Renderer(object):
|
|||||||
|
|
||||||
def _paginate_slide_words(self, lines, line_end):
|
def _paginate_slide_words(self, lines, line_end):
|
||||||
"""
|
"""
|
||||||
Figure out how much text can appear on a slide, using the current
|
Figure out how much text can appear on a slide, using the current theme settings.
|
||||||
theme settings.
|
**Note:** The smallest possible "unit" of text for a slide is one word. If one line is too long it will be
|
||||||
**Note:** The smallest possible "unit" of text for a slide is one word.
|
processed word by word. This is sometimes need for **bible** verses.
|
||||||
If one line is too long it will be processed word by word. This is
|
|
||||||
sometimes need for **bible** verses.
|
|
||||||
|
|
||||||
``lines``
|
``lines``
|
||||||
The text to be fitted on the slide split into lines.
|
The text to be fitted on the slide split into lines.
|
||||||
|
|
||||||
``line_end``
|
``line_end``
|
||||||
The text added after each line. Either ``u' '`` or ``u'<br>``.
|
The text added after each line. Either ``u' '`` or ``u'<br>``. This is needed for **bibles**.
|
||||||
This is needed for **bibles**.
|
|
||||||
"""
|
"""
|
||||||
log.debug(u'_paginate_slide_words - Start')
|
log.debug(u'_paginate_slide_words - Start')
|
||||||
formatted = []
|
formatted = []
|
||||||
@ -476,22 +452,19 @@ class Renderer(object):
|
|||||||
html_line = expand_tags(line)
|
html_line = expand_tags(line)
|
||||||
# Text too long so go to next page.
|
# Text too long so go to next page.
|
||||||
if not self._text_fits_on_slide(previous_html + html_line):
|
if not self._text_fits_on_slide(previous_html + html_line):
|
||||||
# Check if there was a verse before the current one and append
|
# Check if there was a verse before the current one and append it, when it fits on the page.
|
||||||
# it, when it fits on the page.
|
|
||||||
if previous_html:
|
if previous_html:
|
||||||
if self._text_fits_on_slide(previous_html):
|
if self._text_fits_on_slide(previous_html):
|
||||||
formatted.append(previous_raw)
|
formatted.append(previous_raw)
|
||||||
previous_html = u''
|
previous_html = u''
|
||||||
previous_raw = u''
|
previous_raw = u''
|
||||||
# Now check if the current verse will fit, if it does
|
# Now check if the current verse will fit, if it does not we have to start to process the verse
|
||||||
# not we have to start to process the verse word by
|
# word by word.
|
||||||
# word.
|
|
||||||
if self._text_fits_on_slide(html_line):
|
if self._text_fits_on_slide(html_line):
|
||||||
previous_html = html_line + line_end
|
previous_html = html_line + line_end
|
||||||
previous_raw = line + line_end
|
previous_raw = line + line_end
|
||||||
continue
|
continue
|
||||||
# Figure out how many words of the line will fit on screen as
|
# Figure out how many words of the line will fit on screen as the line will not fit as a whole.
|
||||||
# the line will not fit as a whole.
|
|
||||||
raw_words = self._words_split(line)
|
raw_words = self._words_split(line)
|
||||||
html_words = map(expand_tags, raw_words)
|
html_words = map(expand_tags, raw_words)
|
||||||
previous_html, previous_raw = \
|
previous_html, previous_raw = \
|
||||||
@ -505,19 +478,15 @@ class Renderer(object):
|
|||||||
|
|
||||||
def _get_start_tags(self, raw_text):
|
def _get_start_tags(self, raw_text):
|
||||||
"""
|
"""
|
||||||
Tests the given text for not closed formatting tags and returns a tuple
|
Tests the given text for not closed formatting tags and returns a tuple consisting of three unicode strings::
|
||||||
consisting of three unicode strings::
|
|
||||||
|
|
||||||
(u'{st}{r}Text text text{/r}{/st}', u'{st}{r}', u'<strong>
|
(u'{st}{r}Text text text{/r}{/st}', u'{st}{r}', u'<strong><span style="-webkit-text-fill-color:red">')
|
||||||
<span style="-webkit-text-fill-color:red">')
|
|
||||||
|
|
||||||
The first unicode string is the text, with correct closing tags. The
|
The first unicode string is the text, with correct closing tags. The second unicode string are OpenLP's opening
|
||||||
second unicode string are OpenLP's opening formatting tags and the third
|
formatting tags and the third unicode string the html opening formatting tags.
|
||||||
unicode string the html opening formatting tags.
|
|
||||||
|
|
||||||
``raw_text``
|
``raw_text``
|
||||||
The text to test. The text must **not** contain html tags, only
|
The text to test. The text must **not** contain html tags, only OpenLP formatting tags are allowed::
|
||||||
OpenLP formatting tags are allowed::
|
|
||||||
|
|
||||||
{st}{r}Text text text
|
{st}{r}Text text text
|
||||||
"""
|
"""
|
||||||
@ -529,9 +498,8 @@ class Renderer(object):
|
|||||||
if raw_text.count(tag[u'start tag']) != raw_text.count(tag[u'end tag']):
|
if raw_text.count(tag[u'start tag']) != raw_text.count(tag[u'end tag']):
|
||||||
raw_tags.append((raw_text.find(tag[u'start tag']), tag[u'start tag'], tag[u'end tag']))
|
raw_tags.append((raw_text.find(tag[u'start tag']), tag[u'start tag'], tag[u'end tag']))
|
||||||
html_tags.append((raw_text.find(tag[u'start tag']), tag[u'start html']))
|
html_tags.append((raw_text.find(tag[u'start tag']), tag[u'start html']))
|
||||||
# Sort the lists, so that the tags which were opened first on the first
|
# Sort the lists, so that the tags which were opened first on the first slide (the text we are checking) will be
|
||||||
# slide (the text we are checking) will be opened first on the next
|
# opened first on the next slide as well.
|
||||||
# slide as well.
|
|
||||||
raw_tags.sort(key=lambda tag: tag[0])
|
raw_tags.sort(key=lambda tag: tag[0])
|
||||||
html_tags.sort(key=lambda tag: tag[0])
|
html_tags.sort(key=lambda tag: tag[0])
|
||||||
# Create a list with closing tags for the raw_text.
|
# Create a list with closing tags for the raw_text.
|
||||||
@ -547,46 +515,40 @@ class Renderer(object):
|
|||||||
|
|
||||||
def _binary_chop(self, formatted, previous_html, previous_raw, html_list, raw_list, separator, line_end):
|
def _binary_chop(self, formatted, previous_html, previous_raw, html_list, raw_list, separator, line_end):
|
||||||
"""
|
"""
|
||||||
This implements the binary chop algorithm for faster rendering. This
|
This implements the binary chop algorithm for faster rendering. This algorithm works line based (line by line)
|
||||||
algorithm works line based (line by line) and word based (word by word).
|
and word based (word by word). It is assumed that this method is **only** called, when the lines/words to be
|
||||||
It is assumed that this method is **only** called, when the lines/words
|
rendered do **not** fit as a whole.
|
||||||
to be rendered do **not** fit as a whole.
|
|
||||||
|
|
||||||
``formatted``
|
``formatted``
|
||||||
The list to append any slides.
|
The list to append any slides.
|
||||||
|
|
||||||
``previous_html``
|
``previous_html``
|
||||||
The html text which is know to fit on a slide, but is not yet added
|
The html text which is know to fit on a slide, but is not yet added to the list of slides. (unicode string)
|
||||||
to the list of slides. (unicode string)
|
|
||||||
|
|
||||||
``previous_raw``
|
``previous_raw``
|
||||||
The raw text (with formatting tags) which is know to fit on a slide,
|
The raw text (with formatting tags) which is know to fit on a slide, but is not yet added to the list of
|
||||||
but is not yet added to the list of slides. (unicode string)
|
slides. (unicode string)
|
||||||
|
|
||||||
``html_list``
|
``html_list``
|
||||||
The elements which do not fit on a slide and needs to be processed
|
The elements which do not fit on a slide and needs to be processed using the binary chop. The text contains
|
||||||
using the binary chop. The text contains html.
|
html.
|
||||||
|
|
||||||
``raw_list``
|
``raw_list``
|
||||||
The elements which do not fit on a slide and needs to be processed
|
The elements which do not fit on a slide and needs to be processed using the binary chop. The elements can
|
||||||
using the binary chop. The elements can contain formatting tags.
|
contain formatting tags.
|
||||||
|
|
||||||
``separator``
|
``separator``
|
||||||
The separator for the elements. For lines this is ``u'<br>'`` and
|
The separator for the elements. For lines this is ``u'<br>'`` and for words this is ``u' '``.
|
||||||
for words this is ``u' '``.
|
|
||||||
|
|
||||||
``line_end``
|
``line_end``
|
||||||
The text added after each "element line". Either ``u' '`` or
|
The text added after each "element line". Either ``u' '`` or ``u'<br>``. This is needed for bibles.
|
||||||
``u'<br>``. This is needed for bibles.
|
|
||||||
"""
|
"""
|
||||||
smallest_index = 0
|
smallest_index = 0
|
||||||
highest_index = len(html_list) - 1
|
highest_index = len(html_list) - 1
|
||||||
index = int(highest_index / 2)
|
index = int(highest_index / 2)
|
||||||
while True:
|
while True:
|
||||||
if not self._text_fits_on_slide(
|
if not self._text_fits_on_slide(previous_html + separator.join(html_list[:index + 1]).strip()):
|
||||||
previous_html + separator.join(html_list[:index + 1]).strip()):
|
# We know that it does not fit, so change/calculate the new index and highest_index accordingly.
|
||||||
# We know that it does not fit, so change/calculate the
|
|
||||||
# new index and highest_index accordingly.
|
|
||||||
highest_index = index
|
highest_index = index
|
||||||
index = int(index - (index - smallest_index) / 2)
|
index = int(index - (index - smallest_index) / 2)
|
||||||
else:
|
else:
|
||||||
@ -607,14 +569,12 @@ class Renderer(object):
|
|||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
# Check if the remaining elements fit on the slide.
|
# Check if the remaining elements fit on the slide.
|
||||||
if self._text_fits_on_slide(
|
if self._text_fits_on_slide(html_tags + separator.join(html_list[index + 1:]).strip()):
|
||||||
html_tags + separator.join(html_list[index + 1:]).strip()):
|
|
||||||
previous_html = html_tags + separator.join(html_list[index + 1:]).strip() + line_end
|
previous_html = html_tags + separator.join(html_list[index + 1:]).strip() + line_end
|
||||||
previous_raw = raw_tags + separator.join(raw_list[index + 1:]).strip() + line_end
|
previous_raw = raw_tags + separator.join(raw_list[index + 1:]).strip() + line_end
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
# The remaining elements do not fit, thus reset the indexes,
|
# The remaining elements do not fit, thus reset the indexes, create a new list and continue.
|
||||||
# create a new list and continue.
|
|
||||||
raw_list = raw_list[index + 1:]
|
raw_list = raw_list[index + 1:]
|
||||||
raw_list[0] = raw_tags + raw_list[0]
|
raw_list[0] = raw_tags + raw_list[0]
|
||||||
html_list = html_list[index + 1:]
|
html_list = html_list[index + 1:]
|
||||||
@ -626,8 +586,7 @@ class Renderer(object):
|
|||||||
|
|
||||||
def _text_fits_on_slide(self, text):
|
def _text_fits_on_slide(self, text):
|
||||||
"""
|
"""
|
||||||
Checks if the given ``text`` fits on a slide. If it does ``True`` is
|
Checks if the given ``text`` fits on a slide. If it does ``True`` is returned, otherwise ``False``.
|
||||||
returned, otherwise ``False``.
|
|
||||||
|
|
||||||
``text``
|
``text``
|
||||||
The text to check. It may contain HTML tags.
|
The text to check. It may contain HTML tags.
|
||||||
|
Loading…
Reference in New Issue
Block a user