2
0
Fork 0

Compare commits

...

47 Commits

Author SHA1 Message Date
Thomas Touhey d0f9f0ae83
Yet another commit to test mirroring. 2019-06-25 14:57:15 +02:00
Thomas Touhey 9d3ca92b81
Sample commit (to test mirroring). 2019-06-25 14:54:39 +02:00
Thomas Touhey c767082461
Sample edit 2019-06-25 14:51:27 +02:00
Thomas Touhey f1f1be481a
Latest changes (and mirror testing) 2019-06-25 14:49:22 +02:00
Thomas Touhey 0df1699c27
Made the textoutpc.color an independant module: thcolor. 2019-05-08 14:02:46 +02:00
Thomas Touhey 55946468fd
Random things, still did not correct the issue. 2019-03-15 22:06:01 +01:00
Thomas Touhey b1c0968494
Sorta corrected a tiny bug 2019-02-11 21:44:39 +01:00
Thomas Touhey 4dd686e20a
Tiny fixes to docs 2019-02-11 19:26:46 +01:00
Thomas Touhey 5076d0b800
Found a non-functional test. 2019-02-11 17:48:01 +01:00
Thomas Touhey 774d427fda
Fixed space in list. 2018-11-02 12:32:35 +01:00
Thomas Touhey fb75b482ba
Corrected list tag. 2018-11-02 10:46:53 +01:00
Thomas Touhey d1c6d09fc1
Started implementing lists with required attributes, doesn't work yet… 2018-11-02 00:19:58 +01:00
Thomas Touhey d39a356194
Bumped version to 0.2.1 2018-11-01 23:45:36 +01:00
Thomas Touhey 2ba11bc6f6
Corrected bugs with superblocks and raw management. 2018-11-01 23:40:50 +01:00
Thomas Touhey 516726c447
Mainly added 'checkdocs' and updated Pipfile. 2018-09-11 02:20:08 +02:00
Thomas Touhey dcb8611e37
Added a similar system for images to videos 2018-09-10 16:45:17 +02:00
Thomas Touhey dd19f7cf20
Continued, not good for 0.3 yet. 2018-08-25 20:28:05 +02:00
Thomas Touhey ea98e48a15
Corn flake8 2018-08-25 17:38:24 +02:00
Thomas Touhey 4774189495
Corrected docs building. 2018-07-30 15:20:25 +02:00
Thomas Touhey 30f5dd7fd6
Corrected development environment. 2018-07-30 15:12:54 +02:00
Thomas Touhey 261f9eb486
Using Pipfile now, it adds colors. 2018-07-30 14:51:38 +02:00
Thomas Touhey ec66264d81
Corrected video ratio management (once again). 2018-07-30 14:34:58 +02:00
Thomas Touhey 638e5c9195
Added OpenWebVideo as a demonstration. 2018-07-30 14:29:17 +02:00
Thomas Touhey d061fabfd8
Added a few tags. 2018-07-30 14:03:40 +02:00
Thomas Touhey 8459e04f29
Corrected ratio management for video tag (x2). 2018-07-29 22:16:39 +02:00
Thomas Touhey 5e9790b613
Corrected ratio management. 2018-07-29 22:14:53 +02:00
Thomas Touhey 8f7732a07f
Modified the way videos work in textoutpc. 2018-07-29 21:55:18 +02:00
Thomas Touhey 1f80dd6772
Dumped version to 0.2 (bug corrections, added inline mode) 2018-07-29 20:36:59 +02:00
Thomas Touhey 64c49b0ace
Added inline mode. 2018-07-29 20:34:56 +02:00
Thomas Touhey 5acbc428cd
Corrected a few things about the tags. 2018-07-29 19:51:42 +02:00
Thomas Touhey bb36d42a54
Simplified options extraction. 2018-07-28 21:15:49 +02:00
Thomas Touhey a6d6f62d11
Corrected smiley parsing. 2018-07-28 20:08:06 +02:00
Thomas Touhey 3b3b54312a
Removed those 'Textout' prefixes in builtin module, corrected some more things. 2018-07-28 19:49:29 +02:00
Thomas Touhey 8d9d4360ed
Better smiley implementation. 2018-07-28 19:36:43 +02:00
Thomas Touhey a2fdc5d5a0
Updated the README. 2018-07-27 02:30:28 +02:00
Thomas Touhey 41fd882e19
Updated docs and project structure, dumped to 0.1.1 2018-07-27 02:25:38 +02:00
Thomas Touhey 9a4131b074
Updated the metadata. 2018-07-27 02:01:26 +02:00
Thomas Touhey 902a23e23e
Updated the TODO list. 2018-07-27 01:17:52 +02:00
Thomas Touhey 4b47e63fff
0 fails! 2018-07-27 00:53:59 +02:00
Thomas Touhey f162d1b94c
Corrected the [quote][show][justify]hehe problem, still problems to go 2018-07-26 23:40:19 +02:00
Thomas Touhey f4ee519eb3
Updated docs. 2018-07-26 20:33:43 +02:00
Thomas Touhey 3cdd2b4bc1
Works better. Not perfectly, just better. 2018-06-22 02:43:31 +02:00
Thomas Touhey cfed4cc7de
Going forward! 2018-06-22 01:35:41 +02:00
Thomas Touhey d9e6f27979
It works far better! 2018-06-21 02:26:07 +02:00
Thomas Touhey 167438a97f
Having *other* bugs now. 2018-06-21 00:43:03 +02:00
Thomas Touhey 5920a0860d
Added a tag loader class. 2018-05-24 21:57:42 +02:00
Thomas Touhey 2c2ce72719
Fixed a buffer problem, still a queue problem to fix. 2018-05-24 14:43:17 +02:00
63 changed files with 2964 additions and 1701 deletions

4
.gitignore vendored
View File

