2
0
Fork 0

Doesn't work yet with block logic, still working on it, just saving it.

This commit is contained in:
Thomas Touhey 2018-02-11 12:01:32 +01:00
parent b4d161f2ab
commit bc0bcfa65f
No known key found for this signature in database
GPG Key ID: 2ECEB0517AD947FB
35 changed files with 892 additions and 504 deletions

View File

@ -63,7 +63,7 @@ You can change the color of the text using the `[color=xxx]` (or `[xxx]`
directly for simple colors, where `xxx` represents the color) tag. This
tag accepts several types of values:
- simple color names (inspired from CSS <TODO: link required>) as `red`,
- simple color names (inspired from CSS <TODO: link required>) such as `red`,
`blue`, `green`, `transparent`;
- color hex codes using `#` followed by hex digits, e.g. `#01020F`,
where the first group of hex digits represents the red component from

View File

@ -6,26 +6,34 @@ utility named `textout()`.
I, Thomas “Cakeisalie5” Touhey, rewrote it recently, and it works pretty well
while being secure, but as the next version of _Planète Casio_ (the ”v5”)
will be written from scratch, I figured out I could rewrite the `textout()`
utility in Python, and improve the language parsing to be more practicle and
utility in Python, and improve the language parsing to be more practical and
add features that are in the original BBcode markup language.
As this is a rewrite, the vulnerabilities and bug will not be common to this
project and the online version of the transcoder. To use this module, simply
use the `tohtml()` function once imported:
project and the online version of the transcoder.
## Usage
To use this module, simply use the `to<language>()` functions once imported:
#!/usr/bin/env python3
import textoutpc
text = "Hello, [i]beautiful [b]world[/i]!"
print(textoutpc.tohtml(text))
print("---")
print(textoutpc.tolightscript(text))
Some day, this module should be able to convert to other formats such
as [lightscript][ls], a markdown-like formatting language made by another
_Planète Casio_ admin, who also made the native Python module.
The supported output types are:
- `html`: HTML compatible output, requiring some additional style and script
(TODO: document the required CSS classes and JS scripts);
- `lightscript`: Markdown-like language
([official topic on Planète Casio][lstp], [source repository][ls]).
## What is left to do
- Manage paragraph and inline tags differently;
- Manage blocks superseeding each other;
- Implement BBcode lists using `[*]`, `[**]`, …;
- Manage lightscript (or even markdown?) as output languages;
- Check where the errors are to display them to the user:
@ -38,4 +46,5 @@ _Planète Casio_ admin, who also made the native Python module.
- Look for security flaws (we don't want stored XSS here!).
[pc]: https://www.planet-casio.com/Fr/
[ls]: https://bitbucket.org/Lephenixnoir/lightscript
[ls]: https://git.planet-casio.com/lephe/lightscript
[lstp]: https://planet-casio.com/Fr/forums/lecture_sujet.php?id=15022

View File

@ -23,7 +23,8 @@ There are a few public members you can define as a tag:
It is defined as `True` by default for all tags;
- `notempty`: ignore the tag when its content is empty. By default, this
value is `False`;
- `super`: is a super-block (for blocks), adds a paragraph tag implicitely.
- `superblock`: is a super-block (for blocks), adds a paragraph
tag implicitely.
So for example, if I want to make the inline tag `[hello]` as an example,
with the alternate name `[hai]`, I'd start off by writing:

View File

@ -1,5 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
""" textout to HTML converter for the command line. """
import sys, argparse

View File

@ -1,5 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
""" Setup script for the textoutpc Python package and script. """
from setuptools import setup

View File

@ -1,9 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
""" Unit tests for the `textoutpc` Python module. """
# ---
# Simple file to indicate that this folder is a Python module.
# Doesn't actually contain code.
# ---
# This file is only there to indicate that the folder is a module.
# It doesn't actually contain code.
# End of file.

View File

@ -9,60 +9,98 @@ import textoutpc
# Define the tests.
__test_cases = {
# Really basic tests.
# Basic text.
'': '',
'lol': '<p>lol</p>',
'<script>alert(1);</script>': \
'<p>&lt;script&gt;alert(1);&lt;/script&gt;</p>',
# Funny tests.
'<script>alert(1);</script>': '<p>&lt;script&gt;alert(1);&lt;/script&gt;</p>',
'[progress=lol]mdr[/progress]': '<p>[progress=lol]mdr[/progress]</p>',
'[u][b][a][i][/b]': "<p><u><b>[a]<i></i></b></u></p>",
'[u][b]a[/]mdr': '<p><u><b>a</b>mdr</u></p>',
'[blue]text[/blue]': '<p><span style="color: #0000FF">text</span></p>',
'[rot13]obawbhe[/rot13]': '<p>bonjour</p>',
# Links.
'[url=http://hey.org/lol[]>"a]': '<p><a href="http://hey.org/lol[]&gt;' \
'&quot;a" target="_blank" rel="noopener">' \
'http://hey.org/lol[]&gt;&quot;a</a></p>',
'[url]javascript:alert(1)[/url]': '<p>[url]javascript:alert(1)[/url]</p>',
'(http://www.example.org/some-[damn-url]-(youknow))': \
'<p>(<a href="http://www.example.org/some-[damn-url]-(youknow)" ' \
'target="_blank" rel="noopener">' \
'http://www.example.org/some-[damn-url]-(youknow)</a>)</p>',
# No evaluation.
"[b]a[noeval]b[/b]c[/noeval]d": "<p><b>ab[/b]cd</b></p>",
"a[noeval]b[noeval]c[/noeval]d[/noeval]e": "<p>ab[noeval]c[/noeval]de</p>",
"`[code]`": '<p><span style="font-family: monospace;">[code]</span></p>',
"[noeval]``[/noeval]": "<p>``</p>",
"[inlinecode]`[/inlinecode]": \
'<p><span style="font-family: monospace;">`</span></p>',
'[noeval]<>[/noeval]': '<p>&lt;&gt;</p>',
# Completion.
# Other tests. (?)
'[a][c][/a]': '<p>[a][c][/a]</p>',
'[a][a]': '<p>[a][a]</p>',
"[<>]><[/<>]": "<p>[&lt;&gt;]&gt;&lt;[/&lt;&gt;]</p>",
# Autolinking.
'(http://www.example.org/some-[damn-url]-(youknow))': \
'<p>(<a href="http://www.example.org/some-[damn-url]-(youknow)" ' \
'target="_blank" rel="noopener">' \
'http://www.example.org/some-[damn-url]-(youknow)</a>)</p>',
'https://thomas.touhey.fr/, tu vois ?': \
'<p><a href="https://thomas.touhey.fr/" target="_blank" ' \
'rel="noopener">https://thomas.touhey.fr/</a>, tu vois ?</p>',
# Basic text styling.
'[u][b][a][i][/b]': "<p><u><b>[a]<i></i></b></u></p>",
'[u][b]a[/]mdr': '<p><u><b>a</b>mdr</u></p>',
# Blocks, alignment.
'[left]lol[/]hi': '<p class="align-left">lol</p><p>hi</p>',
'a[justify]b': '<p>a</p><p class="align-justify">b</p>',
'a[i]k[center][b]b[justify]c[/center]d[/]wouhou': \
'<p>a<i>k</i></p>' \
'<p class="align-center"><i><b>b</b></i></p>' \
'<p class="align-justify"><i><b>c</b></i></p>' \
'<p><i>d</i>wouhou</p>',
# Titles.
'lolk[title]smth': '<p>lolk</p>' '<h4>smth</h4>',
'[subtitle]<>': '<h5>&lt;&gt;</h5>',
# Fonts.
'[arial]test': '<p><span style="font-family: arial">test</span></p>',
'[font=mono]stereo': \
'<p><span style="font-family: monospace">stereo</span></p>',
'[haettenschweiler]': '',
'[font=hello]yea': '<p>[font=hello]yea</p>',
# Color.
'yea[color=blue]dabadee': \
'<p>yea<span style="color: #0000FF">dabadee</span></p>',
'[color=#12345F]a': '<p><span style="color: #12345F">a</span></p>',
'[color=#123]a': '<p><span style="color: #112233">a</span></p>',
'[color=123]a': '<p><span style="color: #010203">a</span></p>',
'[color=chucknorris]a': '<p><span style="color: #C00000">a</span></p>',
'[color=rgb(1, 22,242)]a': '<p><span style="color: #0116F2">a</span></p>',
'[color= rgb (1,22, 242 , 50.0% )]a': '<p><span style="color: #0116F2; ' \
'color: rgba(1, 22, 242, 0.5)">a</span></p>',
'[color=rgba(1,22,242,0.500)]a': '<p><span style="color: #0116F2; ' \
'color: rgba(1, 22, 242, 0.5)">a</span></p>',
'[color=hsl(0, 1,50.0%)]r': '<p><span style="color: #FF0000">r</span></p>',
# TODO: hls, hwb
# Links.
'[url]<script>alert(1);</script>[/url]': \
'<p>[url]&lt;script&gt;alert(1);&lt;/script&gt;[/url]</p>',
'[url]': '<p>[url]</p>',
'[url=https://thomas.touhey.fr/]mon profil est le meilleur[/url]':
'<p><a href="https://thomas.touhey.fr/" target="_blank" ' \
'rel="noopener">mon profil est le meilleur</a></p>',
'[url=https://thomas.touhey.fr/]': \
'<p><a href="https://thomas.touhey.fr/" target="_blank" ' \
'rel="noopener">https://thomas.touhey.fr/</a></p>',
'https://thomas.touhey.fr/, tu vois ?': \
'<p><a href="https://thomas.touhey.fr/" target="_blank" ' \
'rel="noopener">https://thomas.touhey.fr/</a>, tu vois ?</p>',
'[url=http://hey.org/lol[]>"a]': '<p><a href="http://hey.org/lol[]&gt;' \
'&quot;a" target="_blank" rel="noopener">' \
'http://hey.org/lol[]&gt;&quot;a</a></p>',
'[url]javascript:alert(1)[/url]': '<p>[url]javascript:alert(1)[/url]</p>',
'[url]<script>alert(1);</script>[/url]': \
'<p>[url]&lt;script&gt;alert(1);&lt;/script&gt;[/url]</p>',
# Pictures.
'[img]"incroyable<>"[/img]': \
'<p>[img]&quot;incroyable&lt;&gt;&quot;[/img]</p>',
'[profil]cake[/profil]': \
'<p><a href="https://www.planet-casio.com/Fr/compte/voir_profil.php' \
'?membre=cake">cake</a></p>',
'[profile]ekac': \
'<p><a href="https://www.planet-casio.com/Fr/compte/voir_profil.php' \
'?membre=ekac">ekac</a></p>',
# Quotes.
'[quote]': '',
'[quote]a': \
'<div class="citation"><p>a</p></div>',
'[quote=Test 1 :)]lel[/quote]': \
'<div class="citation"><p><b>Test 1 ' \
'<img src="/images/smileys/smile.gif"> a écrit:</b><br />' \
'lel</p></div>',
# Spoilers.
'[spoiler]': '',
'[spoiler=Hello|world> :D]Close this, quick![/spoiler]': \
'<div class="spoiler"><div class="title on" ' \
'onclick="toggleSpoiler(this.parentNode, ' "'open'" ');">Hello' \
@ -71,6 +109,24 @@ __test_cases = {
'&gt; <img src="/images/smileys/grin.gif"></div><div class="off">' \
'Close this, quick!</div></div>',
# Code.
'[code]': '',
"`[code]`": '<p><span style="font-family: monospace;">[code]</span></p>',
'[inlinecode]': '',
"[inlinecode]`[/inlinecode]": \
'<p><span style="font-family: monospace;">`</span></p>',
"[b]a[noeval]b[/b]c[/noeval]d": "<p><b>ab[/b]cd</b></p>",
"a[noeval]b[noeval]c[/noeval]d[/noeval]e": "<p>ab[noeval]c[/noeval]de</p>",
"[noeval]``[/noeval]": "<p>``</p>",
'[noeval]<>[/noeval]': '<p>&lt;&gt;</p>',
# Pictures.
'[img]': '<p>[img]</p>',
'[img]"incroyable<>"[/img]': \
'<p>[img]&quot;incroyable&lt;&gt;&quot;[/img]</p>',
# Videos.
'[video]"><script>alert(1)</script>[/video]': \
'<p>[video]&quot;&gt;&lt;script&gt;alert(1)&lt;/script&gt;' \
@ -88,18 +144,11 @@ __test_cases = {
'https://www.youtube.com/watch?v=&lt;script&gt;alert(1)' \
'&lt;/script&gt;</a></p>',
# Quotes.
'[quote=Test 1 :)]lel[/quote]': \
'<div class="citation"><b>Test 1 ' \
'<img src="/images/smileys/smile.gif"> a écrit:</b><br />lel</div>',
# Paragraphs.
'a[i]k[center][b]b[justify]c[/center]d[/]wouhou': \
'<p>a<i>k</i></p>' \
'<p class="align-center"><i><b>b</b></i></p>' \
'<p class="align-justify"><i><b>c</b></i></p>' \
'<p><i>d</i>wouhou</p>',
# Progress bars.
'[progress=lol]mdr[/progress]': '<p>[progress=lol]mdr[/progress]</p>',
# Text rotation obfuscation.
'[rot13]obawbhe[/rot13]': '<p>bonjour</p>',
}
# Define the tests wrapper, and define the classes.

40
test/test_lightscript.py Executable file
View File

@ -0,0 +1,40 @@
#!/usr/bin/env python3
""" Unit tests for the Python version of textout, lightscript-related funcs.
Uses the builtin `unittest` module.
"""
import unittest
import textoutpc
# Define the tests.
__test_cases = {
# Basic text.
'': '',
}
# Define the tests wrapper, and define the classes.
_cnt = 0
_len = len(str(len(__test_cases)))
_templ = """\
def test_lightscript{n:0>{l}}(self):
self.assertEqual({r}, textoutpc.tolightscript({i}))
"""
def _wrap_test(inp, res):
global _cnt
_cnt += 1
return _templ.format(n = _cnt, l = _len, i = repr(inp), r = repr(res))
exec("class TextoutLightscriptTest(unittest.TestCase):\n maxDiff = None\n" + \
'\n'.join(map(lambda args: _wrap_test(*args), __test_cases.items())),
globals())
# If run as main script, run the test function.
if __name__ == '__main__':
unittest.main()
# End of file.

View File

@ -17,4 +17,12 @@ def tohtml(message):
return _Translator(_io.StringIO(message), _io.StringIO(), 'html') \
.process().getvalue()
def tolightscript(message):
""" Converts textout BBcode to Lightscript.
Receives a string, returns a string. """
return "" # TODO: real thing one day
return _Translator(_io.StringIO(message), _io.StringIO(), 'lightscript') \
.process().getvalue()
# End of file.

13
textoutpc/color/__init__.py Executable file
View File

@ -0,0 +1,13 @@
#!/usr/bin/env python3
""" HTML/CSS-like color parsing, mainly for the `[color]` tag.
Defines the `get_color()` function which returns an rgba value.
The functions in this module do not aim at being totally compliant with
the W3C standards, although it is inspired from it.
"""
from .read import get_color
__all__ = ["get_color"]
# End of file.

164
textoutpc/color/named.py Executable file
View File

@ -0,0 +1,164 @@
#!/usr/bin/env python3
""" Named colors definitions. Color names are case-insensitive.
Taken from: https://www.w3schools.com/cssref/css_colors.asp """
__all__ = ["colors"]
colors = {
# Standard CSS3 named colors, ordered by hex code.
'black': '#000000',
'navy': '#000080',
'darkblue': '#00008B',
'mediumblue': '#0000CD',
'blue': '#0000FF',
'darkgreen': '#006400',
'green': '#008000',
'teal': '#008080',
'darkcyan': '#008B8B',
'deepskyblue': '#00BFFF',
'darkturquoise': '#00CED1',
'mediumspringgreen': '#00FA9A',
'lime': '#00FF00',
'springgreen': '#00FF7F',
'aqua': '#00FFFF',
'cyan': '#00FFFF',
'midnightblue': '#191970',
'dodgerblue': '#1E90FF',
'lightseagreen': '#20B2AA',
'forestgreen': '#228B22',
'seagreen': '#2E8B57',
'darkslategray': '#2F4F4F',
'darkslategrey': '#2F4F4F',
'limegreen': '#32CD32',
'mediumseagreen': '#3CB371',
'turquoise': '#40E0D0',
'royalblue': '#4169E1',
'steelblue': '#4682B4',
'darkslateblue': '#483D8B',
'mediumturquoise': '#48D1CC',
'indigo': '#4B0082',
'darkolivegreen': '#556B2F',
'cadetblue': '#5F9EA0',
'cornflowerblue': '#6495ED',
'rebeccapurple': '#663399',
'mediumaquamarine': '#66CDAA',
'dimgray': '#696969',
'dimgrey': '#696969',
'slateblue': '#6A5ACD',
'olivedrab': '#6B8E23',
'slategray': '#708090',
'slategrey': '#708090',
'lightslategray': '#778899',
'lightslategrey': '#778899',
'mediumslateblue': '#7B68EE',
'lawngreen': '#7CFC00',
'chartreuse': '#7FFF00',
'aquamarine': '#7FFFD4',
'maroon': '#800000',
'purple': '#800080',
'olive': '#808000',
'gray': '#808080',
'grey': '#808080',
'skyblue': '#87CEEB',
'lightskyblue': '#87CEFA',
'blueviolet': '#8A2BE2',
'darkred': '#8B0000',
'darkmagenta': '#8B008B',
'saddlebrown': '#8B4513',
'darkseagreen': '#8FBC8F',
'lightgreen': '#90EE90',
'mediumpurple': '#9370DB',
'darkviolet': '#9400D3',
'palegreen': '#98FB98',
'darkorchid': '#9932CC',
'yellowgreen': '#9ACD32',
'sienna': '#A0522D',
'brown': '#A52A2A',
'darkgray': '#A9A9A9',
'darkgrey': '#A9A9A9',
'lightblue': '#ADD8E6',
'greenyellow': '#ADFF2F',
'paleturquoise': '#AFEEEE',
'lightsteelblue': '#B0C4DE',
'powderblue': '#B0E0E6',
'firebrick': '#B22222',
'darkgoldenrod': '#B8860B',
'mediumorchid': '#BA55D3',
'rosybrown': '#BC8F8F',
'darkkhaki': '#BDB76B',
'silver': '#C0C0C0',
'mediumvioletred': '#C71585',
'indianred': '#CD5C5C',
'peru': '#CD853F',
'chocolate': '#D2691E',
'tan': '#D2B48C',
'lightgray': '#D3D3D3',
'lightgrey': '#D3D3D3',
'thistle': '#D8BFD8',
'orchid': '#DA70D6',
'goldenrod': '#DAA520',
'palevioletred': '#DB7093',
'crimson': '#DC143C',
'gainsboro': '#DCDCDC',
'plum': '#DDA0DD',
'burlywood': '#DEB887',
'lightcyan': '#E0FFFF',
'lavender': '#E6E6FA',
'darksalmon': '#E9967A',
'violet': '#EE82EE',
'palegoldenrod': '#EEE8AA',
'lightcoral': '#F08080',
'khaki': '#F0E68C',
'aliceblue': '#F0F8FF',
'honeydew': '#F0FFF0',
'azure': '#F0FFFF',
'sandybrown': '#F4A460',
'wheat': '#F5DEB3',
'beige': '#F5F5DC',
'whitesmoke': '#F5F5F5',
'mintcream': '#F5FFFA',
'ghostwhite': '#F8F8FF',
'salmon': '#FA8072',
'antiquewhite': '#FAEBD7',
'linen': '#FAF0E6',
'lightgoldenrodyellow': '#FAFAD2',
'oldlace': '#FDF5E6',
'red': '#FF0000',
'magenta': '#FF00FF',
'fuchsia': '#FF00FF',
'deeppink': '#FF1493',
'orangered': '#FF4500',
'tomato': '#FF6347',
'hotpink': '#FF69B4',
'coral': '#FF7F50',
'darkorange': '#FF8C00',
'lightsalmon': '#FFA07A',
'orange': '#FFA500',
'lightpink': '#FFB6C1',
'pink': '#FFC0CB',
'gold': '#FFD700',
'peachpuff': '#FFDAB9',
'navajowhite': '#FFDEAD',
'moccasin': '#FFE4B5',
'bisque': '#FFE4C4',
'mistyrose': '#FFE4E1',
'blanchedalmond': '#FFEBCD',
'papayawhip': '#FFEFD5',
'lavenderblush': '#FFF0F5',
'seashell': '#FFF5EE',
'cornsilk': '#FFF8DC',
'lemonchiffon': '#FFFACD',
'floralwhite': '#FFFAF0',
'snow': '#FFFAFA',
'yellow': '#FFFF00',
'lightyellow': '#FFFFE0',
'ivory': '#FFFFF0',
'white': '#FFFFFF',
# Keyword/special named color.
'transparent': 'rgba(0,0,0,0)',
}
# End of file.

250
textoutpc/color/read.py Executable file
View File

@ -0,0 +1,250 @@
#!/usr/bin/env python3
""" HTML/CSS like color parsing, mainly for the `[color]` tag.
Defines the `get_color()` function which returns an rgba value.
"""
import re as _re
import math as _math
from .named import colors as _named_colors
from .sys import hls_to_rgb as _hls_to_rgb, hwb_to_rgb as _hwb_to_rgb
__all__ = ["get_color"]
_cr = _re.compile("""
rgba?\s*\(
\s* (?P<rgb_r>[0-9]{1,3}) \s*,
\s* (?P<rgb_g>[0-9]{1,3}) \s*,
\s* (?P<rgb_b>[0-9]{1,3}) \s*(,
\s* ((?P<rgb_a_per> ([0-9]+\.?|[0-9]*\.[0-9]+) )%
|(?P<rgb_a_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*)?
\)|
hsla?\s*\(
\s* (?P<hsl_hue>-? ([0-9]+\.?|[0-9]*\.[0-9]+) )
(?P<hsl_agl>deg|grad|rad|turn|) \s*[,\\s]
\s* ((?P<hsl_sat_per> ([0-9]+\.?|[0-9]*\.[0-9]+) )%
|(?P<hsl_sat_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*[,\\s]
\s* ((?P<hsl_lgt_per> ([0-9]+\.?|[0-9]*\.[0-9]+) )%
|(?P<hsl_lgt_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*([,\\s/]
\s* ((?P<hsl_aph_per> ([0-9]+\.?|[0-9]*\.[0-9]+) )%
|(?P<hsl_aph_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*)?
\)|
hlsa?\s*\(
\s* (?P<hls_hue>-? ([0-9]+\.?|[0-9]*\.[0-9]+) )
(?P<hls_agl>deg|grad|rad|turn|) \s*[,\\s]
\s* ((?P<hls_lgt_per> ([0-9]+\.?|[0-9]*\.[0-9]+) )%
|(?P<hls_lgt_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*[,\\s]
\s* ((?P<hls_sat_per> ([0-9]+\.?|[0-9]*\.[0-9]+) )%
|(?P<hls_sat_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*([,\\s/]
\s* ((?P<hls_aph_per> ([0-9]+\.?|[0-9]*\.[0-9]+) )%
|(?P<hls_aph_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*)?
\)|
hwb\s*\(
\s* (?P<hwb_hue>-? ([0-9]+\.?|[0-9]*\.[0-9]+) )
(?P<hwb_agl>deg|grad|rad|turn|) \s*[,\\s]
\s* ((?P<hwb_wht_per> ([0-9]+\.?|[0-9]*\.[0-9]+) )%
|(?P<hwb_wht_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*[,\\s]
\s* ((?P<hwb_blk_per> ([0-9]+\.?|[0-9]*\.[0-9]+) )%
|(?P<hwb_blk_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*([,\\s/]
\s* ((?P<hwb_aph_per> ([0-9]+\.?|[0-9]*\.[0-9]+) )%
|(?P<hwb_aph_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*)?
\)|
\# (?P<hex_digits> [0-9a-f]+)
|
(?P<legacy_chars> [0-9a-z]+)
""", _re.VERBOSE | _re.I | _re.M)
def get_color(value):
""" Get a color from a string.
Returns an (r, g, b, a) color.
Raises an exception if the color could not be read. """
# Check if is a color name.
try: value = _named_colors[value.lower()]
except: pass
# Initialize the alpha.
alpha = 1.0
# Get the match.
match = _cr.match(value).groupdict()
if match['hex_digits'] or match['legacy_chars']:
# Imitate the Netscape behaviour. Find more about this here:
# https://stackoverflow.com/a/8333464
#
# I've also extended the thing as I could to introduce more
# modern syntaxes described on the dedicated MDN page:
# https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
#
# First of all, depending on our source, we will act differently:
# - if we are using the `hex_digits` source, then we use the modern
# behaviour and do the fancy things such as `#ABC -> #AABBCC`
# management and possible alpha decoding;
# - if we are using the `legacy_chars` source, then we sanitize our
# input by replacing invalid characters by '0' characters (the
# 0xFFFF limit is due to how UTF-16 was managed at the time).
# We shall also truncate our input to 128 characters.
#
# After these sanitization options, we will keep the same method as
# for legacy color decoding. It should work and be tolerant enough…
members = 3
if match['hex_digits']:
hx = match['hex_digits'].lower()
# RGB and RGBA (3 and 4 char.) notations.
if len(hx) in (3, 4):
hx = hx[0:1] * 2 + hx[1:2] * 2 + hx[2:3] * 2 + hx[3:4] * 2
# Check if there is transparency or not.
if len(hx) % 3 != 0 and len(hx) % 4 == 0:
members = 4
else: # our source is `legacy_chars`
hx = match['legacy_chars'].lower()
hx = ''.join(c if c in '0123456789abcdef' \
else ('0', '00')[ord(c) > 0xFFFF] for c in hx[:128])[:128]
# First, calculate some values we're going to need.
# `iv` is the size of the zone for a member.
# `sz` is the size of the digits slice to take in that zone (max. 8).
# `of` is the offset in the zone of the slice to take.
iv = _math.ceil(len(hx) / members)
of = iv - 8 if iv > 8 else 0
sz = iv - of
# Then isolate the slices using the values calculated above.
# `gr` will be an array of 3 or 4 digit strings (depending on the
# number of members).
gr = list(map(lambda i: hx[i * iv + of:i * iv + iv] \
.ljust(sz, '0'), range(members)))
# Check how many digits we can skip at the beginning of each slice.
pre = min(map(lambda x: len(x) - len(x.lstrip('0')), gr))
pre = min(pre, sz - 2)
# Then extract the values.
it = map(lambda x: int('0' + x[pre:pre + 2], 16), gr)
if members == 3:
r, g, b = it
else:
r, g, b, alpha = it
alpha /= 255.0
elif match['rgb_r']:
# Extract the values.
r = int(match['rgb_r'])
g = int(match['rgb_g'])
b = int(match['rgb_b'])
if match['rgb_a_per']:
alpha = float(match['rgb_a_per']) / 100.0
elif match['rgb_a_flt']:
alpha = float(match['rgb_a_flt'])
elif match['hsl_hue'] or match['hls_hue']:
# Extract the values.
if match['hsl_hue']:
hue = float(match['hsl_hue'])
agl = match['hsl_agl']
# Saturation.
if match['hsl_sat_per']:
sat = float(match['hsl_sat_per']) / 100.0
else:
sat = float(match['hsl_sat_flt'])
if sat > 1.0:
sat /= 100.0
# Light.
if match['hsl_lgt_per']:
lgt = float(match['hsl_lgt_per']) / 100.0
else:
lgt = float(match['hsl_lgt_flt'])
if lgt > 1.0:
lgt /= 100.0
# Alpha value.
if match['hsl_aph_per']:
alpha = float(match['hsl_aph_per']) / 100.0
elif match['hsl_aph_flt']:
alpha = float(match['hsl_aph_flt'])
else:
hue = float(match['hls_hue'])
agl = match['hls_agl']
# Saturation.
if match['hls_sat_per']:
sat = float(match['hls_sat_per']) / 100.0
else:
sat = float(match['hls_sat_flt'])
# Light.
if match['hls_lgt_per']:
lgt = float(match['hls_lgt_per']) / 100.0
else:
lgt = float(match['hls_lgt_flt'])
# Alpha value.
if match['hls_aph_per']:
alpha = float(match['hls_aph_per']) / 100.0
elif match['hls_aph_flt']:
alpha = float(match['hls_aph_flt'])
# Prepare the angle.
if agl == 'grad':
hue = hue * 400.0
elif agl == 'rad':
hue = hue / (2 * math.pi)
elif not agl or agl == 'deg':
hue = hue / 360.0
hue = hue % 1.0
if sat > 1 or lgt > 1:
raise Exception
r, g, b = _hls_to_rgb(hue, lgt, sat)
r, g, b = map(lambda x:int(round(x * 255)), (r, g, b))
elif match['hwb_hue']:
hue = float(match['hwb_hue'])
agl = match['hwb_agl']
# Prepare the angle.
if agl == 'grad':
hue = hue * 400.0
elif agl == 'rad':
hue = hue / (2 * math.pi)
elif not agl or agl == 'deg':
hue = hue / 360.0
hue = hue % 1.0
# Saturation.
if match['hwb_wht_per']:
wht = float(match['hwb_wht_per']) / 100.0
else:
wht = float(match['hwb_wht_flt'])
# Light.
if match['hwb_blk_per']:
blk = float(match['hwb_blk_per']) / 100.0
else:
blk = float(match['hwb_blk_flt'])
if wht > 1 or blk > 1:
raise Exception
r, g, b = _hwb_to_rgb(hue, wht, blk)
r, g, b = map(lambda x: int(round(x * 255)), (r, g, b))
if r < 0 or r > 255 or g < 0 or g > 255 or b < 0 or b > 255:
raise Exception
if alpha < 0.0 or alpha > 1.0:
raise Exception
alpha = round(alpha, 4)
return (r, g, b, alpha)
# End of file.

18
textoutpc/color/sys.py Executable file
View File

@ -0,0 +1,18 @@
#!/usr/bin/env python3
""" Conversions between color systems. """
from colorsys import hls_to_rgb
__all__ = ["hls_to_rgb", "hwb_to_rgb"]
def hwb_to_rgb(hue, w, b):
""" Convert HWB to RGB color.
https://drafts.csswg.org/css-color/#hwb-to-rgb """
r, g, b = hls_to_rgb(hue, 0.5, 1.0)
f = lambda x: x * (1 - w - b) + w
r, g, b = f(r), f(g), f(b)
return r, g, b
# End of file.

View File

@ -1,5 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
""" Smiley conversion.
Just convert them.
"""

View File

@ -1,5 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
""" Layer on top of the character stream.
See the `TextoutStream` class description for more information.

View File

@ -1,12 +1,14 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
""" Tag helpers.
As we ought to be able to make separate tag modules, this tag is
""" Tag helpers for the translate utilities.
As we ought to be able to make separate tag modules, this module
does not hardcode the imports and makes it possible to import any
custom module to isolate some tags from the others and make the
`textoutpc` a generic module for BBcode.
"""
from inspect import ismodule as _ismod, isclass as _isclass
from .base import TextoutTag as _TextoutTag, TextoutBlockTag, TextoutInlineTag
from .paragraph import TextoutParagraphTag
from .base import TextoutTag as _TextoutTag, TextoutBlockTag, \
TextoutInlineTag, TextoutParagraphTag
__all__ = ["TextoutParagraphTag", "TextoutBlockTag", "TextoutInlineTag",
"get_tag"]

View File

@ -1,9 +1,13 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
""" Base class for textout tags. For your class to be used as a textout tag,
you have to make it inherit one of these (usually `TextoutBlockTag`
or `TextoutInlineTag`). """
from functools import partial as _p
from inspect import getargspec as _getargspec
__all__ = ["TextoutTag", "TextoutBlockTag", "TextoutInlineTag"]
__all__ = ["TextoutTag", "TextoutBlockTag", "TextoutInlineTag",
"TextoutParagraphTag"]
# ---
# Main base tag class.
@ -26,13 +30,25 @@ class TextoutTag:
# Store internal data.
self.__output_type = ot
self.output_type = ot
# Call both prepare functions.
if hasattr(self, 'prepare_' + ot):
self.prepare = getattr(self, 'prepare_' + ot)
if hasattr(self, 'prepare'):
self.prepare(name, value)
try:
assert len(_getargspec(self.prepare).args) == 4
args = (name, value, ot)
except:
args = (name, value)
self.prepare(*args)
if hasattr(self, 'prepare_' + ot):
prep = getattr(self, 'prepare_' + ot)
try:
assert len(_getargspec(prep).args) == 4
args = (name, value, ot)
except:
args = (name, value)
prep(*args)
# Prepare the preprocessing elements.
if hasattr(self, 'preprocess'):
@ -93,4 +109,17 @@ class TextoutBlockTag(TextoutTag):
class TextoutInlineTag(TextoutTag):
pass
# ---
# Default tag: paragraph.
# ---
class TextoutParagraphTag(TextoutBlockTag):
""" Main tag for basic paragraphs. """
def begin_html(self):
return '<p>'
def end_html(self):
return '</p>'
# End of file.

View File

@ -1,5 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from ..base import *

View File

@ -1,5 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from ..base import *
@ -18,18 +17,18 @@ class TextoutCodeTag(TextoutBlockTag):
generic = False
raw = True
def begin_text(self):
return "```\n"
def end_text(self):
return "\n```\n"
def begin_html(self):
return '<div class="code">'
def end_html(self):
return '</div>'
def begin_lightscript(self):
return '```\n'
def end_lightscript(self):
return '```\n'
class TextoutInlineCodeTag(TextoutInlineTag):
""" Inline code tag, doesn't display a box, simply doesn't evaluate
the content and uses monospace font.
@ -44,18 +43,18 @@ class TextoutInlineCodeTag(TextoutInlineTag):
generic = False
raw = True
def begin_text(self):
return "`"
def end_text(self):
return "`"
def begin_html(self):
return '<span style="font-family: monospace;">'
def end_html(self):
return '</span>'
def begin_lightscript(self):
return '`'
def end_lightscript(self):
return '`'
class TextoutNoEvalTag(TextoutInlineTag):
""" Inline code tag, simply doesn't evaluate the content.
Example uses:

View File

@ -1,5 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from ..base import *
from urllib.parse import urlparse
@ -56,9 +55,6 @@ class TextoutImageTag(TextoutBlockTag):
self._url = content
def content_text(self):
return '![ {} ]!'.format(self._url)
def content_html(self):
style = []
cls = []
@ -80,6 +76,10 @@ class TextoutImageTag(TextoutBlockTag):
' class="{}"'.format(' '.join(cls)) if cls else '',
' style="{}"'.format('; '.join(style)) if style else '')
def content_lightscript(self):
url = self._url.replace('[', '%5B').replace(']', '%5D')
return '[[image:{}]]'.format(url)
class TextoutAdminImageTag(TextoutImageTag):
""" This tag is special for Planète Casio, as it takes images from
the `ad`ministration's image folder.

View File

@ -1,5 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from ..base import *
import re as _re
@ -24,9 +23,6 @@ class TextoutLabelTag(TextoutInlineTag):
raise Exception
self._label = value
def begin_text(self):
return '[{}]: '.format(self._label)
def begin_html(self):
#name = 'label-{}'.format(self._label)
#if _v42compat:
@ -48,9 +44,6 @@ class TextoutTargetTag(TextoutInlineTag):
raise Exception
self._label = value
def end_text(self):
return ' (voir "{}")'.format(self._label)
def begin_html(self):
#name = 'label-' + self._label
name = self._label if _v42compat else 'label-' + self._label

View File

@ -1,5 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from ..base import *
from html import escape as _htmlescape
@ -46,9 +45,6 @@ class TextoutLinkTag(TextoutInlineTag):
self._url = content
self._validate()
def end_text(self):
return ' (voir "{}")'.format(self._url)
def begin_html(self):
return '<a href="{}" target="_blank" rel="noopener">' \
.format(_htmlescape(self._url))
@ -56,6 +52,13 @@ class TextoutLinkTag(TextoutInlineTag):
def end_html(self):
return '</a>'
def begin_lightscript(self):
return '['
def end_lightscript(self):
url = self._url.replace('(', '%28').replace(')', '%29')
return ']({})'.format(url)
class TextoutProfileTag(TextoutLinkTag):
""" A special link tag for Planète Casio's profiles.
Adds the prefix to the content, and sets the value.

View File

@ -1,5 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from ..base import *
@ -20,10 +19,6 @@ class TextoutProgressTag(TextoutBlockTag):
if self._val < 0 or self._val > 100:
raise Exception("progress value should be between 0 and 100 incl.")
def end_text(self):
val = self._val / 5
return '\n|{}|\n'.format('X' * val + '-' * (20 - val))
def begin_html(self):
return '<div>'

View File

@ -1,5 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from ..base import *
from html import escape as _htmlescape
@ -19,21 +18,11 @@ class TextoutQuoteTag(TextoutBlockTag):
"""
aliases = ('[quote]',)
superblock = True
def prepare(self, name, value):
self._value = value
def preprocess_text(self, content):
self._content = content
def begin_text(self):
if not self._value:
return ''
return '{} a écrit:\n'.format(self._value)
def content_text(self):
return "> " + '\n> '.join(self._content.split('\n'))
def begin_html(self):
f = '<div class="citation">'
if self._value:
@ -44,4 +33,13 @@ class TextoutQuoteTag(TextoutBlockTag):
def end_html(self):
return '</div>'
def begin_lightscript(self):
text = '<<<'
if self._value:
text += ' ' + self._value
return text + '\n'
def end_lightscript(self):
return '<<<\n'
# End of file.

View File

@ -1,5 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from ..base import *
import string as _string

View File

@ -1,5 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from ..base import *
from html import escape as _htmlescape
@ -14,6 +13,7 @@ class TextoutShowTag(TextoutBlockTag):
"""
aliases = ('[show]',)
superblock = True
generic = False
raw = False

View File

@ -1,5 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from ..base import *
from html import escape as _htmlescape
@ -19,6 +18,7 @@ class TextoutSpoilerTag(TextoutBlockTag):
"""
aliases = ('[spoiler]',)
superblock = True
def prepare(self, name, value):
self._closed = "Cliquez pour découvrir"
@ -31,15 +31,6 @@ class TextoutSpoilerTag(TextoutBlockTag):
if len(titles) >= 2 and titles[1]:
self._open = titles[1]
def begin_text(self):
return self._closed + "\n"
def preprocess_text(self, content):
self._content = content
def content_text(self):
return "| " + "\n| ".join(self._content.split('\n'))
def begin_html(self):
return '<div class="spoiler">' \
'<div class="title on" onclick="toggleSpoiler(this.parentNode, ' \

View File

@ -1,8 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from ..base import *
from .__color__ import *
from ...color import get_color
__all__ = ["TextoutTextTag"]

View File

@ -1,5 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from ..base import *
@ -19,13 +18,13 @@ class TextoutTitleTag(TextoutBlockTag):
def prepare(self, name, value):
self._level = name[1:-1]
def begin_text(self):
return ('## ', '# ')[self._level == "title"]
def begin_html(self):
return ('<h5>', '<h4>')[self._level == "title"]
def end_html(self):
return ('</h5>', '</h4>')[self._level == "title"]
def begin_lightscript(self):
return '#' * ((self._level == "subtitle") + 1) + ' '
# End of file.

View File

@ -1,5 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from ..base import *
import re as _re
@ -63,6 +62,8 @@ class TextoutVideoTag(TextoutBlockTag):
raise Exception
def preprocess(self, content):
self._url = content
try:
self._getvideo(content)
except:
@ -70,7 +71,6 @@ class TextoutVideoTag(TextoutBlockTag):
if not url.scheme in ('http', 'https'):
raise Exception("No allowed prefix!")
self._type = None
self._url = content
def content_html(self):
""" Produce the embed code for the given type. """
@ -99,4 +99,8 @@ class TextoutVideoTag(TextoutBlockTag):
return code + '</div>'
def content_lightscript(self):
url = self._url.replace('[', '%5B').replace(']', '%5D')
return '[[image:{}]]'.format(url)
# End of file.

View File

@ -1,278 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
""" HTML/CSS like color parsing, mainly for the `[color]` tag.
Defines the `get_color()` function which returns an rgba value.
"""
import re as _re
import colorsys as _color
import math as _math
__all__ = ["get_color"]
# ---
# Color names and correspondances.
# ---
_colors = {
# Standard CSS color names.
'aliceblue': '#F0F8FF', 'antiquewhite': '#FAEBD7', 'aqua': '#00FFFF',
'aquamarine': '#7FFFD4', 'azure': '#F0FFFF', 'beige': '#F5F5DC',
'bisque': '#FFE4C4', 'black': '#000000', 'blanchedalmond': '#FFEBCD',
'blue': '#0000FF', 'blueviolet': '#8A2BE2', 'brown': '#A52A2A',
'burlywood': '#DEB887', 'cadetblue': '#5F9EA0', 'chartreuse': '#7FFF00',
'chocolate': '#D2691E', 'coral': '#FF7F50', 'cornflowerblue': '#6495ED',
'cornsilk': '#FFF8DC', 'crimson': '#DC143C', 'cyan': '#00FFFF',
'darkblue': '#00008B', 'darkcyan': '#008B8B', 'darkgoldenrod': '#B8860B',
'darkgray': '#A9A9A9', 'darkgrey': '#A9A9A9', 'darkgreen': '#006400',
'darkkhaki': '#BDB76B', 'darkmagenta': '#8B008B',
'darkolivegreen': '#556B2F', 'darkorange': '#FF8C00',
'darkorchid': '#9932CC', 'darkred': '#8B0000', 'darksalmon': '#E9967A',
'darkseagreen': '#8FBC8F', 'darkslateblue': '#483D8B',
'darkslategray': '#2F4F4F', 'darkslategrey': '#2F4F4F',
'darkturquoise': '#00CED1', 'darkviolet': '#9400D3',
'deeppink': '#FF1493', 'deepskyblue': '#00BFFF',
'dimgray': '#696969', 'dimgrey': '#696969', 'dodgerblue': '#1E90FF',
'firebrick': '#B22222', 'floralwhite': '#FFFAF0', 'forestgreen': '#228B22',
'fuchsia': '#FF00FF', 'gainsboro': '#DCDCDC', 'ghostwhite': '#F8F8FF',
'gold': '#FFD700', 'goldenrod': '#DAA520', 'gray': '#808080',
'grey': '#808080', 'green': '#008000', 'greenyellow': '#ADFF2F',
'honeydew': '#F0FFF0', 'hotpink': '#FF69B4', 'indianred': '#CD5C5C',
'indigo': '#4B0082', 'ivory': '#FFFFF0', 'khaki': '#F0E68C',
'lavender': '#E6E6FA', 'lavenderblush': '#FFF0F5', 'lawngreen': '#7CFC00',
'lemonchiffon': '#FFFACD', 'lightblue': '#ADD8E6', 'lightcoral': '#F08080',
'lightcyan': '#E0FFFF', 'lightgoldenrodyellow': '#FAFAD2',
'lightgray': '#D3D3D3', 'lightgrey': '#D3D3D3', 'lightgreen': '#90EE90',
'lightpink': '#FFB6C1', 'lightsalmon': '#FFA07A',
'lightseagreen': '#20B2AA', 'lightskyblue': '#87CEFA',
'lightslategray': '#778899', 'lightslategrey': '#778899',
'lightsteelblue': '#B0C4DE', 'lightyellow': '#FFFFE0',
'lime': '#00FF00', 'limegreen': '#32CD32', 'linen': '#FAF0E6',
'magenta': '#FF00FF', 'maroon': '#800000', 'mediumaquamarine': '#66CDAA',
'mediumblue': '#0000CD', 'mediumorchid': '#BA55D3',
'mediumpurple': '#9370DB', 'mediumseagreen': '#3CB371',
'mediumslateblue': '#7B68EE', 'mediumspringgreen': '#00FA9A',
'mediumturquoise': '#48D1CC', 'mediumvioletred': '#C71585',
'midnightblue': '#191970', 'mintcream': '#F5FFFA', 'mistyrose': '#FFE4E1',
'moccasin': '#FFE4B5', 'navajowhite': '#FFDEAD', 'navy': '#000080',
'oldlace': '#FDF5E6', 'olive': '#808000', 'olivedrab': '#6B8E23',
'orange': '#FFA500', 'olivedrab': '#6B8E23', 'orange': '#FFA500',
'orangered': '#FF4500', 'orchid': '#DA70D6', 'palegoldenrod': '#EEE8AA',
'palegreen': '#98FB98', 'paleturquoise': '#AFEEEE',
'palevioletred': '#DB7093', 'papayawhip': '#FFEFD5',
'peachpuff': '#FFDAB9', 'peru': '#CD853F', 'pink': '#FFC0CB',
'plum': '#DDA0DD', 'powderblue': 'B0E0E6', 'purple': '#800080',
'rebeccapurple': '#663399', 'red': '#FF0000', 'rosybrown': '#BC8F8F',
'royalblue': '#4169E1', 'saddlebrown': '#8B4513', 'salmon': '#FA8072',
'sandybrown': '#F4A460', 'seagreen': '#2E8B57', 'seashell': '#FFF5EE',
'sienna': '#A0522D', 'silver': '#C0C0C0', 'skyblue': '#87CEEB',
'slateblue': '#6A5ACD', 'slategray': '#708090', 'slategrey': '#708090',
'snow': '#FFFAFA', 'springgreen': '#00FF7F', 'steelblue': '#4682B4',
'tan': '#D2B48C', 'teal': '#008080', 'thistle': '#D8BFD8',
'tomato': '#FF6347', 'turquoise': '#40E0D0', 'violet': '#EE82EE',
'wheat': '#F5DEB3', 'white': '#FFFFFF', 'whitesmoke': '#F5F5F5',
'yellow': '#FFFF00', 'yellowgreen': '#9ACD32',
# Planète Casio special colors.
'transparent': 'rgba(0, 0, 0, 0)',
}
_cr = _re.compile(''
'rgba?\s*\('
'\s*' '(?P<rgb_r>[0-9]{1,3})' '\s*,'
'\s*' '(?P<rgb_g>[0-9]{1,3})' '\s*,'
'\s*' '(?P<rgb_b>[0-9]{1,3})' '\s*(,'
'\s*' '((?P<rgb_a_per>0*[0-9]{0,3}(\.[0-9]*)?)%'
'|(?P<rgb_a_flt>0*(\.[0-9]*)?))' '\s*)?'
'\)'
'|hsla?\s*\('
'\s*' '(?P<hsl_hue>-?0*[0-9]{1,3}(\.[0-9]*)?)'
'(?P<hsl_agl>deg|grad|rad|turn|)' '\s*[,\\s]'
'\s*' '((?P<hsl_sat_per>0*[0-9]{1,3}(\.[0-9]*)?)%'
'|(?P<hsl_sat_flt>0*(\.[0-9]*)?))' '\s*[,\\s]'
'\s*' '((?P<hsl_lgt_per>0*[0-9]{1,3}(\.[0-9]*)?)%'
'|(?P<hsl_lgt_flt>0*(\.[0-9]*)?))' '\s*([,\\s]'
'\s*' '((?P<hsl_aph_per>0*[0-9]{0,3}(\.[0-9]*)?)%'
'|(?P<hsl_aph_flt>0*(\.[0-9]*)?))' '\s*)?'
'\)'
'|hlsa?\s*\('
'\s*' '(?P<hls_hue>-?0*[0-9]{1,3}(\.[0-9]*)?)'
'(?P<hls_agl>deg|grad|rad|turn|)' '\s*[,\\s]'
'\s*' '((?P<hls_lgt_per>0*[0-9]{1,3}(\.[0-9]*)?)%'
'|(?P<hls_lgt_flt>0*(\.[0-9]*)?))' '\s*[,\\s]'
'\s*' '((?P<hls_sat_per>0*[0-9]{1,3}(\.[0-9]*)?)%'
'|(?P<hls_sat_flt>0*(\.[0-9]*)?))' '\s*([,\\s]'
'\s*' '((?P<hls_aph_per>0*[0-9]{0,3}(\.[0-9]*)?)%'
'|(?P<hls_aph_flt>0*(\.[0-9]*)?))' '\s*)?'
'\)'
'|hwb\s*\('
'\s*' '(?P<hwb_hue>-?0*[0-9]{1,3}(\.[0-9]*)?)'
'(?P<hwb_agl>deg|grad|rad|turn|)' '\s*[,\\s]'
'\s*' '((?P<hwb_wht_per>0*[0-9]{1,3})%'
'|(?P<hwb_wht_flt>0*(\.[0-9]*)?))' '\s*[,\\s]'
'\s*' '((?P<hwb_blk_per>0*[0-9]{1,3})%'
'|(?P<hwb_blk_flt>0*(\.[0-9]*)?))' '\s*'
'\)'
'|(?P<hex_hash>\#?)'
'(?P<hex_digits>[0-9a-z]*)'
'', _re.I)
# ---
# Utilitaires.
# ---
def _hwb_to_rgb(h, w, b):
""" Convert HWB to RGB color.
https://drafts.csswg.org/css-color/#hwb-to-rgb """
r, g, b = _color.hls_to_rgb(h, 0.5, 1.0)
f = lambda x: x * (1 - w - b) + w
return f(r), f(g), f(b)
# ---
# Main function.
# ---
def get_color(value):
""" Get a color from a string.
Returns an (r, g, b, a) color.
Raises an exception if there's a problem. """
# Check if is a color name.
try: value = _colors[value.lower()]
except: pass
# Initialize the alpha.
alpha = 1.0
# Get the match.
match = _cr.match(value).groupdict()
if match['hex_digits']:
# Imitate the Netscape behaviour. Find more about this here:
# https://stackoverflow.com/a/8333464
hx = match['hex_digits'].lower()
hx = ''.join(c if c in '0123456789abcdef' \
else ('0', '00')[ord(c) > 0xFFFF] for c in hx)[:128]
if match['hex_hash'] and len(hx) == 3:
hx = hx[0] * 2 + hx[1] * 2 + hx[2] * 2
iv = _math.ceil(len(hx) / 3)
of = iv - 8 if iv > 8 else 0
sz = iv - of
gr = list(map(lambda i: hx[i * iv + of:i * iv + iv] \
.ljust(sz, '0'), range(3)))
pre = min(map(lambda x: len(x) - len(x.lstrip('0')), gr))
pre = min(pre, sz - 2)
r, g, b = map(lambda x: int('0' + x[pre:pre + 2], 16), gr)
elif match['rgb_r']:
r = int(match['rgb_r'])
g = int(match['rgb_g'])
b = int(match['rgb_b'])
# Alpha value.
if match['rgb_a_per']:
alpha = float(match['rgb_a_per']) / 100.0
elif match['rgb_a_flt']:
alpha = float(match['rgb_a_flt'])
elif match['hsl_hue'] or match['hls_hue']:
if match['hsl_hue']:
hue = float(match['hsl_hue'])
agl = match['hsl_agl']
# Saturation.
if match['hsl_sat_per']:
sat = float(match['hsl_sat_per']) / 100.0
else:
sat = float(match['hsl_sat_flt'])
if sat > 1.0:
sat /= 100.0
# Light.
if match['hsl_lgt_per']:
lgt = float(match['hsl_lgt_per']) / 100.0
else:
lgt = float(match['hsl_lgt_flt'])
if lgt > 1.0:
lgt /= 100.0
# Alpha value.
if match['hsl_aph_per']:
alpha = float(match['hsl_aph_per']) / 100.0
elif match['hsl_aph_flt']:
alpha = float(match['hsl_aph_flt'])
else:
hue = float(match['hls_hue'])
agl = match['hls_agl']
# Saturation.
if match['hls_sat_per']:
sat = float(match['hls_sat_per']) / 100.0
else:
sat = float(match['hls_sat_flt'])
# Light.
if match['hls_lgt_per']:
lgt = float(match['hls_lgt_per']) / 100.0
else:
lgt = float(match['hls_lgt_flt'])
# Alpha value.
if match['hls_aph_per']:
alpha = float(match['hls_aph_per']) / 100.0
elif match['hls_aph_flt']:
alpha = float(match['hls_aph_flt'])
# Prepare the angle.
if agl == 'grad':
hue = hue * 400.0
elif agl == 'rad':
hue = hue / (2 * math.pi)
elif not agl or agl == 'deg':
hue = hue / 360.0
hue = hue % 1.0
if sat > 1 or lgt > 1:
raise Exception
r, g, b = _color.hls_to_rgb(hue, lgt, sat)
r, g, b = map(lambda x:int(round(x * 255)), (r, g, b))
elif match['hwb_hue']:
hue = float(match['hwb_hue'])
agl = match['hwb_agl']
# Saturation.
if match['hwb_wht_per']:
wht = float(match['hwb_wht_per']) / 100.0
else:
wht = float(match['hwb_wht_flt'])
# Light.
if match['hwb_blk_per']:
blk = float(match['hwb_blk_per']) / 100.0
else:
blk = float(match['hwb_blk_flt'])
# Prepare the angle.
if agl == 'grad':
hue = hue * 400.0
elif agl == 'rad':
hue = hue / (2 * math.pi)
elif not agl or agl == 'deg':
hue = hue / 360.0
hue = hue % 1.0
if wht > 1 or blk > 1:
raise Exception
r, g, b = _hwb_to_rgb(hue, wht, blk)
r, g, b = map(lambda x: int(round(x * 255)), (r, g, b))
if r < 0 or r > 255 or g < 0 or g > 255 or b < 0 or b > 255:
raise Exception
if alpha < 0.0 or alpha > 1.0:
raise Exception
return (r, g, b, alpha)
# End of file.

View File

@ -1,5 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
""" Built-in tags for the `textoutpc` module.
Some of these tags will probably have to move to a separate module
Planète Casio-specific, but still, here we are.

View File

@ -1,17 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from .base import *
__all__ = ["TextoutParagraphTag"]
class TextoutParagraphTag(TextoutBlockTag):
""" Main tag for basic paragraphs. """
def begin_html(self):
return '<p>'
def end_html(self):
return '</p>'
# End of file.

View File

@ -1,10 +1,10 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
""" Main translation function.
See the `Translator` class documentation for more information.
"""
import regex as _re
from copy import deepcopy as _deepcopy
from html import escape as _htmlescape
from .tags import TextoutInlineTag, TextoutBlockTag, \
TextoutParagraphTag, get_tag
@ -19,6 +19,9 @@ __all__ = ["Translator"]
# ---
class _TagData:
BLOCK = 1
INLINE = 2
def __init__(self, tag, name, full):
""" Tag data initialization.
Here, we prepare all of the attributes from the tag's
@ -28,15 +31,28 @@ class _TagData:
# `full` is the full tag beginning mark.
self.name = name
self.type = self.BLOCK if isinstance(tag, TextoutBlockTag) \
else self.INLINE
self.full = full
# Tag beginning displaying.
# `notempty` is the moment when (and if) to start displaying the
# tag's code and content.
# `started` is whether the tag's beginning has been processed,
# i.e. if the content is no longer processed.
self.notempty = bool(tag.notempty) if hasattr(tag, 'notempty') \
else False
self.started = False
# `tag` is the actual tag object returned by `get_tag()`.
# XXX: because the tag is going to be called with different contents,
# we might as well do deepcopies from a `self.base` each time we
# want to start a new instance. But while this is not worked on,
# `self.tag` will be enough.
self.tag = tag
self.base = tag
self.tag = _deepcopy(tag)
# Flags and properties calculated from the tag's attributes, using the
# rules given in `TAGS.md`.
@ -55,8 +71,8 @@ class _TagData:
self.raw = bool(tag.raw) if hasattr(tag, 'raw') \
else hasattr(tag, 'preprocess')
self.notempty = bool(tag.notempty) if hasattr(tag, 'notempty') \
else not self.ign and hasattr(tag, 'default')
self.super = bool(tag.superblock) if hasattr(tag, 'superblock') \
else False
# Content processing utilities.
# `last` is the content of the tag. A boolean indicates that we
@ -118,6 +134,10 @@ class Translator:
self.raw_mode = False
self.raw_deg = 0
# ---
# Text and code outputting utilities.
# ---
def process_text_group(self):
""" Process text groups for naked URLs and stuff. """
@ -148,17 +168,11 @@ class Translator:
if not code or self.cign > 0:
return
# If we ought to put code, that means that the paragraph content
# is starting and that we might have to put the start of paragraph.
self.start_block()
# The last queue is composed of booleans (does the group contain
# something or not) and texts.
# As in `flush_text()`, the last queue is composed of booleans.
# We want to set all of the booleans to True until the first text
# group, to which we want to add the current text. We mustn't set
# the booleans after the first text as the content might be replaced
# after that point!
# group, to which we want to add the current text.
# If there is no content preprocessing and we have to output it,
# we want to start the tags first: `dat == None` will be our signal!
for dat in self.queue:
if isinstance(dat.last, bool):
@ -167,6 +181,15 @@ class Translator:
dat.last += code
break
else:
dat = None
# Start the tags that haven't been started, and stuff.
self.start_tags()
# If the content has to be written, we ought to.
if dat == None:
self.outp.write(code)
def put_newline(self):
@ -211,17 +234,12 @@ class Translator:
if not self.text_group or self.cign > 0:
return
# If we ought to put text, that means that the paragraph content
# is starting and that we might have to put the start of paragraph.
self.start_block()
# The last queue is composed of booleans (does the group contain
# something or not) and texts.
# something or not) and texts for content processing.
# We want to set all of the booleans to True until the first text
# group, to which we want to add the current text. We mustn't set
# the booleans after the first text as the content might be replaced
# after that point!
# group, to which we want to add the current text.
# If there is no content preprocessing and we have to output it,
# we want to start the tags first: `dat == None` will be our signal!
for dat in self.queue:
if isinstance(dat.last, bool):
@ -230,6 +248,15 @@ class Translator:
dat.last += self.text_group
break
else:
dat = None
# Start the tags that haven't been started, and stuff.
self.start_tags()
# If the content has to be written, we ought to.
if dat == None:
self.outp.write(self.process_text_group())
# Don't forget to reset the `text_group`, as its content has been
@ -237,33 +264,9 @@ class Translator:
self.text_group = ""
def start_block(self):
""" Start the block paragraph if not started. """
return # TODO
# First of all, we ought to check the `opened_group` member to
# check if the paragraph has started or not.
if self.opened_group:
return
self.opened_group = True
# TODO: other things
def end_block(self):
""" End the block paragraph if not ended. """
return # TODO
# First of all, we ought to check the `opened_group` member to
# check if the paragraph has ended or not.
if not self.opened_group:
return
self.opened_group = False
# TODO: other things
# ---
# Tag queue management.
# ---
def push_tag(self, dat):
""" Push a tag onto the tag stack. """
@ -276,16 +279,22 @@ class Translator:
if dat.ign:
self.cign += 1
# If there is no content processing, let's put the beginning as soon
# as we can, which means right now.
# If it is a block, end the current block.
if not hasattr(tag, 'preprocess') and hasattr(tag, 'begin'):
self.put_code(tag.begin())
if dat.type == dat.BLOCK:
self.end_block()
# Insert the tag into the queue.
self.queue.insert(0, dat)
# Start the tag (and parent tags) if required.
self.start_tags()
# Don't forget to add the tag to the queue, and to enable raw
# mode if the tag expects a raw content (e.g. `[code]`).
self.queue.insert(0, dat)
if dat.raw:
self.raw_mode = True
self.raw_deg = 0
@ -355,7 +364,9 @@ class Translator:
# just put the content that we got earlier.
if hasattr(tag, 'begin'):
dat.started = True
self.put_code(tag.begin())
if hasattr(tag, 'content'):
self.put_code(tag.content())
elif dat.raw:
@ -385,15 +396,13 @@ class Translator:
# Let's put the raw things again as when there is
# content processing.
self.put_text(tag._full)
self.put_text(dat.full)
self.put_text(end)
return
# Don't forget to put the end of the tag, as well.
# That's for when there is content processing or not.
# Don't forget to end the tag!
if hasattr(tag, 'end'):
self.put_code(tag.end())
self.end_last_tag()
# Disable raw mode if it was a raw tag (which means that it enabled it,
# as tags into raw tags cannot be processed).
@ -401,8 +410,111 @@ class Translator:
if dat.raw:
self.raw_mode = False
# ---
# Automatically start and end tags.
# ---
def start_tags(self):
""" Start the tags that haven't been started yet.
This is usually called when content is output, for tags that
aren't empty. """
# First, get the references to the block and inline tags that need
# to be started.
blocks = []
inlines = []
for dat in self.queue:
# Check if the tag hasn't already been started or doesn't call
# for content processing.
if type(dat.last) != bool: break
if dat.notempty and not dat.last: break
if dat.started: continue
# Then put the tag in the appropriate queue, and set it as
# started for methods as `put_code()` that call this method
# back not to re-put anything.
dat.started = True
if dat.type == dat.BLOCK:
blocks.insert(0, dat)
else:
inlines.insert(0, dat)
# Then, put the tag beginnings.
for dat in blocks + inlines:
dat.started = True
self.put_code(dat.tag.begin())
def end_block(self):
""" End the current block. """
# We want to collect inline and block tags, in the order they
# were inserted, reversed.
blocks = []
inlines = []
for dat in self.queue:
# Check if the tag has been started and if it is a super
# block (which means we want to stop here).
if not isinstance(dat.last, bool) or dat.super: break
# Then put the tag in the appropriate queue, and set it as
# unstarted for safety reasons.
dat.started = False
if dat.type == dat.BLOCK:
blocks.append(dat)
else:
inlines.append(dat)
# Then we want to end the tags, and reset them in case we're going
# to use them.
for dat in inlines + blocks:
tag = dat.tag
if hasattr(tag, 'end'):
self.put_code(tag.end())
dat.tag = _deepcopy(dat.base)
dat.started = False
dat.last = False
def end_last_tag(self):
""" End the latest tag entered in the queue. """
if not self.queue:
return
# If the tag hasn't been started, then it shouldn't be ended.
dat = self.queue[0]
if not dat.started:
return
# If it is not a block, then we should just end it like that.
if dat.type != dat.BLOCK:
dat.started = False
tag = dat.tag
if hasattr(tag, 'end'):
self.put_code(tag.end())
return
# If we have arrived there, then the tag to end is a block.
# We want to end the whole current block.
self.end_block()
# ---
# Main function.
# ---
def process(self):
""" Main function of the translator. """
""" Main function of the textout translator. """
# By default, everything is in a paragraph.
# Other blocks will supplant this by being further in the queue.

View File

@ -1,17 +1,16 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
""" Autolinking (URL extraction from raw text) in HTML. """
import regex as _re
from html import escape as _htmlescape
__all__ = ["htmlurls"]
__all__ = ["htmlurls", "lightscripturls"]
# ---
# Autolinking regex.
# ---
def _sub(m):
def _sub_html(m):
sp = m.group('sp')
url = m.group('url')
aft = ''
@ -24,6 +23,20 @@ def _sub(m):
.format(sp, url, url, aft)
return text
def _sub_lightscript(m):
sp = m.group('sp')
url = m.group('url')
aft = ''
# Hack for the last comma.
if url[-1] == ',':
url, aft = url[:-1], ','
url = url.replace('<', '%3C')
url = url.replace('>', '%3E')
text = '{}<{}>{}'.format(sp, url, aft)
return text
_reg = _re.compile("""\
(?P<sp>^|\s|[[:punct:]])
(?P<url>(https?|ftp):
@ -32,10 +45,13 @@ _reg = _re.compile("""\
""", _re.VERBOSE | _re.M)
# ---
# Main function.
# Main functions.
# ---
def htmlurls(text):
return _reg.sub(_sub, text)
return _reg.sub(_sub_html, text)
def lightscripturls(text):
return _reg.sub(_sub_lightscript, text)
# End of file.