@ -3,3 +3,7 @@ __pycache__
/*.egg-info
/dist
/.spyproject
/build
/docs/_build
/venv
/README.html

1
.python-version Normal file
View File

@ -0,0 +1 @@
3.7.2

View File

@ -1,5 +1,6 @@
# The MIT License (MIT)
Copyright (C) 2018 Thomas Touhey <<thomas@touhey.fr>>
The MIT License (MIT)
Copyright (C) 2018-2019 Thomas Touhey <thomas@touhey.fr>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal

10
MANIFEST.in Normal file
View File

@ -0,0 +1,10 @@
include README.rst
include LICENSE.txt
include MANIFEST.in
include setup.py
include setup.cfg
include docs/*.rst
include docs/conf.py
include docs/Makefile
include docs/make.bat

View File

@ -1,9 +1,21 @@
#!/usr/bin/make -f
ST := ./setup.py
PE := pipenv run
ST := $(PE) ./setup.py
DNAME := dist/$(shell $(ST) --name)-$(shell $(ST) --version).tar.gz
test tests:
@$(ST) test
@$(PE) pytest -s -q
prepare:
@pipenv install --dev
update:
@pipenv update --dev
docs:
@$(ST) build_sphinx
checkdocs:
@$(ST) checkdocs
dist: $(DNAME)
$(DNAME):
@ -12,5 +24,5 @@ $(DNAME):
upload: $(DNAME)
@twine upload $(DNAME)
.PHONY: test tests dist
.PHONY: test tests dist docs
# End of file.

17
Pipfile Normal file
View File

@ -0,0 +1,17 @@
[[source]]
url = 'https://pypi.python.org/simple'
verify_ssl = true
name = 'pypi'
[requires]
python_version = '3.7'
[packages]
regex = '*'
thcolor = '*'
[dev-packages]
sphinx = '*'
"collective.checkdocs" = '*'
pudb = '*'
pytest = '*'

311
Pipfile.lock generated Normal file
View File

@ -0,0 +1,311 @@
{
"_meta": {
"hash": {
"sha256": "f2cea3a09ae48de290ab5aa3af45cedaf158f9586a8f84c50c1825de25732522"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.7"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.python.org/simple",
"verify_ssl": true
}
]
},
"default": {
"regex": {
"hashes": [
"sha256:020429dcf9b76cc7648a99c81b3a70154e45afebc81e0b85364457fe83b525e4",
"sha256:0552802b1c3f3c7e4fee8c85e904a13c48226020aa1a0593246888a1ac55aaaf",
"sha256:308965a80b92e1fec263ac1e4f1094317809a72bc4d26be2ec8a5fd026301175",
"sha256:4d627feef04eb626397aa7bdec772774f53d63a1dc7cc5ee4d1bd2786a769d19",
"sha256:93d1f9fcb1d25e0b4bd622eeba95b080262e7f8f55e5b43c76b8a5677e67334c",
"sha256:c3859bbf29b1345d694f069ddfe53d6907b0393fda5e3794c800ad02902d78e9",
"sha256:d56ce4c7b1a189094b9bee3b81c4aeb3f1ba3e375e91627ec8561b6ab483d0a8",
"sha256:ebc5ef4e10fa3312fa1967dc0a894e6bd985a046768171f042ac3974fadc9680",
"sha256:f9cd39066048066a4abe4c18fb213bc541339728005e72263f023742fb912585"
],
"index": "pypi",
"version": "==2019.4.14"
},
"thcolor": {
"hashes": [
"sha256:a93a535f7f81b5e38460531b5731f0fa8c4b30b2a63d1fe763c2ac15e37df8b7"
],
"index": "pypi",
"version": "==0.3.1"
}
},
"develop": {
"alabaster": {
"hashes": [
"sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359",
"sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"
],
"version": "==0.7.12"
},
"atomicwrites": {
"hashes": [
"sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
"sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
],
"version": "==1.3.0"
},
"attrs": {
"hashes": [
"sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79",
"sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"
],
"version": "==19.1.0"
},
"babel": {
"hashes": [
"sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669",
"sha256:8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23"
],
"version": "==2.6.0"
},
"certifi": {
"hashes": [
"sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5",
"sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae"
],
"version": "==2019.3.9"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"collective.checkdocs": {
"hashes": [
"sha256:3a5328257c5224bc72753820c182910d7fb336bc1dba5e09113d48566655e46e"
],
"index": "pypi",
"version": "==0.2"
},
"docutils": {
"hashes": [
"sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6",
"sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274",
"sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"
],
"version": "==0.14"
},
"idna": {
"hashes": [
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
],
"version": "==2.8"
},
"imagesize": {
"hashes": [
"sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8",
"sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"
],
"version": "==1.1.0"
},
"jinja2": {
"hashes": [
"sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013",
"sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"
],
"version": "==2.10.1"
},
"markupsafe": {
"hashes": [
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"
],
"version": "==1.1.1"
},
"more-itertools": {
"hashes": [
"sha256:2112d2ca570bb7c3e53ea1a35cd5df42bb0fd10c45f0fb97178679c3c03d64c7",
"sha256:c3e4748ba1aad8dba30a4886b0b1a2004f9a863837b8654e7059eebf727afa5a"
],
"markers": "python_version > '2.7'",
"version": "==7.0.0"
},
"packaging": {
"hashes": [
"sha256:0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af",
"sha256:9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3"
],
"version": "==19.0"
},
"pluggy": {
"hashes": [
"sha256:25a1bc1d148c9a640211872b4ff859878d422bccb59c9965e04eed468a0aa180",
"sha256:964cedd2b27c492fbf0b7f58b3284a09cf7f99b0f715941fb24a439b3af1bd1a"
],
"version": "==0.11.0"
},
"pudb": {
"hashes": [
"sha256:ac30cfc64580958ab7265decb4cabb9141f08781ff072e9a336d5a7942ce35a6"
],
"index": "pypi",
"version": "==2019.1"
},
"py": {
"hashes": [
"sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa",
"sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"
],
"version": "==1.8.0"
},
"pygments": {
"hashes": [
"sha256:31cba6ffb739f099a85e243eff8cb717089fdd3c7300767d9fc34cb8e1b065f5",
"sha256:5ad302949b3c98dd73f8d9fcdc7e9cb592f120e32a18e23efd7f3dc51194472b"
],
"version": "==2.4.0"
},
"pyparsing": {
"hashes": [
"sha256:1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a",
"sha256:9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03"
],
"version": "==2.4.0"
},
"pytest": {
"hashes": [
"sha256:1a8aa4fa958f8f451ac5441f3ac130d9fc86ea38780dd2715e6d5c5882700b24",
"sha256:b8bf138592384bd4e87338cb0f256bf5f615398a649d4bd83915f0e4047a5ca6"
],
"index": "pypi",
"version": "==4.5.0"
},
"pytz": {
"hashes": [
"sha256:303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda",
"sha256:d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141"
],
"version": "==2019.1"
},
"requests": {
"hashes": [
"sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
"sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"
],
"version": "==2.21.0"
},
"six": {
"hashes": [
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
],
"version": "==1.12.0"
},
"snowballstemmer": {
"hashes": [
"sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128",
"sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89"
],
"version": "==1.2.1"
},
"sphinx": {
"hashes": [
"sha256:423280646fb37944dd3c85c58fb92a20d745793a9f6c511f59da82fa97cd404b",
"sha256:de930f42600a4fef993587633984cc5027dedba2464bcf00ddace26b40f8d9ce"
],
"index": "pypi",
"version": "==2.0.1"
},
"sphinxcontrib-applehelp": {
"hashes": [
"sha256:edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897",
"sha256:fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d"
],
"version": "==1.0.1"
},
"sphinxcontrib-devhelp": {
"hashes": [
"sha256:6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34",
"sha256:9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981"
],
"version": "==1.0.1"
},
"sphinxcontrib-htmlhelp": {
"hashes": [
"sha256:4670f99f8951bd78cd4ad2ab962f798f5618b17675c35c5ac3b2132a14ea8422",
"sha256:d4fd39a65a625c9df86d7fa8a2d9f3cd8299a3a4b15db63b50aac9e161d8eff7"
],
"version": "==1.0.2"
},
"sphinxcontrib-jsmath": {
"hashes": [
"sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178",
"sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"
],
"version": "==1.0.1"
},
"sphinxcontrib-qthelp": {
"hashes": [
"sha256:513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20",
"sha256:79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f"
],
"version": "==1.0.2"
},
"sphinxcontrib-serializinghtml": {
"hashes": [
"sha256:c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227",
"sha256:db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768"
],
"version": "==1.1.3"
},
"urllib3": {
"hashes": [
"sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4",
"sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb"
],
"version": "==1.24.3"
},
"urwid": {
"hashes": [
"sha256:644d3e3900867161a2fc9287a9762753d66bd194754679adb26aede559bcccbc"
],
"version": "==2.0.1"
},
"wcwidth": {
"hashes": [
"sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e",
"sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"
],
"version": "==0.1.7"
}
}
}

View File

@ -1,99 +1,34 @@
Planète Casio's textout() BBcode markup language translator
===========================================================
This module contains a BBcode to HTML translator for
`Planète Casio`_. For more information, read the
documentation accessible on `the official website`_.
.. warning::
If you are accessing this repository from <https://git.planet-casio.com>_,
If you are accessing this repository from another forge (such as
`Planète Casio's forge <https://gitea.planet-casio.com/cake/textout>`_),
keep in mind that it is only a mirror and that the real repository
is located at <https://forge.touhey.fr/pc/textout.git>_ for now.
is located `in my forge <https://forge.touhey.org/pc/textout.git>`_
for now.
BBcode has been invented in the 90s/2000s for bulletin board systems.
It has been implemented in `Planète Casio`_ during its first years (although
some research has to be made on how that choice was done…).
On `Planète Casio`_, which is coded in PHP at the time I'm writing this,
we have our own custom version of BBcode, which we pass through an internal
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 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.
-----
Usage
-----
To use this module, simply use the ``to<language>()`` functions once imported:
For example, if we want to translate some basic multi-block text to HTML:
.. code-block:: python
#!/usr/bin/env python3
import textoutpc
from textoutpc import tohtml
text = "Hello, [i]beautiful [b]world[/i]!"
print(textoutpc.tohtml(text))
print("---")
print(textoutpc.tolightscript(text))
text = """[img=center]https://www.planet-casio.com/skins/bootstrap/img/default/logo.png[/img]
The supported output types are:
Hello [color=R10]world[/color]!
[ul]
[*]This module is made by [url=https://thomas.touhey.fr/]Thomas Touhey[/url]!
[*]Use `.tohtml()` to translate magically to HTML!
[/]
"""
- ``html``: `HTML`_ compatible output, requiring some additional style and
script;
- ``lightscript``: `Lightscript`_ Markdown-like language. See
`the Lightscript topic on Planète Casio <Lightscript topic>`_ for
more information.
print(tohtml(text))
------
Tweaks
------
The ``tohtml()`` and ``tolightscript()`` can take additional keywords that
tags can read so that they can adapt their behaviour. The name of the tweaks
are case-insensitive and non-alphanumeric characters are ignored: for example,
``label_prefix``, ``LABELPREFIX`` and ``__LaBeL___PRE_FIX__`` are all
equivalent.
The following tweaks are read by the translator and built-in tags:
- ``label_prefix`` (HTML): prefix to be used by the ``[label]`` and
``[target]`` tags, e.g. ``msg45529-``. Defaults to `""` for PCv42
compatibility;
- ``obsolete_tags`` (HTML): use obsolete HTML tags for old browsers
(e.g. lynx) compatibility, e.g. ``<b>``, ``<i>``, ``<center>``, and
others. Defaults to ``True``.
An example call would be:
.. code-block:: python
#!/usr/bin/env python3
import textoutpc
print(textoutpc.tohtml("Hello, [i]beautiful[/i]!", obsolete__TAGS=False))
------------------
What is left to do
------------------
- Correct the translator until all the tests pass;
- 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:
* Count character offset, line number and column number in the lexer;
* Produce readable exceptions;
* Make a clean interface to transmit them;
- Check why exceptions on raw tags effectively escape the content, as it
shouldn't…?
- Look for security flaws (we really don't want stored XSS flaws!).
.. _Planète Casio: https://www.planet-casio.com/Fr/
.. _HTML: https://www.w3.org/html/
.. _Lightscript: https://git.planet-casio.com/lephe/lightscript
.. _Lightscript topic: https://planet-casio.com/Fr/forums/lecture_sujet.php?id=15022
.. _Planète Casio: https://www.planet-casio.com/
.. _the official website: https://textout.touhey.pro/

15
TODO.rst Normal file
View File

@ -0,0 +1,15 @@
What is left to do
==================
- Add an ``[imgurl]`` tag?
- Manage lightscript (or even markdown?) as output languages;
- Check where the errors are to display them to the user:
* Count character offset, line number and column number in the lexer;
* Produce readable exceptions;
* Make a clean interface to transmit them;
- Check why exceptions on raw tags effectively escape the content, as it
shouldn't…?
- Look for security flaws (we really don't want stored XSS flaws!).
- Implement match names (such as ``\[\*+\]`` for lists).
- Manage keywords with tags such as ``[tag key=value other="something else"]``.

20
docs/Makefile Normal file
View File

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SPHINXPROJ = textoutpc
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

0
docs/_static/.gitkeep vendored Normal file
View File

0
docs/_templates/.gitkeep vendored Normal file
View File

167
docs/conf.py Normal file
View File

@ -0,0 +1,167 @@
# -*- coding: utf-8 -*-
#
# Configuration file for the Sphinx documentation builder.
#
# This file does only contain a selection of the most common options. For a
# full list see the documentation:
# http://www.sphinx-doc.org/en/master/config
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
def _add_paths():
import os
import sys
sys.path.insert(0, os.path.abspath('..'))
_add_paths()
# -- Project information -----------------------------------------------------
project = 'textoutpc'
copyright = '2019, Thomas Touhey'
author = 'Thomas Touhey'
# The full version, including alpha/beta/rc tags
def _get_release():
from os.path import dirname, join
from pkg_resources import find_distributions as find_dist
module_path = join(dirname(__file__), '..')
dist = next(find_dist(module_path, True))
return dist.version
release = _get_release()
# -- General configuration ---------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc'
]
# Add any paths that contain templates here, relative to this directory.
templates_path = []
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path .
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '.goutput*']
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = []
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# The default sidebars (for documents that don't match any pattern) are
# defined by theme itself. Builtin themes are using these templates by
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
# 'searchbox.html']``.
#
# html_sidebars = {}
# -- Options for HTMLHelp output ---------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'textoutpcdoc'
# -- Options for LaTeX output ------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'textoutpc.tex', 'textoutpc Documentation',
'Thomas Touhey', 'manual'),
]
# -- Options for manual page output ------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'textoutpc', 'textoutpc Documentation',
[author], 1)
]
# -- Options for Texinfo output ----------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'textoutpc', 'textoutpc Documentation',
author, 'textoutpc', 'One line description of project.',
'Miscellaneous'),
]

31
docs/index.rst Normal file
View File

@ -0,0 +1,31 @@
Welcome to textoutpc's documentation!
=====================================
textoutpc is a BBcode markup language translator project for `Planète Casio`_.
BBcode has been invented in the 90s/2000s for bulletin board systems.
It has been implemented in `Planète Casio`_ during its first years (although
some research has to be made on how that choice was done…).
On `Planète Casio`_, which is coded in PHP at the time I'm writing this,
we have our own custom version of BBcode, which we pass through an internal
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 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.
.. toctree::
:maxdepth: 2
:caption: Contents:
language
usage
tags
.. _Planète Casio: https://www.planet-casio.com/Fr/

View File

@ -1,4 +1,4 @@
User's guide to the textout BBcode
The textout BBcode markup language
==================================
The BBcode markup language mainly uses tags, which the starting mark looks
@ -52,9 +52,10 @@ You can add some basic text style by using the following tags:
- ``[b]`` for **bold** text;
- ``[i]`` for *italic* text;
- ``[u]`` for underlined text;
- ``[s]`` or ``[strike]`` for :strike:`striked` text.
- ``[o]`` for overlined text;
- ``[s]`` or ``[strike]`` for striked text.
They can all be ended with the generic ending mark `[/]`.
They can all be ended with the generic ending mark ``[/]``.
----------------------
Changing the text font
@ -81,15 +82,16 @@ 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 <CSS named colors>`_) 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
0 to 255, the second group of hex digits represents the green component
from 0 to 255, and the third group of hex digits represents the blue
component from 0 to 255. Incomplete composites will be filled by zero
on the left (e.g. ``#0000F`` is equivalent to ``#00000F``), invalid
characters such as `A` will be replaced by `0`s;
- simple color names (`inspired from CSS <CSS named colors_>`_) such as
``red``, ``blue``, ``green``, ``transparent``;
- color hex codes using a hash (``#``) followed by hex digits, e.g.
``#01020F``, where the first group of hex digits represents the
red component from 0 to 255, the second group of hex digits represents
the green component from 0 to 255, and the third group of hex digits
represents the blue component from 0 to 255.
Incomplete composites will be filled by zero on the left (e.g. ``#0000F``
is equivalent to ``#00000F``), invalid characters such as ``A`` will be
replaced by 0s;
- three hex digits codes using ``#`` followed by three hex digits, e.g.
``#123`` which will be translated to ``#112233``;
- ``rgb(<red>, <green>, <blue>)``, where the red, green and blue components
@ -109,6 +111,7 @@ Here are some examples:
.. code::
[blue]I'm blue![/]
[color=#ff69b4]That color is called “Cuisse de Nymphe émue”![/]
[color=rgb(255, 255,255,0.4)]I'm black![/]
[color=hsl(0,100%, 0.5)]I'm red![/]
@ -131,14 +134,32 @@ Here are examples:
[url=https://planet-casio.com]Planète Casio[/]
[url=/relative/url.html][/]
To link to profiles, the ``[profil]`` and ``[profile]`` tags can be used. They
take no attribute but take a content which is the user whose the profile
For links to profiles, the ``[profil]`` and ``[profile]`` tags can be used.
They take no attribute but take a content which is the user whose the profile
is to be linked's name. For example:
.. code::
[profil]Cakeisalie5[/]
For links to topics and tutorials, the ``[topic]`` and ``[tutorial]``
tags can be used. They take no attribute but take a content which is the
identifier of the topic or tutorial to which to link to.
For example:
.. code::
[topic]234[/]
[tutorial]32[/]
For links to programs, the ``[program]`` and ``[prog]`` tags can be used.
They take no attribute but take a content which is the identifier of the
program to which to link to. For example:
.. code::
[program]3598[/program]
---------------
Quoting someone
---------------

36
docs/make.bat Normal file
View File

@ -0,0 +1,36 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
set SPHINXPROJ=textoutpc
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
:end
popd

View File

@ -1,5 +1,5 @@
Adding a tag in the Python port of Planète Casio's textout
==========================================================
Defining tags for textoutpc
===========================
A tag is a class defined in the ``textoutpc.Tags`` submodule and referenced
in the `_tags` array in ``textoutpc.Tags.__init__``. It usually takes a name
@ -16,25 +16,37 @@ There are a few public members you can define as a tag:
- ``aliases``: the array of names this tag can be accessed as.
Basic tags (the ones with brackets ``[]``) are defined as ``[<name>]``
(e.g. ``[hello]``) and special characters are defined with their symbol,
e.g. ``\```;
e.g. `````;
- ``raw``: the tag's content shall not be interpreted, which is generally
only useful when the content is preprocessed (see below). The default
is ``False`` if there is no preprocess method, and `True` otherwise;
- ``noinline``: for raw tags, forbid inline tags (declared outside of the
tag) within the tag (*only works with ``raw = True``*!);
- ``generic``: the tag can be ended using the generic tag ending mark ``[/]``.
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`;
- ``superblock``: is a super-block (for blocks), adds a paragraph
tag implicitely.
- ``superblock``: is a super-block (for blocks) which means it adds a block
level, and adds a paragraph tag implicitely;
- ``inlined``: if is a block, transforms automatically the surrounding block
into a superblock while it's there;
- ``procvalue``: process the value as normal text before passing it;
- ``not_within_itself``: make that if a tag is opened within itself (depth
included), the tag above and all tags below are closed first;
- ``only_in``: allow the tag to only be beneath certain tags;
- ``allowed_tags``: allowed tags right beneath the current one;
- ``no_text``: disable raw text within the tag;
- ``expect_child``: make that all content below (without depth) that isn't
within the specified tags is ignored.
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:
.. code-block:: python
from .__base__ import *
from textoutpc import InlineTag as _InlineTag
class TextoutHelloTag(TextoutInlineTag):
class TextoutHelloTag(_InlineTag):
""" The [hello] tag, which does things.
Example uses:

54
docs/usage.rst Normal file
View File

@ -0,0 +1,54 @@
Using textoutpc
===============
To use this module, simply use the ``to<language>()`` functions once imported:
.. code-block:: python
#!/usr/bin/env python3
import textoutpc
text = "Hello, [i]beautiful [b]world[/i]!"
print(textoutpc.tohtml(text))
print("---")
print(textoutpc.tolightscript(text))
The supported output types are:
- ``html``: `HTML`_ compatible output, requiring some additional style and
script;
- ``lightscript``: `Lightscript`_ Markdown-like language. See
`the Lightscript topic on Planète Casio <Lightscript topic_>`_ for
more information.
The ``tohtml()`` and ``tolightscript()`` can take additional keywords that
tags can read so that they can adapt their behaviour. The name of the tweaks
are case-insensitive and non-alphanumeric characters are ignored: for example,
``label_prefix``, ``LABELPREFIX`` and ``__LaBeL___PRE_FIX__`` are all
equivalent.
The following tweaks are read by the translator and built-in tags:
- ``inline``: if ``True``, only use inline tags, not blocks (for inline
contexts such as instant messaging or one-line comments).
- ``label_prefix`` (HTML): prefix to be used by the ``[label]`` and
``[target]`` tags, e.g. ``msg45529-``. Defaults to `""` for PCv42
compatibility;
- ``obsolete_tags`` (HTML): use obsolete HTML tags for old browsers
(e.g. lynx) compatibility, e.g. ``<b>``, ``<i>``, ``<center>``, and
others. Defaults to ``True``.
- ``title_level`` (HTML): level at which to start for titles and subtitles,
e.g. ``h5`` for ``h5`` for titles and ``h6`` for subtitles.
An example call would be:
.. code-block:: python
#!/usr/bin/env python3
import textoutpc
print(textoutpc.tohtml("Hello, [i]beautiful[/i]!", obsolete__TAGS=False))
.. _HTML: https://www.w3.org/html/
.. _Lightscript: https://git.planet-casio.com/lephe/lightscript
.. _Lightscript topic: https://planet-casio.com/Fr/forums/lecture_sujet.php?id=15022

View File

@ -1 +0,0 @@
regex

View File

@ -16,6 +16,12 @@ def parse_args():
ap.add_argument('-o', dest='output', default=sys.stdout,
type=lambda x: sys.stdout if x == '-' else open(x, 'w'),
help='the output source, stdout by default.')
ap.add_argument('--inline', dest='inline', action='store_true',
help='only inline tags will be interpreted')
ap.add_argument('--obsolete-tags', dest='obs_tags', action='store_true',
help='whether to use obsolete HTML tags such as <b>')
ap.add_argument('--label-prefix', dest='lbl_prefix',
help='prefix to use for label tags, e.g. "msg55459-"')
ap.add_argument('input', nargs='?', default=sys.stdin,
type=lambda x: sys.stdin if x == '-' else open(x, 'r'),
help='the input source, stdin by default.')
@ -27,10 +33,14 @@ def main():
""" Main function of the script. """
args = parse_args()
print(textoutpc.tohtml(args.input.read()), file=args.output, end='')
print(textoutpc.tohtml(args.input.read(), inline=args.inline,
obsolete_tags=args.obs_tags, label_prefix=args.lbl_prefix),
file=args.output, end='')
if __name__ == '__main__':
# TODO: manage exceptions
main()
try:
main()
except e:
print("textout2html: error: " + str(e))
# End of file.

View File

@ -30,7 +30,9 @@ def main():
print(textoutpc.tolightscript(args.input.read()), file=args.output, end='')
if __name__ == '__main__':
# TODO: manage exceptions
main()
try:
main()
except e:
print("textout2ls: error: " + str(e))
# End of file.

View File

@ -1,2 +1,50 @@
[metadata]
name = textoutpc
version = attr: textoutpc.version
url = https://textout.touhey.pro/
project_urls =
Documentation = https://textout.touhey.pro/docs/
author = Thomas Touhey
author_email = thomas@touhey.fr
description = textout() equivalent from Planète Casio
long_description = file: README.rst
keywords = planète casio, textout, bbcode, translator, parser
license = MIT
classifiers =
Development Status :: 2 - Pre-Alpha
License :: OSI Approved :: MIT License
Natural Language :: French
Operating System :: OS Independent
Programming Language :: Python :: 3
Intended Audience :: Developers
Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries
Topic :: Text Processing :: Markup :: HTML
[options]
zip_safe = False
include_package_data = True
packages = textoutpc, textoutpc.builtin
test_suite = test
scripts =
scripts/textout2html
scripts/textout2ls
install_requires =
regex
thcolor
[options.package_data]
* = *.txt, *.rst
[build_sphinx]
source-dir = docs
[wheel]
universal = True
[flake8]
ignore = F401, F403, E128, E131, E241, E261, E265, E271, W191
exclude = .git, __pycache__, build, dist, docs/conf.py, test.py, test
[tool:pytest]
python_files = tests.py test_*.py *_tests.py
testpaths = tests

View File

@ -1,37 +1,22 @@
#!/usr/bin/env python3
#******************************************************************************
# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
# This file is part of the fingerd Python 3.x module, which is MIT-licensed.
# This file is part of the textoutpc Python 3.x module, which is MIT-licensed.
#******************************************************************************
""" Setup script for the textoutpc Python package and script. """
from setuptools import setup, find_packages
from setuptools import setup as _setup
setup(name='textoutpc',
version='0.1',
description='Textout() equivalent from Planète Casio',
author='Thomas "Cakeisalie5" Touhey',
author_email='thomas@touhey.fr',
url='https://forge.touhey.fr/pc/textout.git/',
license='MIT',
keywords='planète casio textout bbcode translator parser',
kwargs = {}
packages=find_packages(),
scripts=['textout2html', 'textout2lightscript'],
test_suite="test",
try:
from sphinx.setup_command import BuildDoc as _BuildDoc
kwargs['cmdclass'] = {'build_sphinx': _BuildDoc}
except:
pass
install_requires=['regex'],
# Actually, most of the project's data is read from the `setup.cfg` file.
classifiers = [
'Development Status :: 2 - Pre-Alpha',
'License :: OSI Approved :: MIT License',
'Natural Language :: French',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3',
'Intended Audience :: Developers',
'Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries',
'Topic :: Text Processing :: Markup :: HTML'
]
)
_setup(**kwargs)
# End of file.

View File

@ -1,191 +0,0 @@
#!/usr/bin/env python3
#******************************************************************************
# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
""" Unit tests for the Python version of textout.
Uses the builtin `unittest` module.
"""
import unittest
import textoutpc
# Define the tests.
__test_cases = {
# Basic text.
'': '',
'lol': '<p>lol</p>',
'<script>alert(1);</script>': \
'<p>&lt;script&gt;alert(1);&lt;/script&gt;</p>',
# 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)">' \
'http://www.example.org/some-[damn-url]-(youknow)</a>)</p>',
'https://thomas.touhey.fr/, tu vois ?': \
'<p><a href="https://thomas.touhey.fr/">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]': '',
'[left]lol[/]hi': '<div class="align-left"><p>lol</p></div><p>hi</p>',
'a[justify]b': '<p>a</p><div class="align-justify"><p>b</p></div>',
'a[i][justify]b': '<p>a</p>' \
'<div class="align-justify"><p><i>b</i></p></div>',
'a[i]k[center]b': '<p>a<i>k</i></p>' \
'<div class="align-center"><p><i>b</i></p></div>',
'a[i]k[center][b]b[justify]c[/center]d[/]wouhou': \
'<p>a<i>k</i></p>' \
'<div class="align-center"><p><i><b>b</b></i></p></div>' \
'<div class="align-justify"><p><i><b>c</b></i></p></div>' \
'<p><i>d</i>wouhou</p>',
# Show tag for super preprocessing blocks.
'[quote][show][justify]hehe': \
'<div class="citation"><p>&lt;div class="align-justify"&gt;' \
'&lt;p&gt;hehe&lt;/p&gt;&lt;/div&gt;</p></div>',
# 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]': '<p>[url]</p>',
'[url=https://thomas.touhey.fr/]mon profil est le meilleur[/url]':
'<p><a href="https://thomas.touhey.fr/">mon profil est le meilleur' \
'</a></p>',
'[url=https://thomas.touhey.fr/]': \
'<p><a href="https://thomas.touhey.fr/">https://thomas.touhey.fr/' \
'</a></p>',
'[url=http://hey.org/lol[]>"a]': '<p><a href="http://hey.org/lol[]&gt;' \
'&quot;a">' \
'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>',
'[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></p><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' \
'</div><div class="title off" ' \
'onclick="toggleSpoiler(this.parentNode, ' "'close'" ');">world' \
'&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;' \
'[/video]</p>',
'[video]<script>alert(document.cookie)</script>[/video]': \
'<p>[video]&lt;script&gt;alert(document.cookie)&lt;/script&gt;' \
'[/video]</p>',
'[video]https://www.youtube.com/watch?v=6odDOOyUawY[/video]': \
'<div class="video-wrapper video-medium"><iframe ' \
'src="https://www.youtube.com/embed/6odDOOyUawY" ' \
'frameborder="0" allowfullscreen></iframe></div>',
'[video]https://www.youtube.com/watch?v=<script>alert(1)</script>': \
'<p><a href="https://www.youtube.com/watch?v=&lt;script&gt;alert(1)' \
'&lt;/script&gt;">' \
'https://www.youtube.com/watch?v=&lt;script&gt;alert(1)' \
'&lt;/script&gt;</a></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.
_cnt = 0
_len = len(str(len(__test_cases)))
_templ = """\
def test_html{n:0>{l}}(self):
self.assertEqual({r}, textoutpc.tohtml({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 TextoutHTMLTest(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

@ -1,44 +0,0 @@
#!/usr/bin/env python3
#******************************************************************************
# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
""" 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_lgsp{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 TextoutLSTest(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.

230
tests/test_html.py Executable file
View File

@ -0,0 +1,230 @@
#!/usr/bin/env python3
#******************************************************************************
# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
""" Unit tests for the Python version of textout. """
import pytest
from textoutpc import tohtml as _tohtml
# Define the tests.
@pytest.mark.parametrize('test_input,expected', (
# Basic text.
('', ''),
('lol', '<p>lol</p>'),
('<script>alert(1);</script>',
'<p>&lt;script&gt;alert(1);&lt;/script&gt;</p>'),
# 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)">' \
'http://www.example.org/some-[damn-url]-(youknow)</a>)</p>'),
('https://thomas.touhey.fr/, tu vois ?',
'<p><a href="https://thomas.touhey.fr/">https://thomas.touhey.fr/' \
'</a>, tu vois ?</p>'),
# Basic text styling.
('[u][b][a][i][/b]', "<p><u><b>[a]</b></u></p>"),
('[u][b]a[/]mdr', '<p><u><b>a</b>mdr</u></p>'),
# Blocks, alignment.
('[left]', ''),
('[left]lol[/]hi', '<div class="align-left"><p>lol</p></div><p>hi</p>'),
('a[justify]b', '<p>a</p><div class="align-justify"><p>b</p></div>'),
('a[i]', '<p>a</p>'),
('a[i][justify]b', '<p>a</p>' \
'<div class="align-justify"><p><i>b</i></p></div>'),
('a[i]k[center]b', '<p>a<i>k</i></p>' \
'<div class="align-center"><p><i>b</i></p></div>'),
('a[i]k[center][b]b[justify]c[/center]d[/]wouhou',
'<p>a<i>k</i></p>' \
'<div class="align-center"><p><i><b>b</b></i></p>' \
'<div class="align-justify"><p><i><b>c</b></i></p></div></div>' \
'<p><i>d</i>wouhou</p>'),
# Show tag for super preprocessing blocks.
('[show]lol', '<p><span class="inline-code">lol</span></p>'),
('[quote][show][justify]hehe',
'<div class="citation"><p><span class="inline-code">' \
'&lt;div class=&quot;align-justify&quot;&gt;' \
'&lt;p&gt;hehe&lt;/p&gt;&lt;/div&gt;' \
'</span></p></div>'),
# Titles.
('lolk[title]smth', '<p>lolk</p>' '<h1 class="title">smth</h1>'),
('[subtitle]<>', '<h2 class="subtitle">&lt;&gt;</h2>'),
# 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=rbga(5, 7)]b', '<p><span style="color: #050007">b</span></p>'),
('[color=hsl(0, 1,50.0%)]r',
'<p><span style="color: #FF0000">r</span></p>'),
# TODO: hls, hwb
# Links.
('[url]', '<p>[url]</p>'),
('[url=https://thomas.touhey.fr/]mon profil est le meilleur[/url]',
'<p><a href="https://thomas.touhey.fr/">mon profil est le meilleur' \
'</a></p>'),
('[url=https://thomas.touhey.fr/]',
'<p><a href="https://thomas.touhey.fr/">https://thomas.touhey.fr/' \
'</a></p>'),
('[url=http://hey.org/lol[]>"a]', '<p><a href="http://hey.org/lol[]&gt;' \
'&quot;a">' '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>'),
('[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></p><p>' \
'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'" ');"><p>Hello' \
'</p></div><div class="title off" ' \
'onclick="toggleSpoiler(this.parentNode, ' "'close'" ');"><p>world' \
'&gt; <img src="/images/smileys/grin.gif"></p></div>' \
'<div class="off"><p>Close this, quick!</p></div></div>'),
# Code.
('[code]', ''),
("`[code]`", '<p><span class="inline-code">[code]</span></p>'),
('[inlinecode]', ''),
("[inlinecode]`[/inlinecode]",
'<p><span class="inline-code">`</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>'),
('[img=right|float|12x345]https://example.org/image.png',
'<img src="https://example.org/image.png" class="img-float-right" ' \
'style="width: 12px; height: 345px" />'),
# Videos.
('[video]"><script>alert(1)</script>[/video]',
'<p>[video]&quot;&gt;&lt;script&gt;alert(1)&lt;/script&gt;' \
'[/video]</p>'),
('[video]<script>alert(document.cookie)</script>[/video]',
'<p>[video]&lt;script&gt;alert(document.cookie)&lt;/script&gt;' \
'[/video]</p>'),
('[video]https://www.youtube.com/watch?v=6odDOOyUawY[/video]',
'<div class="video-wrapper" style="padding-bottom: 56.25%"><iframe ' \
'src="https://www.youtube.com/embed/6odDOOyUawY" ' \
'frameborder="0" allowfullscreen></iframe></div>'),
('[video]https://www.youtube.com/watch?v=<script>alert(1)</script>',
'<p><a href="https://www.youtube.com/watch?v=&lt;script&gt;alert(1)' \
'&lt;/script&gt;">' \
'https://www.youtube.com/watch?v=&lt;script&gt;alert(1)' \
'&lt;/script&gt;</a></p>'),
('[video=left|float|4:3]https://www.youtube.com/watch?v=XEjLoHdbVeE',
'<div class="video-wrapper img-float-left" ' \
'style="padding-bottom: 75%"><iframe ' \
'src="https://www.youtube.com/embed/XEjLoHdbVeE" frameborder="0" ' \
'allowfullscreen></iframe></div>'),
('lol[youtube]h4WLX8hfpJw', '<p>lol</p><div class="video-wrapper" ' \
'style="padding-bottom: 56.25%"><iframe ' \
'src="https://www.youtube.com/embed/h4WLX8hfpJw" frameborder="0" ' \
'allowfullscreen></iframe></div>'),
('[color=blue][youtube]h4WLX8hfpJw',
'<div class="video-wrapper" style="padding-bottom: 56.25%">' \
'<iframe src="https://www.youtube.com/embed/h4WLX8hfpJw" ' \
'frameborder="0" allowfullscreen></iframe></div>'),
('[color=blue]oh[youtube]h4WLX8hfpJw',
'<p><span style="color: #0000FF">oh</span></p>' \
'<div class="video-wrapper" style="padding-bottom: 56.25%"><iframe ' \
'src="https://www.youtube.com/embed/h4WLX8hfpJw" frameborder="0" ' \
'allowfullscreen></iframe></div>'),
# Progress bars.
('[progress=lol]mdr[/progress]', '<p>[progress=lol]mdr[/progress]</p>'),
# Text rotation obfuscation.
('[rot13]obawbhe[/rot13]', '<p>bonjour</p>'),
# Lists.
('[list]haha[b][*]wow[*]incredible[/b][/*]wow[*]yuy[/list]',
'<ul><li><p>wow</p></li><li><p>incredible[/b]</p></li>' \
'<li><p>yuy</p></li></ul>'),
('[list]\n[*]bonjour', '<ul><li><p>bonjour</p></li></ul>'),
# Smileys.
(':)', '<p><img src="/images/smileys/smile.gif"></p>'),
(':):)', '<p>:):)</p>'),
(':) :D', '<p><img src="/images/smileys/smile.gif"> ' \
'<img src="/images/smileys/grin.gif"></p>'),
))
def test_html(test_input, expected):
assert _tohtml(test_input) == expected
# End of file.

30
tests/test_htmli.py Executable file
View File

@ -0,0 +1,30 @@
#!/usr/bin/env python3
#******************************************************************************
# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
""" Unit tests for the Python version of textout. """
import pytest
from textoutpc import tohtml as _tohtml
@pytest.mark.parametrize('test_input,expected', (
# Basic text.
('', ''),
('lol', 'lol'),
# Basic text styling.
('[u][b]a[/]mdr', '<u><b>a</b>mdr</u>'),
# Links.
('[url=https://thomas.touhey.fr/]',
'<a href="https://thomas.touhey.fr/">https://thomas.touhey.fr/</a>'),
))
def test_htmli(test_input, expected):
assert _tohtml(test_input, inline = True) == expected
# End of file.

View File

@ -3,15 +3,19 @@
# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
""" HTML/CSS-like color parsing, mainly for the `[color]` tag.
Defines the `get_color()` function which returns an rgba value.
""" Unit tests for the Python version of textout, lightscript-related
functions. """
The functions in this module do not aim at being totally compliant with
the W3C standards, although it is inspired from it.
"""
import pytest
from .read import get_color
from textoutpc import tolightscript as _tolightscript
__all__ = ["get_color"]
@pytest.mark.parametrize('test_input,expected', (
# Basic text.
('', ''),
))
def test_lightscript(test_input, expected):
assert _tolightscript(test_input) == expected
# End of file.

View File

@ -7,27 +7,41 @@
Really simplifies the thing.
"""
import io as _io
from .translate import Translator as _Translator
from io import StringIO as _StringIO
__all__ = ["version", "tohtml", "tolightscript"]
from ._options import TextoutOptions as Options, \
TextoutBlockTag as BlockTag, TextoutInlineTag as InlineTag, \
TextoutParagraphTag as ParagraphTag, TextoutSmiley as Smiley, \
TextoutImage as Image, TextoutVideo as Video
from ._translate import Translator as _Translator
version = "0.1"
__all__ = ["version", "tohtml", "tolightscript",
"Options", "BlockTag", "ParagraphTag", "InlineTag",
"Smiley", "Image", "Video"]
def tohtml(message, **tweaks):
version = "0.2.1"
# ---
# Public functions.
# ---
_default_options = Options()
def tohtml(message, options = _default_options, **tweaks):
""" Converts textout BBcode to HTML.
Receives a string, returns a string. """
return _Translator(_io.StringIO(message), _io.StringIO(), 'html', \
tweaks).process().getvalue()
t = _Translator(_StringIO(message), _StringIO(), 'html', \
tweaks, options)
return t.process().getvalue()
def tolightscript(message, **tweaks):
def tolightscript(message, options = _default_options, **tweaks):
""" 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', \
tweaks).process().getvalue()
return _Translator(_StringIO(message), _StringIO(), 'lightscript', \
tweaks, options).process().getvalue()
# End of file.

70
textoutpc/_html.py Executable file
View File

@ -0,0 +1,70 @@
#!/usr/bin/env python3
#******************************************************************************
# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
""" Utilities for HTML conversions. """
import regex as _re
from html import escape
__all__ = ["escape", "urls", "SmileyConvertor"]
# ---
# Smileys.
# ---
class SmileyConvertor:
""" Smileys convertor. """
def __init__(self, smileys = {}):
self._html = {escape(a): b.url \
for a, b in smileys.items() if b.url != None}
self._re = _re.compile('(^|\\s)(' + '|'.join(map(_re.escape,
self._html.keys())) + ')(\\s|$)')
def convert(self, text):
cv = ""
while text:
try:
m = next(self._re.finditer(text))
except StopIteration:
break
cv += text[:m.start()] + m.group(1)
cv += '<img src="' + self._html[m.group(2)] + '">'
text = m.group(3) + text[m.end():]
return cv + text
# ---
# URLs.
# ---
_urlreg = _re.compile("""\
(?P<sp>^|\s|[[:punct:]])
(?P<url>(https?|ftp):
(?P<ucore>[^\[\]\(\)\s]* (\[(?&ucore)\]?)* (\((?&ucore)\)?)*)*
)
""", _re.VERBOSE | _re.M)
def urls(text):
""" Convert URLs. """
def _sub_html(m):
sp = m.group('sp')
url = m.group('url')
aft = ''
# Hack for the last comma.
if url[-1] == ',':
url, aft = url[:-1], ','
text = '{}<a href="{}">{}</a>{}' \
.format(sp, url, url, aft)
return text
return _urlreg.sub(_sub_html, text)
# End of file.

42
textoutpc/_ls.py Executable file
View File

@ -0,0 +1,42 @@
#!/usr/bin/env python3
#******************************************************************************
# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
""" Utilities for Lightscript conversions. """
import regex as _re
__all__ = ["urls"]
# ---
# URLs.
# ---
_urlreg = _re.compile("""\
(?P<sp>^|\s|[[:punct:]])
(?P<url>(https?|ftp):
(?P<ucore>[^\[\]\(\)\s]* (\[(?&ucore)\]?)* (\((?&ucore)\)?)*)*
)
""", _re.VERBOSE | _re.M)
def urls(text):
""" Convert URLs. """
def _sub_ls(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
return _regurl.sub(_sub_ls, text)
# End of file.

355
textoutpc/_options.py Executable file
View File

@ -0,0 +1,355 @@
#!/usr/bin/env python3
#******************************************************************************
# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
""" Base classes to use with options (tags, smileys) in textoutpc, with a
manager class.
For your tag to be used as a textoutpc tag, you have to make it
inherit one of the `TextoutBlockTag` or `TextoutInlineTag` classes.
Making separate tag modules is possible through the manager class,
which allows not to hardcode the tags into the module. """
from functools import partial as _p
from inspect import ismodule as _ismod, isclass as _isclass, \
getargspec as _getargspec, getfullargspec as _getfullargspec, \
currentframe as _currentframe, getouterframes as _getouterframes
from importlib import import_module as _importmod
from ._html import SmileyConvertor as _htmlsm
__all__ = ["TextoutOptions",
"TextoutTag", "TextoutBlockTag", "TextoutInlineTag", "TextoutParagraphTag",
"TextoutSmiley", "TextoutImage", "TextoutVideo"]
def _getargscount(func):
try:
return len(_getfullargspec(func).args)
except:
return len(_getargspec(func).args)
# ---
# Tags.
# ---
# Main base tag class.
# For more about defining a tag, see `doc/tags.md`.
class TextoutTag:
""" The textout tag base class.
Is initialized with these values:
<name><content><name>
| name: "<name>" (only special chars such as `)
| value: None
[<name>]<content>[/<name>]
| name: "[<name>]"
| value: None
[<name>]<content>[/] (when possible)
| name: "[<name>]"
| value: None
[<name>=<value>]<content>[/<name>]
| name: "[<name>]"
| value: "<value>"
[<name>=<value>]<content>[/] (when possible)
| name: "[<name>]"
| value: "<value>" """
aliases = ()
def __init__(self, name, value, ot, tweaks, options):
""" Initialize the textout tag with the documented members. """
# Store internal data.
self.__name = name
self.__value = value
self.__output_type = ot
self.__tweaks = tweaks
self.__options = options
self.output_type = ot
# Call both prepare functions.
if hasattr(self, 'prepare'):
try:
assert _getargscount(self.prepare) == 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'):
if hasattr(self, 'preprocess_' + ot):
self.__preprocess0 = self.preprocess
self.preprocess = self.__preprocess_double
elif hasattr(self, 'preprocess_' + ot):
self.preprocess = getattr(self, 'preprocess_' + ot)
if hasattr(self, 'preprocess'):
self.__preprocess2 = self.preprocess
self.preprocess = self.__preprocess_and_prepare
else:
self.__after_preprocess()
if hasattr(self, 'default_' + ot):
self.default = getattr(self, 'default_' + ot)
def __repr__(self):
return f"{self.__class__.__name__}(name = {repr(self.__name)}, " \
f"value = {repr(self.__value)}, " \
f"ot = {repr(self.__output_type)})"
def __preprocess_double(self, content):
""" Preprocess using the two methods. """
ct = self.__preprocess0(content)
if ct != None:
content = ct
del ct
ct = self.__preprocess1(content)
if ct != None:
content = ct
del ct
return content
def __preprocess_and_prepare(self, content):
""" Preprocess and do the things after. """
ret = self.__preprocess2(content)
self.__after_preprocess()
return ret
def __out(self, name):
""" Generic function to call two output functions of the same
type. """
getattr(self, '__' + name)()
getattr(self, name + '_' + self.__output_type)()
def __after_preprocess(self):
""" After preprocessing, check the begin, content and end that may
have been set by the preprocessing function. """
ot = self.__output_type
for otype in ('begin', 'content', 'end'):
if hasattr(self, otype):
if hasattr(self, otype + '_' + ot):
setattr(self, '__' + otype, getattr(self, otype))
setattr(self, otype, _p(self.__out, otype))
elif hasattr(self, otype + '_' + ot):
setattr(self, otype, getattr(self, otype + '_' + ot))
def tweak(self, key, default = None):
try:
return self.__tweaks[key]
except KeyError:
return default
def image(self, *args, **kwargs):
return self.__options.get_image(*args, **kwargs)
def video(self, *args, **kwargs):
return self.__options.get_video(*args, **kwargs)
# Role-specific base tag classes.
class TextoutBlockTag(TextoutTag):
pass
class TextoutInlineTag(TextoutTag):
pass
# Default tag: paragraph.
class TextoutParagraphTag(TextoutBlockTag):
""" Main tag for basic paragraphs. """
notempty = True
def begin_html(self):
return '<p>'
def end_html(self):
return '</p>'
# ---
# Smileys.
# ---
class TextoutSmiley:
""" Base class for smileys. """
aliases = ()
url = None
def __repr__(self):
return f"{self.__class__.__name__}(aliases = {repr(self.aliases)}, " \
f"url = {repr(self.url)})"
# ---
# Multimedia.
# ---
class TextoutImage:
""" Base class for images. """
def __init__(self, url):
raise ValueError("no URL supported")
class TextoutVideo:
""" Base class for videos. """
def __init__(self, url):
raise ValueError("no URL supported")
# ---
# Options extractor and manager.
# ---
_builtin_module = None
def _get_builtin_module():
""" Get the `.builtin` module. """
global _builtin_module
if _builtin_module == None:
_builtin_module = _importmod('..builtin', __name__)
return _builtin_module
class TextoutOptions:
""" Options manager.
Object responsible for getting the tags. """
def __init__(self, *modules, default = True):
self._aliases = {}
self._s_aliases = {}
self._videos = []
self._images = []
if default:
self.add(_get_builtin_module())
for mod in modules:
self.add(mod)
def __repr__(self):
return f"{self.__class__.__name__}()"
def add(self, element):
""" Add an option. """
if isinstance(element, str):
element = str(element)
element = _importmod(element,
_getouterframes(_currentframe(), 1)[0].name)
if _ismod(element):
self.__extract(element)
return True
if _isclass(element) and issubclass(element, TextoutTag):
for alias in element.aliases:
self._aliases[alias] = element
return True
if _isclass(element) and issubclass(element, TextoutSmiley):
for alias in element.aliases:
self._s_aliases[alias] = element
self._htmlsm = None
return True
if _isclass(element) and issubclass(element, TextoutImage):
if not any(image is element for image in self._images):
self._images.append(element)
if _isclass(element) and issubclass(element, TextoutVideo):
if not any(video is element for video in self._videos):
self._videos.append(element)
def __extract(self, module):
""" Extract options from a module. """
tags = []
smileys = []
# Obtain the list of properties from the module.
try:
ds = module.__all__
except:
ds = dir(module)
# Get the submodules from the module (usually different files in the
# tags module folder).
for submodule in (obj for name, obj in ((nm, getattr(module, nm)) \
for nm in ds) if (name == '__init__' or name[0] != '_') \
and _ismod(obj)):
self.__extract(submodule)
# Extract the tags from the current module.
for obj in (obj for name, obj in ((nm, getattr(module, nm)) \
for nm in ds) if name[0] != '_'):
self.add(obj)
def get_smileys(self):
""" Get the smileys dictionary. """
return self._s_aliases.copy()
def htmlsmileys(self, text):
""" Get the smileys convertor for HTML. """
if not self._htmlsm:
self._htmlsm = _htmlsm(self._s_aliases)
return self._htmlsm.convert(text)
def get_video(self, url):
""" Get a video using its URL. """
for video in self._videos:
try:
v = video(url)
except:
continue
break
else:
raise ValueError("invalid video URL")
return v
def get_image(self, url):
""" Get an image using its URL. """
for image in self._images:
try:
i = image(url)
except:
continue
break
else:
raise ValueError("invalid image URL")
return i
def get_tag(self, name):
""" Get the tag class corresponding to a name. """
return self._aliases[name]
# End of file.

View File

@ -27,6 +27,7 @@ class TextoutUnit:
PARSEP = 5
def __init__(self, *args):
self.full = ''
if len(args) > 1:
self.type, self.name, self.value, *_ = args + (None,)
return
@ -66,10 +67,10 @@ class TextoutUnit:
def __repr__(self):
typetab = {self.BEGIN: "begin", self.END: "end",
self.SPECIAL: "special", self.NEWLINE: "newline"}
return '<TextoutUnit type={}{}{}>'.format(\
return '_TextoutUnit(type={}{}{})'.format(\
typetab[self.type],
' name=' + self.name if self.name != None else "",
' value="{}"'.format(self.value) if self.value != None else "")
', name=' + repr(self.name) if self.name != None else "",
', value=' + repr(self.value) if self.value != None else "")
def __equ__(self, other):
if not isinstance(other, TextoutUnit):
@ -100,22 +101,24 @@ class TextoutStream:
#
# FIXME: check the sizes? it seems that it stopped working…
_Tag = _re.compile("""\
\[\s?(?P<bname>
_Tag = _re.compile(r"""
\[\s?
(?P<bname>
(?P<bname_e>[^\/\[\]\=][^\[\]\=]* (\[(?&bname_e)\]?)*)*
)
(\s?=\s?(?P<value>
(?P<value_e>[^\[\]]* (\[(?&value_e)\]?)*)*
))?\s?\]
))?
\s?\]
|
\[[\\/]\s?(?P<ename>
\[[\\\/]\s?(?P<ename>
(?P<ename_e>[^\/\[\]\=][^\[\]\=]* (\[(?&ename_e)\]?)*)*
)\s?\]
|
(?P<parsep>[\n]{2,})
|
(?P<sname>`|[\n])
""", _re.VERBOSE | _re.M)
""", _re.VERBOSE | _re.DOTALL | _re.MULTILINE)
# Keep this buffer size above the maximum size of a tag (387)
# for this class to work alright. Anything above 512 should work great.
@ -125,18 +128,22 @@ class TextoutStream:
def __init__(self, stream):
# If the 'stream' is a string, we want to use standard stream
# functions, so we're gonna enforce them using the `StringIO` class.
if isinstance(stream, str):
stream = _io.StringIO(stream)
# Buffer management.
self.stream = stream
self.buf = ""
# Management of the last tag match.
self.result = None
self.last = None
# Error position.
self.pos = 0
self.line = 0
self.col = 0
@ -144,22 +151,26 @@ class TextoutStream:
def __iter__(self):
# This class is (obviously) iterable.
# We want to use this class as the iterator as well.
return self
def __next__(self):
# If we have a result, process it.
if self.result:
data, self.result = TextoutUnit(self.result), None
self.last = data
return data
# Make sure to have enough data to read.
self.buf += self.stream.read(self.BUFFER_SIZE - len(self.buf))
if not self.buf:
self.last = None
raise StopIteration
# Check that we have a result.
result = self._Tag.search(self.buf, partial = True)
if not result:
text = self.buf
@ -169,6 +180,7 @@ class TextoutStream:
# If there is some text, return it.
# Eventually store the result so we can process it later.
if result.start() > 0:
ret = self.buf[:result.start()]
self.buf = self.buf[result.end():]
@ -178,6 +190,7 @@ class TextoutStream:
return ret
# Process the result now!
self.buf = self.buf[result.end():]
data = TextoutUnit(result)
self.last = data

View File

@ -9,12 +9,11 @@
import string as _string
from copy import deepcopy as _deepcopy
from html import escape as _htmlescape
from .tags import TextoutBlockTag as _TextoutBlockTag, \
TextoutParagraphTag as _TextoutParagraphTag, get_tag as _get_tag
from .stream import TextoutStream as _TextoutStream
from .smileys import htmlsmileys as _htmlsmileys
from .urls import htmlurls as _htmlurls
from ._options import TextoutBlockTag as _TextoutBlockTag, \
TextoutParagraphTag as _TextoutParagraphTag, TextoutOptions as _Options
from ._stream import TextoutStream as _TextoutStream
from ._html import escape as _htmlescape, urls as _htmlurls
__all__ = ["Translator"]
@ -27,17 +26,26 @@ class _TweaksDictionary:
tweak keyword, e.g. `label_prefix`, `LABELPREFIX` and
`__LaBeL___PRE_FIX__`. """
def __normalize(self, name):
return ''.join(c for c in name if c in _string.ascii_letters).lower()
def __init__(self, base):
self.__elts = {}
for kw in base:
self.__elts[self.__normalize(kw)] = base[kw]
def __repr__(self):
return f"{self.__class__.__name__}({repr(self.__elts)})"
def __getitem__(self, key):
return self.__elts[key]
return self.__elts[self.__normalize(key)]
def __getattr__(self, key):
try:
return self.__elts[self.__normalize(key)]
except:
raise AttributeError(key)
def __normalize(self, name):
return ''.join(c for c in name if c in _string.ascii_letters).lower()
# ---
# Tag data utility.
@ -60,15 +68,32 @@ class _TagData:
else self.INLINE
self.full = full
# Tag stack behaviour.
# `nwi` is whether the tag can be in itself directly or not (see
# the `not_within_itself` property in the docs).
# `onlyin` is the list of tags that are allowed as a parent tag
# to this one.
# `allowed` is the tags that are allowed amongst the children tags.
self.nwi = bool(tag.not_within_itself) \
if hasattr(tag, 'not_within_itself') else False
self.onlyin = list(tag.only_in) \
if hasattr(tag, 'only_in') else None
self.allowed = list(tag.allowed_tags) \
if hasattr(tag, 'allowed_tags') else None
# 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.
# `notext` is whether text within the tag directly is printed or not.
self.notempty = bool(tag.notempty) if hasattr(tag, 'notempty') \
else False
self.started = False
self.notext = bool(tag.no_text) if hasattr(tag, 'no_text') \
else False
# `base` is the actual tag object returned by `get_tag()`.
@ -77,11 +102,12 @@ class _TagData:
# Flags and properties calculated from the tag's attributes, using the
# rules given in `TAGS.md`.
# `ign` is whether the content should be read while the tag is opened.
# `raw` is whether the tag's content should be read as raw.
# `generic` is whether the tag can be terminated by the generic
# tag ending mark [/].
# `notempty` is whether the tag should be used with an empty
# content or not (e.g. to avoid `<p></p>`), True if not.
# `raw` is whether the tag's content should be read as raw.
# `super` is whether the tag is a superblock or not.
# `inlined` is whether the next block on the same level is turned into
# a superblock or not.
self.ign = not hasattr(tag, 'preprocess') and hasattr(tag, 'content')
@ -95,6 +121,12 @@ class _TagData:
bool(tag.superblock) if hasattr(tag, 'superblock') \
else False
self.inlined = bool(tag.inlined) if self.super \
and hasattr(tag, 'inlined') and bool(tag.inlined) else False
self.noinline = bool(tag.noinline) if self.type == self.BLOCK \
and hasattr(tag, 'noinline') else False
# Content processing utilities.
# `last` is the content of the tag. A boolean indicates that we
# only want to know if the content is empty or not, and a string
@ -120,7 +152,7 @@ class _TagData:
self.last = ""
def __repr__(self):
return '<TagData>'
return f'TagData(tag = {repr(self.tag)})'
# ---
# Translator main class.
@ -129,11 +161,11 @@ class _TagData:
class Translator:
""" One-time usage class for translating.
Use it this way: `Translator(my_inp, my_outp).process()`.
You can even chain calls as the `process()` method returns
the output stream object. """
def __init__(self, inp, outp, output_type = 'html', tweaks = {}):
def __init__(self, inp, outp, output_type, tweaks, options):
""" Initializer. """
if not output_type in ('html', 'lightscript'):
@ -141,6 +173,7 @@ class Translator:
self.output_type = output_type
self.tweaks = _TweaksDictionary(tweaks)
self.options = options
self.inp = inp
self.outp = outp
@ -174,24 +207,49 @@ class Translator:
self.raw_mode = False
self.raw_deg = 0
# `inline_mode` is whether the inline mode is on or not.
# Actually, for now, this mode is only global and cannot be enabled
# by tags.
self.inline_mode = bool(self.tweak("inline", False))
def __repr__(self):
p = []
p.append(f"inp = {repr(self.inp)}")
p.append(f"outp = {repr(self.outp)}")
p.append(f"output_type = {repr(self.output_type)}")
p.append(f"tweaks = {repr(self.tweaks)}")
p.append(f"options = {repr(self.options)}")
return f"{self.__class__.__name__}({', '.join(p)})"
def tweak(self, key, default = None):
""" Get a tweak from the tweaks dictionary. """
try:
return self.tweaks[key]
except KeyError:
return default
# ---
# Text outputting utilities.
# ---
def process_text_group(self):
def process_text(self, text):
""" Process text groups for naked URLs and stuff. """
# In all cases, we want to escape for HTML things, so that the
# user doesn't insert raw HTML tags (which would be a security flaw!).
if self.output_type == 'html':
text = _htmlescape(self.text_group)
text = _htmlescape(text)
# For non-raw HTML, we want to add smiley and URLs conversion,
# because it's nicer!
if not self.raw_mode and self.output_type == 'html':
text = _htmlsmileys(_htmlurls(text))
text = _htmlurls(text)
text = self.options.htmlsmileys(text)
return text
@ -199,9 +257,10 @@ class Translator:
""" Output some text. """
# If we want to ignore the content (because it is not used
# nor output), let the text fall into the void.
# nor output or the current tag doesn't allow text), let the
# text fall into the void.
if self.cign > 0:
if self.cign > 0 or (self.queue and self.queue[0].notext):
return
# Add to the text group, which will be processed when `flush_text()`
@ -209,7 +268,8 @@ class Translator:
self.text_group += text
def flush_text(self):
def flush_text(self, superblocks_only = False,
next_block_is_super = False):
""" Flush the text that has been output. """
# First of all, check if the text group is empty or if we want to
@ -218,81 +278,126 @@ class Translator:
if not self.text_group or self.cign > 0:
return
# Pop the text group and put the code, with the process function in
# case it is given to a non-raw processing tag or given to the
# output.
text = self.text_group
self.text_group = ""
self.add_text(text, process_func = lambda x: self.process_text(x),
superblocks_only = superblocks_only,
next_block_is_super = next_block_is_super)
# ---
# Code outputting utilities.
# ---
def add_text(self, text, process_func = lambda x: x, start_tags = True,
superblocks_only = False, next_block_is_super = False,
skip_first = False):
""" Add text to the higher blocks if available. """
# The last queue is composed of booleans (does the group contain
# 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.
# 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!
#
# Think about resetting `text_group` as its content has been used
# somewhere (unbuffer data).
blockfound = False
for dat in self.queue:
# Check if it is a tag we want to contribute to.
if dat.type == dat.BLOCK:
if dat.super or next_block_is_super:
blockfound = True
next_block_is_super = dat.inlined
elif not superblocks_only and not blockfound:
blockfound = True
next_block_is_super = dat.inlined
else:
continue
# Check if it is the first tag we want to skip.
if skip_first:
skip_first = False
continue
# Contribute to it, either by or-ing the content if it is
# a boolean (but anything or True == True), or by contributing
# to the buffer otherwise.
if isinstance(dat.last, bool):
dat.last = True
continue
dat.last += self.text_group
# Start the tags if we're about to give this content to
# preprocessing.
if start_tags:
self.start_tags()
# Add the content to the preprocess buffer.
if not dat.raw:
text = process_func(text)
dat.last += text
break
else:
dat = None
text = self.process_text_group()
# No `break` has been encountered, which means the content has
# not been added to any preprocessing tag. Please process it!
self.text_group = ""
if start_tags:
self.start_tags()
self.outp.write(process_func(text))
# Start the tags that haven't been started, and stuff.
return False
self.start_tags()
# The content has been given for preprocessing.
# If the content has to be written, we ought to.
return True
if dat == None:
self.outp.write(text)
def put_debug(self, message):
""" Put a debug message directly into the output. """
# ---
# Code outputting utilities.
# ---
self.outp.write(message)
def put_code(self, code):
def put_code(self, code, start_tags = True, flush_text = True,
superblocks_only = True, next_block_is_super = False,
skip_first = False):
""" Put some code. """
# We don't want to mix text and code, so we'll flush to be sure that
# the order doesn't get mixed up.
self.flush_text()
if flush_text:
self.flush_text()
# First of all, check if the text is empty or if we want to ignore it.
if not code or self.cign > 0:
return
# 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.
# 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!
# Add the code.
for dat in self.queue:
if isinstance(dat.last, bool):
dat.last = True
continue
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)
self.add_text(code, start_tags = start_tags,
superblocks_only = superblocks_only,
next_block_is_super = next_block_is_super,
skip_first = skip_first)
def put_newline(self):
""" Put a newline. """
# If we want to ignore the content (because it is not used
# nor output or the current tag doesn't allow text), let the
# text fall into the void.
if self.cign > 0 or (self.queue and self.queue[0].notext):
return
# The newline depends on the output type and the context, of course.
if self.output_type == 'html' and not self.raw_mode:
@ -317,10 +422,10 @@ class Translator:
if dat.ign:
self.cign += 1
# If it is a block, end the current block.
# If we're about to put a tag or anything, empty the text block
# here.
if dat.type == dat.BLOCK:
self.end_block()
self.flush_text()
# Insert the tag into the queue.
@ -342,6 +447,9 @@ class Translator:
`end` represents the full version of the ending tag marker,
for displaying if the tag is invalid. """
if not self.queue:
return
# Even if we had no beginning, no content and no end, what is
# here has to be distinguished from what was right before!
# So we need to flush the text group for this.
@ -356,6 +464,9 @@ class Translator:
dat = self.queue.pop(0)
tag = dat.tag
pcattrs = {'superblocks_only': dat.type == dat.BLOCK,
'next_block_is_super': dat.inlined}
# If preprocessing has been enabled, we ought to process the content,
# check if the tag is valid, and do everything we would have done
# while pushing the tag if it didn't do content processing.
@ -401,26 +512,27 @@ class Translator:
# Output the beginning and the content. If there was no content,
# just put the content that we got earlier.
dat.started = True
if hasattr(tag, 'begin'):
self.put_code(tag.begin())
self.put_code(tag.begin(), **pcattrs)
dat.started = True
if hasattr(tag, 'content'):
self.put_code(tag.content())
self.put_code(tag.content(), **pcattrs)
elif dat.raw:
# XXX: I'm unsure about this. Shall raw tags return code
# or text? The text will only be escaped as raw mode is
# still enabled at this point.
self.put_text(content)
else:
self.put_code(content)
self.put_code(content, **pcattrs)
elif hasattr(tag, 'content'):
# Tag replaces content without preprocessing, which means
# the content has been ignored and the tag only puts the
# things.
self.cign -= 1
self.put_code(tag.content())
self.put_code(tag.content(), **pcattrs)
elif hasattr(tag, 'default'):
# Tag defines a default content if there might be none,
# without text preprocessing. If there is no content, print it.
@ -428,6 +540,11 @@ class Translator:
# an exception if the tag in its current configuration should
# not have an empty content.
if not dat.started:
if hasattr(dat.tag, 'begin'):
self.put_code(dat.tag.begin(), **pcattrs)
dat.started = True
if not dat.last:
try:
self.put_text(tag.default())
@ -442,15 +559,13 @@ class Translator:
# Don't forget to end the tag!
if dat.notempty and not dat.started:
if not dat.started:
pass
elif dat.type != dat.BLOCK or dat.super:
if hasattr(tag, 'end'):
self.put_code(tag.end())
else:
self.queue.insert(0, dat)
self.end_block()
self.queue.pop(0)
if dat.type == dat.BLOCK:
self.close_inline_tags()
if hasattr(tag, 'end'):
self.put_code(tag.end(), start_tags = False, **pcattrs)
# Disable raw mode if it was a raw tag (which means that it enabled it,
# as tags into raw tags cannot be processed).
@ -464,85 +579,99 @@ class Translator:
def start_tags(self):
""" Start the tags that haven't been started yet.
If a block has been newly opened, we ought to close the block at
the same level as them before opening it.
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.
# First, get the references to the blocks to end, the blocks to
# start, and all of the inline tags.
blocks = []
superblocks = []
block_to_start = None
block_to_end = None
inlines = []
for dat in self.queue:
# Check if the tag hasn't already been started or doesn't call
next_block_is_super = False
for idx, dat in enumerate(self.queue):
# Check that 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.
if dat.type == dat.BLOCK:
blocks.insert(0, dat)
else:
inlines.insert(0, dat)
# Only select super blocks and the last non-super block.
selected = [block for block in blocks if block.super]
if blocks and not blocks[0].super:
selected.insert(0, blocks[0])
blocks = selected
# Then, put the tag beginnings.
for dat in blocks + inlines:
dat.started = True
if hasattr(dat.tag, 'begin'):
self.put_code(dat.tag.begin())
def end_block(self):
""" End the current block. """
queue = self.queue.copy()
# 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 dat.super: break
if not dat.started: continue
if idx > 0 and type(dat.last) != bool:
break
# Then put the tag in the appropriate queue.
if dat.type == dat.BLOCK:
blocks.append(dat)
if block_to_start is not None and \
dat.super or next_block_is_super:
# The block is to be considered as the block to start.
# Sometimes the block to start is the latest superblock!
superblocks.insert(0, dat)
next_block_is_super = dat.inlined
elif dat.started:
block_to_end = dat
next_block_is_super = dat.inlined
elif block_to_end is None and block_to_start is None:
block_to_start = dat
next_block_is_super = dat.inlined
else:
inlines.append(dat)
inlines.insert(0, dat)
# Then we want to end the tags, and reset them in case we're going
# to use them.
# If there is no new block to start, there's no need to end the
# current block.
self.queue = inlines + blocks
while self.queue:
dat = self.queue.pop(0)
tag = dat.tag
if hasattr(tag, 'end'):
self.put_code(tag.end())
if not block_to_start:
block_to_end = None
# Put the tag ends for the blocks to end.
# If there are some, we ought to close the inline tags first.
if block_to_end is not None:
for dat in inlines[::-1] + [block_to_end]:
if not dat.started:
continue
if hasattr(dat.tag, 'end'):
self.put_code(dat.tag.end(), start_tags = False,
skip_first = True)
dat.started = False
dat.reset()
# Then, put the tag beginnings.
to_begin = superblocks \
+ ([block_to_start] if block_to_start else [])
if all(not x.noinline for x in to_begin):
to_begin += inlines
for dat in to_begin:
if dat.started:
continue
if dat.notempty and not dat.last:
break
if hasattr(dat.tag, 'begin'):
self.put_code(dat.tag.begin(), start_tags = False,
flush_text = False, skip_first = dat == self.queue[0])
dat.started = True
def close_inline_tags(self):
""" We're about to close a block, so we want to close any inline tags
that could have been taken within it. """
for dat in self.queue:
# Check that the tag hasn't already been closed.
if dat.type != dat.INLINE or not dat.started:
continue
if hasattr(dat.tag, 'end'):
self.put_code(dat.tag.end(), start_tags = False)
dat.started = False
dat.reset()
# Restore the queue.
self.queue = queue
# ---
# Main function.
# ---
@ -553,8 +682,9 @@ class Translator:
# By default, everything is in a paragraph.
# Other blocks will supplant this by being further in the queue.
self.push_tag(_TagData(_TextoutParagraphTag(None, None,
self.output_type, self.tweaks), None, ''))
if not self.inline_mode:
self.push_tag(_TagData(_TextoutParagraphTag(None, None,
self.output_type, self.tweaks, self.options), None, ''))
# We want to get our elements out of the element stream (Lephe
# told me that the `TextoutStream` class was actually a lexer,
@ -655,22 +785,79 @@ class Translator:
# Get the initialized tag with the name and value.
# If the tag is unknown, output the full thing and just go on.
tag = _get_tag(tagdata.name, tagdata.value, self.output_type,
self.tweaks)
if not tag:
try:
tag = self.options.get_tag(tagdata.name)
except:
self.put_text(tagdata.full)
continue
# And don't forget to push the tag.
value = tagdata.value
if value != None and hasattr(tag, 'procvalue') and tag.procvalue:
value = self.process_text(value)
try:
tag = tag(tagdata.name, value, self.output_type, self.tweaks,
self.options)
except:
self.put_text(tagdata.full)
continue
# Check if it is a block tag.
dat = _TagData(tag, tagdata.name, tagdata.full)
if self.inline_mode and dat.type == dat.BLOCK:
self.put_text(tagdata.full)
continue
# Check if is an allowed tag.
if dat.type == dat.BLOCK:
try:
sb = next(d for d in self.queue if d.super)
except StopIteration:
alw = None
else:
alw = sb.allowed
else:
try:
pr = self.queue[0]
except IndexError:
alw = None
else:
alw = pr.allowed
if alw is not None and not any(cls for cls in alw \
if isinstance(dat.base, cls)):
self.put_text(tagdata.full)
continue
# Check if it is within itself and it can't.
if dat.nwi and any(d for d in self.queue \
if isinstance(d.base, type(dat.base))):
while not isinstance(self.queue[0].base, type(dat.base)):
self.pop_tag()
self.pop_tag()
# Check if it is allowed in this parent.
if dat.onlyin is not None and self.queue \
and not any(cls for cls in dat.onlyin \
if isinstance(self.queue[0].base, cls)):
self.put_text(tagdata.full)
continue
# And don't forget to push the tag (through its data).
self.push_tag(dat)
# Push a paragraph tag if the block is a superblock.
if dat.type == dat.BLOCK and dat.super:
if dat.type == dat.BLOCK and dat.super and not dat.raw \
and not dat.inlined and (dat.allowed is None \
or _TextoutParagraphTag in dat.allowed):
self.push_tag(_TagData(_TextoutParagraphTag(None, None,
self.output_type, self.tweaks), None, ''))
self.output_type, self.tweaks, self.options), None, ''))
# End of file, it seems! Let's close the tags, flush the text
# and just resume our lives from there.
@ -684,4 +871,10 @@ class Translator:
return self.outp
def reopen(self, inp, outp):
""" Open another instance of this translator for sub-translators. """
return Translator(inp, outp, self.output_type, self.tweaks,
self.options)
# End of file.

View File

@ -4,32 +4,45 @@
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
from ..base import TextoutBlockTag as _TextoutBlockTag
from .. import BlockTag as _BlockTag
__all__ = ["TextoutAlignTag"]
__all__ = ["AlignTag"]
class TextoutAlignTag(_TextoutBlockTag):
class AlignTag(_BlockTag):
""" Main tag for aligning paragraphs.
Example uses:
[align=center]This text is centered horizontally.[/align]
[justify]This text is justified.[/justify]
"""
aliases = ('[align]', '[center]', '[left]', '[right]', '[justify]')
aliases = ('[align]', '[center]', '[centre]', '[left]', '[right]',
'[justify]')
superblock = True
notempty = True
def prepare(self, name, value):
align = None
if not name: pass
elif name == 'align' and value != None:
align = value
elif name[1:-1] in ('center', 'left', 'right', 'justify'):
align = name[1:-1]
_align = {
'center': 'center',
'centre': 'center',
'left': 'left',
'right': 'right',
'justify': 'justify'}
if not name:
align = None
elif name == 'align' and value is not None:
align = _align[value]
else:
align = _align[name[1:-1]]
self._align = align
def begin_html(self):
if not self._align:
return ''
cl = []
if self._align:
cl.append('align-' + self._align)
@ -37,6 +50,8 @@ class TextoutAlignTag(_TextoutBlockTag):
return '<div{}>'.format(' class="' + ' '.join(cl) + '"' if cl else '')
def end_html(self):
if not self._align:
return ''
return '</div>'
# End of file.

View File

@ -4,15 +4,15 @@
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
from ..base import TextoutBlockTag as _TextoutBlockTag, \
TextoutInlineTag as _TextoutInlineTag
from .. import BlockTag as _BlockTag, InlineTag as _InlineTag
__all__ = ["TextoutCodeTag", "TextoutInlineCodeTag", "TextoutNoEvalTag"]
__all__ = ["CodeTag", "InlineCodeTag", "NoEvalTag"]
class TextoutCodeTag(_TextoutBlockTag):
class CodeTag(_BlockTag):
""" The basic code tag, for displaying code.
Example uses:
[code]int main()
{
printf("hello, world");
@ -21,6 +21,7 @@ class TextoutCodeTag(_TextoutBlockTag):
aliases = ('[code]',)
generic = False
raw = True
notempty = True
def begin_html(self):
return '<div class="code">'
@ -34,11 +35,12 @@ class TextoutCodeTag(_TextoutBlockTag):
def end_lightscript(self):
return '```\n'
class TextoutInlineCodeTag(_TextoutInlineTag):
class InlineCodeTag(_InlineTag):
""" Inline code tag, doesn't display a box, simply doesn't evaluate
the content and uses monospace font.
Example uses:
`some inline code`
[inlinecode][b]The tags will be shown verbatim.[/b][/inlinecode]
[inlinecode][inlinecode][i]This also[/inlinecode] works![/inlinecode]
@ -49,7 +51,7 @@ class TextoutInlineCodeTag(_TextoutInlineTag):
raw = True
def begin_html(self):
return '<span style="font-family: monospace;">'
return '<span class="inline-code">'
def end_html(self):
return '</span>'
@ -60,14 +62,15 @@ class TextoutInlineCodeTag(_TextoutInlineTag):
def end_lightscript(self):
return '`'
class TextoutNoEvalTag(_TextoutInlineTag):
class NoEvalTag(_InlineTag):
""" Inline code tag, simply doesn't evaluate the content.
Example uses:
[noeval][b]wow, and no need for monospace![/b][/noeval]
"""
aliases = ('[noeval]',)
aliases = ('[noeval]', '[nobbcode]')
generic = False
raw = True

View File

@ -4,15 +4,18 @@
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
from ..base import TextoutBlockTag as _TextoutBlockTag
import urllib.parse as _urlparse
from .. import BlockTag as _BlockTag
from html import escape as _htmlescape
__all__ = ["TextoutImageTag", "TextoutAdminImageTag"]
__all__ = ["ImageTag", "AdminImageTag"]
class TextoutImageTag(_TextoutBlockTag):
class ImageTag(_BlockTag):
""" The main tag for displaying an image.
Example uses:
[img]picture_url[/img]
[img=center]picture_url[/img]
[img=12x24]picture_url[/img]
@ -24,12 +27,25 @@ class TextoutImageTag(_TextoutBlockTag):
raw = True
def prepare(self, name, value):
_align = {
'center': ('center', False),
'centre': ('center', False),
'left': ('left', False),
'right': ('right', False),
'float': (None, True),
'floating': (None, True),
'float-left': ('left', True),
'float-center': ('center', True),
'float-centre': ('center', True),
'float-right': ('right', True),
}
self._width = None
self._height = None
self._align = None
self._float = False
for arg in ("", value)[value != None].split('|'):
for arg in ("", value)[value is not None].split('|'):
if not arg:
pass
elif arg[0] in '0123456789x':
@ -37,28 +53,36 @@ class TextoutImageTag(_TextoutBlockTag):
self._height = None
dim = arg.split('x')
try: self._width = int(dim[0])
except: pass
try: self._height = int(dim[1])
except: pass
elif arg in ('center', 'left', 'right'):
self._align = arg
elif arg in ('float-left', 'float-right'):
self._align = arg[6:]
self._float = True
elif arg in ('float', 'floating'):
self._float = True
try:
self._width = int(dim[0])
except ValueError:
pass
try:
self._height = int(dim[1])
except ValueError:
pass
elif arg in _align:
al, fl = _align[arg]
if al is not None:
self._align = al
if fl:
self._float = True
def preprocess(self, content):
for prefix in ('http://', 'https://', 'ftp://', '/'):
if content.startswith(prefix):
break
else:
raise Exception("No allowed prefix!")
try:
self._image = self.image(content)
except:
url = _urlparse.urlparse(content)
if url.scheme not in ('http', 'https'):
raise Exception("No allowed prefix!")
self._url = content
self._image = content
def content_html(self):
if isinstance(self._image, str):
url = _htmlescape(self._image)
return '<p><a href="{}">{}</a></p>'.format(url, url)
style = []
cls = []
if self._width:
@ -70,25 +94,25 @@ class TextoutImageTag(_TextoutBlockTag):
elif self._width:
style.append('height: auto')
if self._float:
cls.append('img-float')
if self._align:
cls.append('img-float-{}'.format(self._align or 'right'))
elif self._align:
cls.append('img-{}'.format(self._align))
return '<img src="{}"{}{} />'.format(\
_htmlescape(self._url),
return '<img src="{}"{}{} />'.format(_htmlescape(self._image.embed),
' 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')
url = self._image.embed.replace('[', '%5B').replace(']', '%5D')
return '[[image:{}]]'.format(url)
class TextoutAdminImageTag(TextoutImageTag):
class AdminImageTag(ImageTag):
""" This tag is special for Planète Casio, as it takes images from
the `ad`ministration's image folder.
It just adds this folder's prefix.
Example uses:
[adimg]some_picture.png[/img]
[adimg=center]some_picture.png[/img]
[adimg=12x24]some_picture.png[/img]
@ -99,8 +123,7 @@ class TextoutAdminImageTag(TextoutImageTag):
aliases = ('[adimg]',)
def preprocess(self, content):
path = content
# FIXME: check image URL!
self._url = 'https://www.planet-casio.com/images/ad/' + path
self._url = 'https://www.planet-casio.com/images/ad/' + content
self._checkurl()
# End of file.

View File

@ -0,0 +1,46 @@
#!/usr/bin/env python3
#******************************************************************************
# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
import urllib.parse as _urlparse
from .. import Image as _Image
__all__ = ["GenericImage"]
class GenericImage(_Image):
""" Get a direct image. Actually this doesn't test anything, we should
use like the Embed module again, as for videos. """
# FIXME: make that disappear one day for the OpenWebImage.
def __init__(self, content):
url = _urlparse.urlparse(content)
if url.scheme not in ('http', 'https'):
raise Exception("No allowed prefix!")
self.embed = content
# WARNING: This is only for demonstration sake. Do not use without a cache!
# This demonstration class uses the `embed-python` module.
#
#from embed import Embed as _Embed
#
#class OpenWebImage(_Image):
# """ Decentralized way to gather an image data. """
#
# def __init__(self, url):
# u = _urlparse.urlparse(url)
# if not u.scheme in ('https',):
# raise Exception
#
# embed = _Embed(url)
# embed = embed.embed
# assert embed['type'] == 'image'
#
# self.embed = embed['url']
# End of file.

View File

@ -4,17 +4,18 @@
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
from ..base import TextoutInlineTag as _TextoutInlineTag
from .. import InlineTag as _InlineTag
import re as _re
__all__ = ["TextoutLabelTag", "TextoutTargetTag"]
__all__ = ["LabelTag", "TargetTag"]
_labelexpr = _re.compile('^[a-z0-9-]{1,16}$', _re.I)
class TextoutLabelTag(_TextoutInlineTag):
class LabelTag(_InlineTag):
""" The label tag, defines an anchor at a point of the post.
Example uses:
[label=installation]Installation de tel logiciel... (no ending req.)
[label=compilation][/label] Compilation de tel logiciel...
"""
@ -33,10 +34,11 @@ class TextoutLabelTag(_TextoutInlineTag):
name = self.tweak("label_prefix", "") + self._label
return '<a name="{}"></a>'.format(name)
class TextoutTargetTag(_TextoutInlineTag):
class TargetTag(_InlineTag):
""" The goto tag, links to an anchor defined in the post.
Example uses:
[target=installation]Check out the installation manual[/target]!
"""

176
textoutpc/builtin/_Link.py Executable file
View File

@ -0,0 +1,176 @@
#!/usr/bin/env python3
#******************************************************************************
# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
from .. import InlineTag as _InlineTag
from html import escape as _htmlescape
__all__ = ["LinkTag", "ProfileTag", "TopicTag", "TutorialTag", "ProgramTag"]
class LinkTag(_InlineTag):
""" The main link tag.
Example uses:
[url=https://example.org/hi]Go to example.org[/url]!
[url=/Fr/index.php][/url]
[url]https://random.org/randomize.php[/url] """
aliases = ('[url]',)
raw = True
def _validate(self):
for prefix in ('http://', 'https://', 'ftp://', '/', '#'):
if self._url.startswith(prefix):
break
else:
raise Exception("No allowed prefix!")
def prepare(self, name, value):
self._url = None
# If there is no value, wait until we have a content to
# decide if we are valid or not.
if value is None:
self.preprocess = self._preprocess_if_no_value
return
# Otherwise, get the URL and validate.
self._url = value
self._validate()
self.default = self._default_if_value
def _default_if_value(self):
return self._url
def _preprocess_if_no_value(self, content):
self._url = content
self._validate()
def begin_html(self):
return '<a href="{}">'.format(_htmlescape(self._url))
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 ProfileTag(LinkTag):
""" A special link tag for Planète Casio's profiles.
Adds the prefix to the content, and sets the value.
Example uses:
[profil]Cakeisalie5[/] """
aliases = ('[profil]', '[profile]')
def prepare(self, name, value):
# Override the LinkTag's prepare method.
pass
def preprocess(self, content):
# Check the username's content (see `check(…, "pseudo")` in PCv42).
username = content
allowed = "abcdefghijklmnopqrstuvwxyz0123456789_ -."
if any(car not in allowed for car in allowed):
raise ValueError("invalid username!")
# Prepare the tag.
self._url = 'https://www.planet-casio.com/Fr/compte/voir_profil.php' \
'?membre={}'.format(username)
self._validate()
class TopicTag(LinkTag):
""" A special link tag for Planète Casio's topics.
Adds the prefix to the content, and sets the value.
Example uses:
[topic]234[/] """
aliases = ('[topic]',)
def prepare(self, name, value):
# Override the LinkTag's prepare method.
pass
def preprocess(self, content):
# Check the topic number.
topic = int(content)
# Prepare the tag.
self._url = 'https://www.planet-casio.com/Fr/forums/' \
f'lecture_sujet.php?id={topic}'
self._validate()
class TutorialTag(LinkTag):
""" A special link tag for Planète Casio's tutorial.
Adds the prefix to the content, and sets the value.
Example uses:
[tutorial]71[/tutorial]
[tuto]71[/tuto] """
aliases = ('[tutorial]', '[tuto]')
def prepare(self, name, value):
# Override the LinkTag's prepare method.
pass
def preprocess(self, content):
# Check the topic number.
topic = int(content)
# Prepare the tag.
self._url = 'https://www.planet-casio.com/Fr/programmation/' \
f'tutoriels.php?id={topic}'
self._validate()
class ProgramTag(LinkTag):
""" A special link tag for a Planète Casio's program.
Adds the prefix to the content, and sets the value.
Example uses:
[program]3598[/program]
[prog]3598[/prog] """
aliases = ('[program]', '[prog]')
def prepare(self, name, value):
# Override the LinkTag's prepare method.
pass
def preprocess(self, content):
# Check the program number.
program = int(content)
# Prepare the tag.
self._url = 'https://www.planet-casio.com/Fr/programmes/' \
f'voir_un_programme_casio.php?showid={program}'
self._validate()
# End of file.

113
textoutpc/builtin/_List.py Normal file
View File

@ -0,0 +1,113 @@
#!/usr/bin/env python3
#******************************************************************************
# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
from .. import BlockTag as _BlockTag
__all__ = ["ListTag", "ListElementTag"]
# Bullet style names.
_ol_list_style_types = {
'disc': 'disc',
'circle': 'circle',
'square': 'square',
}
_ul_list_style_types = {
'1': 'decimal',
'a': 'lower-alpha',
'A': 'upper-alpha',
'i': 'lower-roman',
'I': 'upper-roman',
}
_list_style_types = _ol_list_style_types.copy()
_list_style_types.update(_ul_list_style_types)
_ul_lst_names = set(_ul_list_style_types.keys())
_ol_lst_names = set(_ol_list_style_types.keys())
# Tag definitions.
class _ListTagBase(_BlockTag):
pass
class ListElementTag(_BlockTag):
""" List element for basic lists (see `ListTag`). """
aliases = ('[*]', '[li]')
only_in = (_ListTagBase,)
notempty = True
superblock = True
not_within_itself = True
def begin_html(self):
return '<li>'
def end_html(self):
return '</li>'
class ListTag(_ListTagBase):
""" Main tag for making basic lists.
Example use:
[ul]
[*] Item number one.
[*] Item number [b]two[/b].
[/ul]
"""
aliases = ('[list]', '[ul]', '[ol]')
notempty = True
superblock = True
allowed_tags = (ListElementTag,)
no_text = True
def prepare(self, name, value):
us = _ul_lst_names
os = _ol_lst_names
if name == '[list]' and value == None:
self._tag = 'ul'
self._style = None
elif name == '[list]' and value in us:
self._tag = 'ul'
self._style = value
elif name == '[list]' and value in os:
self._tag = 'ol'
self._style = value
elif name == '[ul]' and value == None:
self._tag = 'ul'
self._style = None
elif name == '[ul]' and value in us:
self._tag = 'ul'
self._style = value
elif name == '[ol]' and value == None:
self._tag = 'ol'
self._style = None
elif name == '[ol]' and value in os:
self._tag = 'ol'
self._style = value
else:
raise ValueError("invalid bullet style")
# Find out the HTML style name.
if self._style != None:
self._style = _list_style_types[self._style]
def begin_html(self):
tag = f'<{self._tag}'
if self._style != None:
tag += f' style="list-style-type: {self._style}"'
tag += '>'
return tag
def end_html(self):
return '</ul>'
# End of file.

View File

@ -4,14 +4,15 @@
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
from ..base import TextoutBlockTag as _TextoutBlockTag
from .. import BlockTag as _BlockTag
__all__ = ["TextoutProgressTag"]
__all__ = ["ProgressTag"]
class TextoutProgressTag(_TextoutBlockTag):
class ProgressTag(_BlockTag):
""" Progress tag, used to display the progress on anything.
Usage:
[progress=50]My great progress bar[/progress]
[progress=100][/progress] """
@ -28,11 +29,8 @@ class TextoutProgressTag(_TextoutBlockTag):
def end_html(self):
return '' \
'<div style="background-color: white; border: 1px solid black; ' \
'width: 50%; margin-top: 2px; text-align: left;">' \
'<div style="background-color: #FF3E28; color: black; ' \
'font-weight: bold; max-width: 100%; width: {}%;' \
'height: 18px;">{}%' \
'</div></div></div>'.format(self._val, self._val)
'<div class="progress">' \
'<div class="progress-inner" style="width: {}%;">{}%' \
'</div></div></div>'.format(self._val, self._val)
# End of file.

View File

@ -4,16 +4,15 @@
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
from ..base import TextoutBlockTag as _TextoutBlockTag
from html import escape as _htmlescape
from ...smileys import htmlsmileys as _htmlsmileys
from .. import BlockTag as _BlockTag
__all__ = ["TextoutQuoteTag"]
__all__ = ["QuoteTag"]
class TextoutQuoteTag(_TextoutBlockTag):
class QuoteTag(_BlockTag):
""" The main tag for quoting someone.
Example uses:
[quote]Hey, I said that![/quote]
[quote=Someone important]I said something important, and it's
multiline and [b]formatted[/b]!
@ -23,6 +22,8 @@ class TextoutQuoteTag(_TextoutBlockTag):
aliases = ('[quote]',)
superblock = True
notempty = True
procvalue = True
def prepare(self, name, value):
self._value = value
@ -30,8 +31,7 @@ class TextoutQuoteTag(_TextoutBlockTag):
def begin_html(self):
f = '<div class="citation">'
if self._value:
f += '<p><b>{} a écrit:</b></p>' \
.format(_htmlsmileys(_htmlescape(self._value)))
f += '<p><b>{} a écrit:</b></p>'.format(self._value)
return f
def end_html(self):

View File

@ -4,18 +4,19 @@
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
from ..base import TextoutInlineTag as _TextoutInlineTag
import string as _string
from .. import InlineTag as _InlineTag
__all__ = ["TextoutRotTag"]
__all__ = ["RotTag"]
class TextoutRotTag(_TextoutInlineTag):
class RotTag(_InlineTag):
""" Tag which un-rot13 a content.
Demonstration tag for content processing.
Example uses:
[rot=13]obawbhe[/rot]
[rot13]obawbhe[/rot13]
[rot13]Obawbhe[/rot13]
"""
aliases = ('[rot]', '[rot13]')
@ -23,8 +24,11 @@ class TextoutRotTag(_TextoutInlineTag):
def prepare(self, name, value):
if name == "[rot]":
rot = int(value)
assert 1 <= rot <= 25
if not value:
value = 13
else:
rot = int(value)
assert 1 <= rot <= 25
else:
rot = int(name[4:-1])

View File

@ -4,20 +4,23 @@
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
from ..base import TextoutBlockTag as _TextoutBlockTag
from html import escape as _htmlescape
from .. import BlockTag as _BlockTag
__all__ = ["TextoutShowTag"]
__all__ = ["ShowTag"]
class TextoutShowTag(_TextoutBlockTag):
class ShowTag(_BlockTag):
""" Tag which shows the HTML code that is produced by textout().
Example uses:
[show][b]hello world![/show]
"""
aliases = ('[show]',)
notempty = True
superblock = True
inlined = True
generic = False
raw = False
@ -25,7 +28,7 @@ class TextoutShowTag(_TextoutBlockTag):
return _htmlescape(content)
def begin_html(self):
return '<span style="font-family: monospace;">'
return '<span class="inline-code">'
def end_html(self):
return '</span>'

171
textoutpc/builtin/_Smileys.py Executable file
View File

@ -0,0 +1,171 @@
#!/usr/bin/env python3
#******************************************************************************
# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
from .. import Smiley as _Smiley
__all__ = ["TwistedSmiley", "EvilSmiley", "SmileSmiley", "WinkSmiley",
"SadSmiley", "GrinSmiley", "HeheSmiley", "CoolSmiley", "Cool2Smiley",
"MadSmiley", "EekSmiley", "MrGreenSmiley", "ShockedSmiley",
"ConfusedSmiley", "EyebrowsSmiley", "CrySmiley", "LolSmiley",
"SorrySmiley", "RollEyesSmiley", "WazaSmiley", "HereSmiley",
"BowSmiley", "GoodSmiley", "LoveSmiley", "OuchSmiley", "FacepalmSmiley",
"InsultsSmiley", "WhatSmiley", "ExclSmiley"]
_prefix = '/images/smileys/'
class TwistedSmiley(_Smiley):
aliases = ('>:)',)
url = _prefix + 'twisted.gif'
class EvilSmiley(_Smiley):
aliases = ('>:(', ':grr:')
url = _prefix + 'evil.gif'
class SmileSmiley(_Smiley):
aliases = (':)',)
url = _prefix + 'smile.gif'
class WinkSmiley(_Smiley):
aliases = (';)',)
url = _prefix + 'wink.gif'
class SadSmiley(_Smiley):
aliases = (':(',)
url = _prefix + 'sad.gif'
class GrinSmiley(_Smiley):
aliases = (':D', ':grin:')
url = _prefix + 'grin.gif'
class HeheSmiley(_Smiley):
aliases = (':p',)
url = _prefix + 'hehe.gif'
class CoolSmiley(_Smiley):
aliases = (':cool:',)
url = _prefix + 'cool.gif'
class Cool2Smiley(_Smiley):
aliases = ('8-)',)
url = _prefix + 'cool2.gif'
class MadSmiley(_Smiley):
aliases = (':@',)
url = _prefix + 'mad.gif'
class EekSmiley(_Smiley):
aliases = ('0_0',)
url = _prefix + 'eek.gif'
class MrGreenSmiley(_Smiley):
aliases = (':E', ':mrgreen:')
url = _prefix + 'mrgreen.gif'
class ShockedSmiley(_Smiley):
aliases = (':O',)
url = _prefix + 'shocked.gif'
class ConfusedSmiley(_Smiley):
aliases = (':s', ':oops:')
url = _prefix + 'confused2.gif'
class EyebrowsSmiley(_Smiley):
aliases = ('^^',)
url = _prefix + 'eyebrows.gif'
class CrySmiley(_Smiley):
aliases = (":'(", ":cry:")
url = _prefix + 'cry.gif'
# FIXME
#class WhistleSmiley(_Smiley):
# aliases = (":-°", ':whistle:')
# url = _prefix + 'whistle.gif'
# height = '15px'
class LolSmiley(_Smiley):
aliases = (":lol:",)
url = _prefix + 'lol.gif'
class SorrySmiley(_Smiley):
aliases = (":sry:",)
url = _prefix + 'redface.gif'
class RollEyesSmiley(_Smiley):
aliases = (":mmm:",)
url = _prefix + 'rolleyes.gif'
class WazaSmiley(_Smiley):
aliases = (":waza:",)
url = _prefix + 'waza.gif'
class HereSmiley(_Smiley):
aliases = (":here:", ":arrow:")
url = _prefix + 'pointer.gif'
class BowSmiley(_Smiley):
aliases = (":bow:",)
url = _prefix + 'bow.gif'
class GoodSmiley(_Smiley):
aliases = (":good:",)
url = _prefix + 'welldone.gif'
class LoveSmiley(_Smiley):
aliases = (":love:",)
url = _prefix + 'love.gif'
class OuchSmiley(_Smiley):
aliases = (":aie:",)
url = _prefix + 'banghead2.gif'
class FacepalmSmiley(_Smiley):
aliases = (":facepalm:",)
url = _prefix + 'facepalm.gif'
class InsultsSmiley(_Smiley):
aliases = (":argh:",)
url = _prefix + 'insults.gif'
class WhatSmiley(_Smiley):
aliases = (":?:",)
url = _prefix + 'what.gif'
class ExclSmiley(_Smiley):
aliases = (":!:",)
url = _prefix + 'excl.gif'
# End of file.

View File

@ -4,18 +4,17 @@
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
from ..base import TextoutBlockTag as _TextoutBlockTag
from html import escape as _htmlescape
from ...smileys import htmlsmileys as _htmlsmileys
from .. import BlockTag as _BlockTag
__all__ = ["TextoutSpoilerTag"]
__all__ = ["SpoilerTag"]
class TextoutSpoilerTag(_TextoutBlockTag):
class SpoilerTag(_BlockTag):
""" Hide content at first glance, force people to click to read content.
These elements can contain 'secret' elements such as solutions, source
code, or various other things.
Example uses:
[spoiler]This is hidden![/spoiler]
[spoiler=Y a quelque chose de caché!|Ah, bah en fait non :)]:E
And it's multiline, [big]and formatted[/big], as usual :D[/spoiler]
@ -23,6 +22,8 @@ class TextoutSpoilerTag(_TextoutBlockTag):
aliases = ('[spoiler]',)
superblock = True
notempty = True
procvalue = True
def prepare(self, name, value):
self._closed = "Cliquez pour découvrir"
@ -32,17 +33,16 @@ class TextoutSpoilerTag(_TextoutBlockTag):
titles = value.split('|')
if titles[0]:
self._closed = titles[0]
if len(titles) >= 2 and titles[1]:
self._open = titles[1]
if len(titles) >= 2 and (len(titles) > 2 or titles[1]):
self._open = '|'.join(titles[1:])
def begin_html(self):
return '<div class="spoiler">' \
'<div class="title on" onclick="toggleSpoiler(this.parentNode, ' \
'\'open\');">{}</div>' \
'<div class="title off" onclick="toggleSpoiler(this.parentNode, ' \
'\'close\');">{}</div>' \
'<div class="off">'.format(_htmlsmileys(_htmlescape(self._closed)),
_htmlsmileys(_htmlescape(self._open)))
'<div class="title on" onclick="toggleSpoiler(this.parentNode, ' \
'\'open\');"><p>{}</p></div>' \
'<div class="title off" onclick="toggleSpoiler(this.parentNode, ' \
'\'close\');"><p>{}</p></div>' \
'<div class="off">'.format(self._closed, self._open)
def end_html(self):
return '</div></div>'

View File

@ -4,10 +4,13 @@
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
from ..base import TextoutInlineTag as _TextoutInlineTag
from ...color import get_color
from .. import InlineTag as _InlineTag
from thcolor import Color as _Color
__all__ = ["TextoutTextTag"]
__all__ = ["TextTag"]
def _get_color(text):
return _Color.from_text(text).rgba()
# ---
# Data.
@ -30,10 +33,11 @@ _fonts = {
# Tag definition.
# ---
class TextoutTextTag(_TextoutInlineTag):
class TextTag(_InlineTag):
""" Main tag for setting text formatting.
Example uses:
[b]Bold text.[/b]
[i]Italic text.[/i]
[u]Underlined text.[/u]
@ -45,28 +49,31 @@ class TextoutTextTag(_TextoutInlineTag):
[color=blue]This as well[/color]
[color=rgb(255, 255, 255, 0.4)]BLACKNESS[/color]
[color=hsl(0, 100%, 0.5)]This will be red.[/color]
Also supports a hack used on Planète Casio for a while, which
is a CSS injection, e.g.:
[color=brown; size: 16pt]Hello world![/color]
"""
aliases = ('[b]', '[i]', '[u]', '[s]', '[strike]',
'[monospace]', '[mono]', '[font]', '[color]',
aliases = ('[css]', '[b]', '[i]', '[u]', '[o]', '[s]', '[strike]',
'[monospace]', '[mono]', '[font]', '[color]', '[c]',
'[size]', '[big]', '[small]',
'[arial]', '[comic]', '[tahoma]', '[courier]',
'[haettenschweiler]', '[red]', '[green]', '[blue]',
'[yellow]', '[maroon]', '[purple]', '[gray]',
'[grey]', '[brown]')
notempty = True
def prepare(self, name, value):
self._bold = False
self._italic = False
self._underline = False
self._overline = False
self._strike = False
self._font = None
self._color = None
self._bgcolor = None
self._size = None
# Récupérer la partie correspondant à l'injection CSS s'il y
@ -74,7 +81,7 @@ class TextoutTextTag(_TextoutInlineTag):
def get_props(value):
props = ''
if value != None:
if value is not None:
index = value.find(';')
if index >= 0:
props = value[index + 1:]
@ -85,17 +92,24 @@ class TextoutTextTag(_TextoutInlineTag):
name = name[1:-1]
props = ""
if name == "b":
if name == "css":
props = value
elif name == "b":
self._bold = True
elif name == "i":
self._italic = True
elif name == "u":
self._underline = True
elif name == "o":
self._overline = True
elif name in ("s", "strike", "striked"):
self._strike = True
elif name == "color":
elif name in ("color", 'c'):
value, props = get_props(value)
self._color = get_color(value)
self._color = _get_color(value)
elif name == 'f':
value, props = get_props(value)
self._bgcolor = _get_color(value)
elif name == "font":
value, props = get_props(value)
assert value in _fonts
@ -116,9 +130,9 @@ class TextoutTextTag(_TextoutInlineTag):
elif name in _fonts:
self._font = name
else:
self._color = get_color(name)
self._color = _get_color(name)
# Gestion des injections CSS.
# Gestion des propriétés CSS (par injection ou via `[css]`).
for prop in props.split(';'):
prop = prop.strip()
@ -131,7 +145,9 @@ class TextoutTextTag(_TextoutInlineTag):
name = name.strip()
value = ':'.join(value).strip()
if name == 'size':
if name in ('size', 'font-size'):
# Control the font size.
unit = 'pt'
if value.endswith('pt'):
value = value[:-2].rstrip()
@ -141,7 +157,7 @@ class TextoutTextTag(_TextoutInlineTag):
if not value or \
any(c != '0' for c in value[:-3]) or \
any(not c in '0123456789' for c in value[-3:]):
any(c not in '0123456789' for c in value[-3:]):
continue
value = int(value[-3:])
@ -150,6 +166,14 @@ class TextoutTextTag(_TextoutInlineTag):
if 0 < value <= 3.0:
self._size = value
elif name == 'color':
# Control the text color.
self._color = _get_color(value)
elif name == 'background-color':
# Control the background color.
self._bgcolor = _get_color(value)
def _get_css(self):
""" Get the `style` CSS classes and properties for HTML output. """
@ -161,10 +185,15 @@ class TextoutTextTag(_TextoutInlineTag):
props.append('font-weight: bold')
if self._italic:
props.append('font-style: italic')
if self._underline or self._strike:
props.append('text-decoration:{}{}'.format(' underline' \
if self._underline else '', ' line-through' \
if self._strike else ''))
if self._underline or self._strike or self._overline:
props.append('text-decoration:{}{}{}'.format(' underline'
if self._underline else '', ' line-through'
if self._strike else '', ' overline'
if self._overline else ''))
else:
if self._overline:
props.append('text-decoration:{}'.format(' overline'
if self._overline else ''))
if self._font:
props.append('font-family: ' + self._font)
@ -181,9 +210,15 @@ class TextoutTextTag(_TextoutInlineTag):
props.append('color: #%02X%02X%02X' % self._color[0:3])
if self._color[3] < 1.0:
props.append('color: rgba({}, {}, {}, {})' \
props.append('color: rgba({}, {}, {}, {})'
.format(*self._color))
if self._bgcolor and self._bgcolor[3] != 0.0:
props.append('background-color: #%02X%02X%02X' % self._color[0:3])
if self._bgcolor[3] < 1.0:
props.append('background-color: rgba({}, {}, {}, {})'
.format(*self._bgcolor))
if self._size:
props.append('font-size: {}em'.format(self._size))
@ -194,8 +229,8 @@ class TextoutTextTag(_TextoutInlineTag):
cls, props = self._get_css()
if cls or props:
props = '<span{}{}>'.format(' class="{}"'.format(' '.join(cls)) \
if cls else '', ' style="{}"'.format('; '.join(props)) \
props = '<span{}{}>'.format(' class="{}"'.format(' '.join(cls))
if cls else '', ' style="{}"'.format('; '.join(props))
if props else '')
else:
props = ''

View File

@ -4,14 +4,15 @@
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
from ..base import TextoutBlockTag as _TextoutBlockTag
from .. import BlockTag as _BlockTag
__all__ = ["TextoutTitleTag"]
__all__ = ["TitleTag"]
class TextoutTitleTag(_TextoutBlockTag):
class TitleTag(_BlockTag):
""" The title tag.
Example uses:
[title]Some title[/title]
[subtitle]Some subtitle[/subtitle]
"""
@ -20,13 +21,26 @@ class TextoutTitleTag(_TextoutBlockTag):
raw = True
def prepare(self, name, value):
level = self.tweak("title_level", "1")
if level[0] == "h":
level = level[1:]
level = int(level)
assert 1 <= level <= 5
# Name.
self._level = name[1:-1]
# HTML tag.
level += self._level == "subtitle"
self._tag = f"h{level}"
def begin_html(self):
return ('<h5>', '<h4>')[self._level == "title"]
return f'<{self._tag} class="{self._level}">'
def end_html(self):
return ('</h5>', '</h4>')[self._level == "title"]
return f'</{self._tag}>'
def begin_lightscript(self):
return '#' * ((self._level == "subtitle") + 1) + ' '

138
textoutpc/builtin/_Video.py Executable file
View File

@ -0,0 +1,138 @@
#!/usr/bin/env python3
#******************************************************************************
# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
import urllib.parse as _urlparse
from html import escape as _htmlescape
from .. import BlockTag as _BlockTag
__all__ = ["VideoTag", "YoutubeTag"]
_defaultratio_w = 16
_defaultratio_h = 9
class VideoTag(_BlockTag):
""" The video tag, puts a preview of the video whose URL is given.
Only a few 'big' services are supported for now.
Example uses:
[video]video_url[/video]
[video=4:3]video_url[/video]
[video tiny]video_url[/video tiny]
[video]https://www.youtube.com/watch?v=yhXpV8hRKxQ[/video]
"""
aliases = ('[video]', '[video tiny]')
raw = True
noinline = True
def prepare(self, name, value):
""" Prepare the video tag. """
_align = {
'center': ('center', False),
'centre': ('center', False),
'left': ('left', False),
'gauche': ('left', False),
'right': ('right', False),
'droite': ('right', False),
'float': (None, True),
'floating': (None, True),
'flotte': (None, True),
'flottant': (None, True),
'float-left': ('left', True),
'float-center': ('center', True),
'float-centre': ('center', True),
'float-right': ('right', True),
}
self._sizeclass = "video-tiny" if "tiny" in name \
else None
self._align = None
self._float = False
self._ratio = None
for arg in map(str.strip, (value or "").split('|')):
if not arg:
pass
elif arg[0] in '0123456789:':
rx, ry = _defaultratio_w, _defaultratio_h
rn = 0
rat = arg.split(':')
try: rx = int(rat[0]); rn += 1
except: pass
try: ry = int(rat[1]); rn += 1
except: pass
if rn:
self._ratio = round(ry / rx, 4)
elif arg in _align:
al, fl = _align[arg]
if al != None:
self._align = al
if fl:
self._float = True
def preprocess(self, content):
try:
self._video = self.video(content)
except:
url = _urlparse.urlparse(content)
if url.scheme not in ('http', 'https'):
raise Exception("No allowed prefix!")
self._video = content
def content_html(self):
""" Produce the embed code for the given type. """
if isinstance(self._video, str):
url = _htmlescape(self._video)
return '<p><a href="{}">{}</a></p>'.format(url, url)
align = "float-" + (self._align or "left") if self._align \
else self._align
if self._ratio:
ratio = self._ratio * 100
elif hasattr(self._video, 'ratio'):
ratio = self._video.ratio * 100
else:
ratio = round(_defaultratio_h / _defaultratio_w, 4) * 100
iratio = int(ratio)
if ratio == iratio:
ratio = iratio
ratio = str(ratio)
code = '<div class="video-wrapper{}{}"{}>' \
.format(f" {self._sizeclass}" if self._sizeclass else "",
f' img-{align}' if align else "",
f' style="padding-bottom: {ratio}%"')
code += '<iframe src="{}" frameborder="0" allowfullscreen>' \
'</iframe>'.format(self._video.embed)
return code + '</div>'
def content_lightscript(self):
url = self._url.replace('[', '%5B').replace(']', '%5D')
return '[[image:{}]]'.format(url)
class YoutubeTag(VideoTag):
""" Alias for the video tag with only the Youtube possibility.
Example uses:
[youtube]okMK1NYRySI[/youtube] """
aliases = ('[youtube]',)
def preprocess(self, content):
super().preprocess(f'https://www.youtube.com/watch?v={content}')
# End of file.

100
textoutpc/builtin/_Videos.py Executable file
View File

@ -0,0 +1,100 @@
#!/usr/bin/env python3
#******************************************************************************
# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
import re as _re
import urllib.parse as _urlparse
from .. import Video as _Video
__all__ = ["YouTubeVideo", "DailymotionVideo", "VimeoVideo"]
class YouTubeVideo(_Video):
""" Get a video from Youtube. """
_hexcode = _re.compile('[a-zA-Z0-9_-]+')
def __init__(self, url):
url = _urlparse.urlparse(url)
if url.scheme not in ('http', 'https'):
raise Exception
if url.netloc == "youtu.be":
self._id = url.path[1:]
if not self._hexcode.match(self._id):
raise ValueError("invalid id")
elif url.netloc in ('youtube.com', 'www.youtube.com'):
if url.path != '/watch':
raise ValueError("invalid id")
self._id = _urlparse.parse_qs(url.query)['v'][0]
if not self._hexcode.fullmatch(self._id):
raise Exception
else:
raise ValueError("unknown URL")
self.embed = f"https://www.youtube.com/embed/{self._id}"
class DailymotionVideo(_Video):
""" Get a video from Dailymotion. """
_dailypath = _re.compile('^/video/([a-z0-9]+)$')
def __init__(self, url):
url = _urlparse.urlparse(url)
if url.scheme not in ('http', 'https'):
raise Exception
if url.netloc in ('dailymotion.com', 'www.dailymotion.com'):
self._code = self._dailypath.match(url.path).groups()[0]
else:
raise ValueError("unknown URL")
self.embed = f"https://www.dailymotion.com/embed/video/{self._code}"
class VimeoVideo(_Video):
""" Get a video from Vimeo. """
_numcode = _re.compile('^/[0-9]+$')
def __init__(self, url):
url = _urlparse.urlparse(url)
if url.scheme not in ('http', 'https'):
raise Exception
if url.netloc in ('vimeo.com', 'www.vimeo.com'):
self._code = url.path[1:]
if not self._numcode.match(self._code):
raise ValueError("invalid video code")
else:
raise ValueError("unknown URL")
self.embed = f"https://player.vimeo.com/video/{self._code}" \
"?title=0&byline=0&portrait=0"
# WARNING: This is only for demonstration sake. Do not use without a cache!
# This demonstration class uses the `embed-python` module.
#
#from embed import Embed as _Embed
#
#class OpenWebVideo(_Video):
# """ Decentralized way to gather a video data. """
#
# def __init__(self, url):
# u = _urlparse.urlparse(url)
# if not u.scheme in ('https',):
# raise Exception
#
# embed = _Embed(url)
# embed = embed.embed
# assert embed['type'] == 'video'
#
# self.embed = embed['url']
# if 'ratio' in embed:
# self.ratio = embed['ratio'] / 100
# End of file.

35
textoutpc/builtin/__init__.py Executable file
View File

@ -0,0 +1,35 @@
#!/usr/bin/env python3
#******************************************************************************
# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
""" Built-in tags and smileys for the `textoutpc` module.
Some of these options will probably have to move to a separate module
Planète Casio-specific, but still, here we are.
"""
# Tags.
from ._Align import *
from ._Code import *
from ._Image import *
from ._Label import *
from ._Link import *
from ._List import *
from ._Progress import *
from ._Quote import *
from ._Rot import *
from ._Show import *
from ._Spoiler import *
from ._Text import *
from ._Title import *
from ._Video import *
# Other resources (smileys, multimedia).
from ._Smileys import *
from ._Images import *
from ._Videos import *
# End of file.

View File

@ -1,168 +0,0 @@
#!/usr/bin/env python3
#******************************************************************************
# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
""" 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.

View File

@ -1,257 +0,0 @@
#!/usr/bin/env python3
#******************************************************************************
# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
""" 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]
\s* (?P<rgb_g>[0-9]{1,3}) \s* ([,\\s]
\s* (?P<rgb_b>[0-9]{1,3}) \s* ([,\\s]
\s* ((?P<rgb_a_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*%
|(?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]+) ) \s*%
|(?P<hsl_sat_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*[,\\s]
\s* ((?P<hsl_lgt_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*%
|(?P<hsl_lgt_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*([,\\s/]
\s* ((?P<hsl_aph_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*%
|(?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]+) ) \s*%
|(?P<hls_lgt_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*[,\\s]
\s* ((?P<hls_sat_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*%
|(?P<hls_sat_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*([,\\s/]
\s* ((?P<hls_aph_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*%
|(?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]+) ) \s*%
|(?P<hwb_wht_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*[,\\s]
\s* ((?P<hwb_blk_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*%
|(?P<hwb_blk_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*([,\\s/]
\s* ((?P<hwb_aph_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*%
|(?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.
value = value.strip()
try: value = _named_colors[value.lower()]
except: pass
# Initialize the alpha.
alpha = 1.0
# Get the match.
match = _cr.fullmatch(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']) if match['rgb_g'] else 0
b = int(match['rgb_b']) if match['rgb_b'] else 0
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.

View File

@ -1,22 +0,0 @@
#!/usr/bin/env python3
#******************************************************************************
# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
""" 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,83 +0,0 @@
#!/usr/bin/env python3
#******************************************************************************
# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
""" Smiley conversion.
Just convert them.
"""
import regex as _re
from html import escape as _htmlescape
__all__ = ["htmlsmileys"]
# ---
# List of them.
# ---
_Smileys_prefix = "/images/smileys/"
_Smileys = {
'>:)': 'twisted.gif',
'>:(': 'evil.gif',
':)': 'smile.gif',
';)': 'wink.gif',
':(': 'sad.gif',
':D': 'grin.gif',
':p': 'hehe.gif',
'8-)': 'cool2.gif',
':@': 'mad.gif',
'0_0': 'eek.gif',
':E': 'mrgreen.gif',
':O': 'shocked.gif',
':s': 'confused2.gif',
'^^': 'eyebrows.gif',
":'(": 'cry.gif',
# ':-°': ('whistle.gif', 'height: 15px;'),
# Name-based smileys.
':lol:': 'lol.gif',
':oops:': 'confused2.gif',
':grr:': 'evil.gif',
':sry:': 'redface.gif',
':mmm:': 'rolleyes.gif',
':waza:': 'waza.gif',
# ':whistle:': ('whistle.gif', 'height: 15px;'),
':here:': 'pointer.gif',
':bow:': 'bow.gif',
':cool:': 'cool.gif',
':good:': 'welldone.gif',
':love:': 'love.gif',
':aie:': 'banghead2.gif',
':cry:': 'cry.gif',
':facepalm:': 'facepalm.gif',
':argh:': 'insults.gif',
':?:': 'what.gif',
':!:': 'excl.gif',
':arrow:': 'here.gif',
':grin:': 'grin.gif',
}
_Smileys_html = {_htmlescape(a): _Smileys_prefix + b \
for a, b in _Smileys.items()}
def _Smiley_sub_html(m):
return m.group(1) + '<img src="' + _Smileys_html[m.group(2)] \
+ '">' + m.group(3)
_Smiley_html_re = _re.compile('(^|\\s)(' + '|'.join(map(_re.escape,
_Smileys_html.keys())) + ')(\\s|$)')
# ---
# Functions.
# ---
def htmlsmileys(text):
""" HTML smileys """
text = _Smiley_html_re.sub(_Smiley_sub_html, text)
text = _Smiley_html_re.sub(_Smiley_sub_html, text)
return text
# End of file.

View File

@ -1,73 +0,0 @@
#!/usr/bin/env python3
#******************************************************************************
# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
""" 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, TextoutParagraphTag
__all__ = ["TextoutParagraphTag", "TextoutBlockTag", "TextoutInlineTag",
"get_tag"]
# ---
# Gathering of the tags.
# ---
def _extract_tags(module):
""" Load all tags from a module. """
tags = []
# Obtain the list of properties from the module.
try:
ds = module.__all__
except:
ds = dir(module)
# Get the submodules from the module (usually different files in the
# tags module folder).
for submodule in (obj for name, obj in ((nm, getattr(module, nm)) \
for nm in ds) if (name == '__init__' or name[0] != '_') \
and _ismod(obj)):
obtained = _extract_tags(submodule)
tags += [tag for tag in obtained if not any(tag is x for x in tags)]
del obtained
# Extract the tags from the current module.
for tag in (obj for name, obj in ((nm, getattr(module, nm)) for nm in ds) \
if name[0] != '_' and _isclass(obj) and issubclass(obj, _TextoutTag)):
tags.append(tag)
return tags
_tags = _extract_tags(__import__("builtin", \
{'__name__': __name__ + '.__init__'}, level=1))
_aliases = {alias: tag for alias, tag in \
[(alias, tag) for tag in _tags for alias in tag.aliases]}
# ---
# Function to get a tag.
# ---
def get_tag(name, value, output_type = 'html', tweaks = {}):
""" Find a tag using its name. """
try:
als = _aliases[name]
als = als(name, value, output_type, tweaks)
return als
except:
return None
# End of file.

View File

@ -1,146 +0,0 @@
#!/usr/bin/env python3
#******************************************************************************
# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
""" 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`). """
import inspect as _inspect
from functools import partial as _p
from inspect import getargspec as _getargspec
__all__ = ["TextoutTag", "TextoutBlockTag", "TextoutInlineTag",
"TextoutParagraphTag"]
def _getargscount(func):
try:
return len(_inspect.getfullargspec(func).args)
except:
return len(_inspect.getargspec(func).args)
# ---
# Main base tag class.
# For more about defining a tag, see `/TAGS.md`.
# ---
class TextoutTag:
""" The textout tag base class.
Is initialized with these values:
<name (only special)><content><name>
[<name (with brackets)>]<content>[/<name>]
[<name (with brackets)>=<value>]<content>[/<name>] """
aliases = ()
def __init__(self, name, value, ot, tweaks):
""" Initialize the textout tag with the documented members. """
# Store internal data.
self.__output_type = ot
self.__tweaks = tweaks
self.output_type = ot
# Call both prepare functions.
if hasattr(self, 'prepare'):
try:
assert _getargscount(self.prepare) == 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'):
if hasattr(self, 'preprocess_' + ot):
self.__preprocess0 = self.preprocess
self.preprocess = self.__preprocess_double
elif hasattr(self, 'preprocess_' + ot):
self.preprocess = getattr(self, 'preprocess_' + ot)
if hasattr(self, 'preprocess'):
self.__preprocess2 = self.preprocess
self.preprocess = self.__preprocess_and_prepare
else:
self.__after_preprocess()
if hasattr(self, 'default_' + ot):
self.default = getattr(self, 'default_' + ot)
def __preprocess_double(self, content):
""" Preprocess using the two methods. """
ct = self.__preprocess0(content)
if ct != None: content = ct; del ct
ct = self.__preprocess1(content)
if ct != None: content = ct; del ct
return content
def __preprocess_and_prepare(self, content):
""" Preprocess and do the things after. """
ret = self.__preprocess2(content)
self.__after_preprocess()
return ret
def __out(self, name):
""" Generic function to call two output functions of the same type. """
getattr(self, '__' + name)()
getattr(self, name + '_' + self.__output_type)()
def __after_preprocess(self):
ot = self.__output_type
for otype in ('begin', 'content', 'end'):
if hasattr(self, otype):
if hasattr(self, otype + '_' + ot):
setattr(self, '__' + otype, getattr(self, otype))
setattr(self, otype, _p(self.__out, otype))
elif hasattr(self, otype + '_' + ot):
setattr(self, otype, getattr(self, otype + '_' + ot))
def tweak(self, key, default = None):
try:
return self.__tweaks[key]
except KeyError:
return default
# ---
# Role-specific base tag classes.
# ---
class TextoutBlockTag(TextoutTag):
pass
class TextoutInlineTag(TextoutTag):
pass
# ---
# Default tag: paragraph.
# ---
class TextoutParagraphTag(TextoutBlockTag):
""" Main tag for basic paragraphs. """
notempty = True
def begin_html(self):
return '<p>'
def end_html(self):
return '</p>'
# End of file.

View File

@ -1,86 +0,0 @@
#!/usr/bin/env python3
#******************************************************************************
# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
from ..base import TextoutInlineTag as _TextoutInlineTag
from html import escape as _htmlescape
__all__ = ["TextoutLinkTag", "TextoutProfileTag"]
class TextoutLinkTag(_TextoutInlineTag):
""" The main link tag.
Example uses:
[url=https://example.org/hi]Go to example.org[/url]!
[url=/Fr/index.php][/url]
[url]https://random.org/randomize.php[/url]
"""
aliases = ('[url]',)
raw = True
def _validate(self):
for prefix in ('http://', 'https://', 'ftp://', '/', '#'):
if self._url.startswith(prefix):
break
else:
raise Exception("No allowed prefix!")
def prepare(self, name, value):
self._url = None
# If there is no value, wait until we have a content to
# decide if we are valid or not.
if value == None:
self.preprocess = self._preprocess_if_no_value
return
# Otherwise, get the URL and validate.
self._url = value
self._validate()
self.default = self._default_if_value
def _default_if_value(self):
return self._url
def _preprocess_if_no_value(self, content):
self._url = content
self._validate()
def begin_html(self):
return '<a href="{}">'.format(_htmlescape(self._url))
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.
Example uses:
[profil]Cakeisalie5[/profil]
"""
aliases = ('[profil]', '[profile]')
def prepare(self, name, value):
# Override the TextoutLinkTag's prepare method.
pass
def preprocess(self, content):
# FIXME: check the username content!
username = content
self._url = 'https://www.planet-casio.com/Fr/compte/voir_profil.php' \
'?membre={}'.format(username)
self._validate()
# End of file.

View File

@ -1,109 +0,0 @@
#!/usr/bin/env python3
#******************************************************************************
# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
from ..base import TextoutBlockTag as _TextoutBlockTag
import re as _re
import urllib.parse as _urlparse
from html import escape as _htmlescape
__all__ = ["TextoutVideoTag"]
_hexcode = _re.compile('[a-zA-Z0-9_]+')
_numcode = _re.compile('^/[0-9]+$')
_dailypath = _re.compile('^/video/([a-z0-9]+)$')
class TextoutVideoTag(_TextoutBlockTag):
""" The video tag, puts a preview of the video whose URL is given.
Only a few 'big' services are supported for now.
Example uses:
[video]video_url[/video]
[video tiny]video_url[/video tiny]
[video]https://www.youtube.com/watch?v=yhXpV8hRKxQ[/video]
"""
aliases = ('[video]', '[video tiny]')
raw = True
def prepare(self, name, value):
""" Prepare the video tag. """
self._sizeclass = "video-tiny" if "tiny" in name \
else "video-medium"
self._center = False
def _getvideo(self, url):
""" Try to get the video type for preprocessing. """
url = _urlparse.urlparse(url)
if not url.scheme in ('http', 'https'):
raise Exception
if url.netloc == "youtu.be":
self._id = url.path[1:]
if not _hexcode.match(self._id):
raise Exception
self._type = "youtube"
elif url.netloc in ('youtube.com', 'www.youtube.com'):
if url.path != '/watch':
raise Exception
self._id = _urlparse.parse_qs(url.query)['v'][0]
if not _hexcode.fullmatch(self._id):
raise Exception
self._type = "youtube"
elif url.netloc in ('dailymotion.com', 'www.dailymotion.com'):
self._code = _dailypath.match(url.path).groups()[0]
self._type = "dailymotion"
elif url.netloc in ('vimeo.com', 'www.vimeo.com'):
self._code = url.path[1:]
if not _numcode.match(self._code):
raise Exception
self._type = "vimeo"
else:
raise Exception
def preprocess(self, content):
self._url = content
try:
self._getvideo(content)
except:
url = _urlparse.urlparse(content)
if not url.scheme in ('http', 'https'):
raise Exception("No allowed prefix!")
self._type = None
def content_html(self):
""" Produce the embed code for the given type. """
if not self._type:
url = _htmlescape(self._url)
return '<a href="{}">{}</a>'.format(url, url)
code = '<div class="video-wrapper {}{}">'.format(self._sizeclass,
" video-center" if self._center else "")
if self._type == "youtube":
code += '<iframe ' \
'src="https://www.youtube.com/embed/{}" frameborder="0" ' \
'allowfullscreen></iframe>'.format(self._id)
elif self._type == "dailymotion":
code += '<iframe frameborder="0" ' \
'src="https://www.dailymotion.com/embed/video/{}">' \
'</iframe>'.format(self._code)
elif self._type == "vimeo":
code += '<iframe src="https://player.vimeo.com/video/{}' \
'?title=0&byline=0&portrait=0" frameborder="0" ' \
'webkitAllowFullScreen allowFullScreen>' \
'</iframe>'.format(self._code)
return code + '</div>'
def content_lightscript(self):
url = self._url.replace('[', '%5B').replace(']', '%5D')
return '[[image:{}]]'.format(url)
# End of file.

View File

@ -1,16 +0,0 @@
#!/usr/bin/env python3
#******************************************************************************
# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
""" 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.
"""
_names = ["Align", "Code", "Image", "Label", "Link", "Progress",
"Quote", "Rot", "Show", "Spoiler", "Text", "Title", "Video"]
for name in _names:
__import__(name, globals(), level=1)
# End of file.

View File

@ -1,60 +0,0 @@
#!/usr/bin/env python3
#******************************************************************************
# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
""" Autolinking (URL extraction from raw text) in HTML. """
import regex as _re
__all__ = ["htmlurls", "lightscripturls"]
# ---
# Autolinking regex.
# ---
def _sub_html(m):
sp = m.group('sp')
url = m.group('url')
aft = ''
# Hack for the last comma.
if url[-1] == ',':
url, aft = url[:-1], ','
text = '{}<a href="{}">{}</a>{}' \
.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):
(?P<ucore>[^\[\]\(\)\s]* (\[(?&ucore)\]?)* (\((?&ucore)\)?)*)*
)
""", _re.VERBOSE | _re.M)
# ---
# Main functions.
# ---
def htmlurls(text):
return _reg.sub(_sub_html, text)
def lightscripturls(text):
return _reg.sub(_sub_lightscript, text)
# End of file.