aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Touhey <thomas@touhey.fr>2019-05-06 12:28:06 +0200
committerThomas Touhey <thomas@touhey.fr>2019-05-06 12:28:06 +0200
commitbf4bb84d94a5855799d4638f7fe24c0891a89588 (patch)
treea1853f5124597049357988d5b65e7468558c46ef
Initial commit.
-rw-r--r--.gitignore9
-rw-r--r--.python-version1
-rw-r--r--LICENSE.txt21
-rw-r--r--MANIFEST.in5
-rwxr-xr-xMakefile28
-rw-r--r--Pipfile16
-rw-r--r--Pipfile.lock297
-rw-r--r--README.rst7
-rw-r--r--setup.cfg42
-rwxr-xr-xsetup.py22
-rwxr-xr-xtests/__init__.py11
-rwxr-xr-xtests/test_all.py27
-rwxr-xr-xthcolor/__init__.py24
-rwxr-xr-xthcolor/_builtin/__init__.py14
-rwxr-xr-xthcolor/_builtin/_css.py218
-rwxr-xr-xthcolor/_color.py650
-rw-r--r--thcolor/_named.py31
-rwxr-xr-xthcolor/_sys.py22
18 files changed, 1445 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..868792e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+__pycache__
+/test.py
+/*.egg-info
+/dist
+/.spyproject
+/build
+/docs/_build
+/venv
+/README.html
diff --git a/.python-version b/.python-version
new file mode 100644
index 0000000..0b2eb36
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+3.7.2
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..45ccebe
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (C) 2018 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
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..0a3c3cc
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,5 @@
+include README.rst
+include LICENSE.txt
+include MANIFEST.in
+include setup.py
+include setup.cfg
diff --git a/Makefile b/Makefile
new file mode 100755
index 0000000..3d840bc
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,28 @@
+#!/usr/bin/make -f
+ PE := pipenv run
+ ST := $(PE) ./setup.py
+ DNAME := dist/$(shell $(ST) --name)-$(shell $(ST) --version).tar.gz
+
+test tests:
+ @$(PE) pytest -s -q
+
+prepare:
+ @pipenv install --dev
+update:
+ @pipenv update --dev
+
+docs:
+ @$(ST) build_sphinx
+
+checkdocs:
+ @$(ST) checkdocs
+
+dist: $(DNAME)
+$(DNAME):
+ @$(ST) sdist
+
+upload: $(DNAME)
+ @twine upload $(DNAME)
+
+.PHONY: test tests dist docs
+# End of file.
diff --git a/Pipfile b/Pipfile
new file mode 100644
index 0000000..2b67cce
--- /dev/null
+++ b/Pipfile
@@ -0,0 +1,16 @@
+[[source]]
+url = 'https://pypi.python.org/simple'
+verify_ssl = true
+name = 'pypi'
+
+[requires]
+python_version = '3.7'
+
+[packages]
+regex = '*'
+
+[dev-packages]
+sphinx = '*'
+"collective.checkdocs" = '*'
+pudb = '*'
+pytest = '*'
diff --git a/Pipfile.lock b/Pipfile.lock
new file mode 100644
index 0000000..9758a34
--- /dev/null
+++ b/Pipfile.lock
@@ -0,0 +1,297 @@
+{
+ "_meta": {
+ "hash": {
+ "sha256": "0376534457dcacd720d6a912bf87bdf10739752646bf34d5c06ef3de9863f538"
+ },
+ "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"
+ }
+ },
+ "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:19ecf9ce9db2fce065a7a0586e07cfb4ac8614fe96edf628a264b1c70116cf8f",
+ "sha256:84d306a647cc805219916e62aab89caa97a33a1dd8c342e87a37f91073cd4746"
+ ],
+ "version": "==0.9.0"
+ },
+ "pudb": {
+ "hashes": [
+ "sha256:ac30cfc64580958ab7265decb4cabb9141f08781ff072e9a336d5a7942ce35a6"
+ ],
+ "index": "pypi",
+ "version": "==2019.1"
+ },
+ "py": {
+ "hashes": [
+ "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa",
+ "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"
+ ],
+ "version": "==1.8.0"
+ },
+ "pygments": {
+ "hashes": [
+ "sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a",
+ "sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d"
+ ],
+ "version": "==2.3.1"
+ },
+ "pyparsing": {
+ "hashes": [
+ "sha256:1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a",
+ "sha256:9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03"
+ ],
+ "version": "==2.4.0"
+ },
+ "pytest": {
+ "hashes": [
+ "sha256:3773f4c235918987d51daf1db66d51c99fac654c81d6f2f709a046ab446d5e5d",
+ "sha256:b7802283b70ca24d7119b32915efa7c409982f59913c1a6c0640aacf118b95f5"
+ ],
+ "index": "pypi",
+ "version": "==4.4.1"
+ },
+ "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"
+ }
+ }
+}
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..316be47
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,7 @@
+thcolor -- switch between color formats
+=======================================
+
+This module defines the ``Color`` primitive. For more information, read the
+documentation accessible on `the official website`_.
+
+.. _the official website: https://thcolor.touhey.pro/
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..f9e8759
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,42 @@
+[metadata]
+name = thcolor
+version = attr: thcolor.version
+url = https://thcolor.touhey.fr/
+author = Thomas Touhey
+author_email = thomas@touhey.fr
+description = color management module
+long_description = file: README.rst
+keywords = textout, color, parser, css
+license = MIT
+classifiers =
+ Development Status :: 2 - Pre-Alpha
+ License :: OSI Approved :: MIT License
+ Operating System :: OS Independent
+ Programming Language :: Python :: 3
+ Intended Audience :: Developers
+ Topic :: Text Processing :: Markup :: HTML
+
+[options]
+zip_safe = False
+include_package_data = True
+packages = thcolor
+test_suite = test
+install_requires =
+ regex
+
+[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
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..e228793
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python3
+#******************************************************************************
+# Copyright (C) 2019 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
+# This file is part of the thcolor Python 3.x module, which is MIT-licensed.
+#******************************************************************************
+""" Setup script for the textoutpc Python package and script. """
+
+from setuptools import setup as _setup
+
+kwargs = {}
+
+try:
+ from sphinx.setup_command import BuildDoc as _BuildDoc
+ kwargs['cmdclass'] = {'build_sphinx': _BuildDoc}
+except:
+ pass
+
+# Actually, most of the project's data is read from the `setup.cfg` file.
+
+_setup(**kwargs)
+
+# End of file.
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100755
index 0000000..47268e6
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python3
+#******************************************************************************
+# Copyright (C) 2019 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
+# This file is part of the thcolor project, which is MIT-licensed.
+#******************************************************************************
+""" Unit tests for the `thcolor` Python module. """
+
+# This file is only there to indicate that the folder is a module.
+# It doesn't actually contain code.
+
+# End of file.
diff --git a/tests/test_all.py b/tests/test_all.py
new file mode 100755
index 0000000..73fa900
--- /dev/null
+++ b/tests/test_all.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python3
+#******************************************************************************
+# Copyright (C) 2019 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
+# This file is part of the thcolor project, which is MIT-licensed.
+#******************************************************************************
+""" Unit tests for the thcolor color decoding and management module. """
+
+import pytest
+from thcolor import Color
+
+_rgbcolors = (
+ ('blue', ( 0, 0, 255, 1.0)),
+ ('#12345F', ( 18, 52, 95, 1.0)),
+ ('#123', ( 17, 34, 51, 1.0)),
+ ('123', ( 1, 2, 3, 1.0)),
+ ('chucknorris', (192, 0, 0, 1.0)),
+ ('rgb(1, 22,242)', ( 1, 22, 242, 1.0)),
+ (' rgb (1,22, 242 , 50.0% )', ( 1, 22, 242, 0.5)),
+ ('rgba(1,22,242,0.500)', ( 1, 22, 242, 0.5)),
+ ('rbga(5, 7)', ( 5, 0, 7, 1.0)),
+ ('hsl(0, 1,50.0%)', (255, 0, 0, 1.0)))
+
+@pytest.mark.parametrize('test_input,expected', _rgbcolors)
+def test_rgba(test_input, expected):
+ assert Color.from_text(test_input).rgba() == expected
+
+# End of file.
diff --git a/thcolor/__init__.py b/thcolor/__init__.py
new file mode 100755
index 0000000..959b647
--- /dev/null
+++ b/thcolor/__init__.py
@@ -0,0 +1,24 @@
+#!/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.
+
+ The functions in this module do not aim at being totally compliant with
+ the W3C standards, although it is inspired from it.
+"""
+
+from ._color import Color
+from ._named import NamedColors
+from ._builtin import (CSS1NamedColors, CSS2NamedColors, CSS3NamedColors,
+ CSS4NamedColors)
+
+__all__ = ["version", "Color", "NamedColors",
+ "CSS1NamedColors", "CSS2NamedColors", "CSS3NamedColors",
+ "CSS4NamedColors"]
+
+version = "0.1"
+
+# End of file.
diff --git a/thcolor/_builtin/__init__.py b/thcolor/_builtin/__init__.py
new file mode 100755
index 0000000..b34966a
--- /dev/null
+++ b/thcolor/_builtin/__init__.py
@@ -0,0 +1,14 @@
+#!/usr/bin/env python3
+#******************************************************************************
+# Copyright (C) 2019 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
+# This file is part of the thcolor project, which is MIT-licensed.
+#******************************************************************************
+""" Named colors references, using various sources. """
+
+from ._css import (CSS1NamedColors, CSS2NamedColors, CSS3NamedColors,
+ CSS4NamedColors)
+
+__all__ = ["CSS1NamedColors", "CSS2NamedColors", "CSS3NamedColors",
+ "CSS4NamedColors"]
+
+# End of file.
diff --git a/thcolor/_builtin/_css.py b/thcolor/_builtin/_css.py
new file mode 100755
index 0000000..5cea361
--- /dev/null
+++ b/thcolor/_builtin/_css.py
@@ -0,0 +1,218 @@
+#!/usr/bin/env python3
+#******************************************************************************
+# Copyright (C) 2019 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 """
+
+from .._color import Color as _Color
+from .._named import NamedColors as _NamedColors
+
+__all__ = ["CSS1NamedColors", "CSS2NamedColors", "CSS3NamedColors",
+ "CSS4NamedColors"]
+
+def _rgb(hex):
+ return _Color.from_text(hex, _NamedColors())
+
+class CSS1NamedColors(_NamedColors):
+ """ Named colors from CSS Level 1:
+ https://www.w3.org/TR/CSS1/ """
+
+ __colors = {
+ 'black': _rgb('#000000'),
+ 'silver': _rgb('#c0c0c0'),
+ 'gray': _rgb('#808080'),
+ 'white': _rgb('#ffffff'),
+
+ 'maroon': _rgb('#800000'),
+ 'red': _rgb('#ff0000'),
+ 'purple': _rgb('#800080'),
+ 'fuchsia': _rgb('#ff00ff'),
+ 'green': _rgb('#008000'),
+ 'lime': _rgb('#00ff00'),
+ 'olive': _rgb('#808000'),
+ 'yellow': _rgb('#ffff00'),
+ 'navy': _rgb('#000080'),
+ 'blue': _rgb('#0000ff'),
+ 'teal': _rgb('#008080'),
+ 'aqua': _rgb('#00ffff')}
+
+ def get(self, name):
+ if name == 'transparent':
+ return _Color(_Color.Type.RGB, 0, 0, 0, 0)
+
+ try:
+ return self.__colors[name]
+ except:
+ return super().get(name)
+
+class CSS2NamedColors(CSS1NamedColors):
+ """ Named colors from CSS Level 2 (Revision 1):
+ https://www.w3.org/TR/CSS2/ """
+
+ __colors = {
+ 'orange': _rgb('#ffa500')}
+
+ def get(self, name):
+ try:
+ return self.__colors[name]
+ except:
+ return super().get(name)
+
+class CSS3NamedColors(CSS2NamedColors):
+ """ Named colors from CSS Color Module Level 3:
+ https://drafts.csswg.org/css-color-3/ """
+
+ __colors = {
+ 'darkblue': _rgb('#00008B'),
+ 'mediumblue': _rgb('#0000CD'),
+ 'darkgreen': _rgb('#006400'),
+ 'darkcyan': _rgb('#008B8B'),
+ 'deepskyblue': _rgb('#00BFFF'),
+ 'darkturquoise': _rgb('#00CED1'),
+ 'mediumspringgreen': _rgb('#00FA9A'),
+ 'springgreen': _rgb('#00FF7F'),
+ 'cyan': _rgb('#00FFFF'),
+ 'midnightblue': _rgb('#191970'),
+ 'dodgerblue': _rgb('#1E90FF'),
+ 'lightseagreen': _rgb('#20B2AA'),
+ 'forestgreen': _rgb('#228B22'),
+ 'seagreen': _rgb('#2E8B57'),
+ 'darkslategray': _rgb('#2F4F4F'),
+ 'darkslategrey': _rgb('#2F4F4F'),
+ 'limegreen': _rgb('#32CD32'),
+ 'mediumseagreen': _rgb('#3CB371'),
+ 'turquoise': _rgb('#40E0D0'),
+ 'royalblue': _rgb('#4169E1'),
+ 'steelblue': _rgb('#4682B4'),
+ 'darkslateblue': _rgb('#483D8B'),
+ 'mediumturquoise': _rgb('#48D1CC'),
+ 'indigo': _rgb('#4B0082'),
+ 'darkolivegreen': _rgb('#556B2F'),
+ 'cadetblue': _rgb('#5F9EA0'),
+ 'cornflowerblue': _rgb('#6495ED'),
+ 'mediumaquamarine': _rgb('#66CDAA'),
+ 'dimgray': _rgb('#696969'),
+ 'dimgrey': _rgb('#696969'),
+ 'slateblue': _rgb('#6A5ACD'),
+ 'olivedrab': _rgb('#6B8E23'),
+ 'slategray': _rgb('#708090'),
+ 'slategrey': _rgb('#708090'),
+ 'lightslategray': _rgb('#778899'),
+ 'lightslategrey': _rgb('#778899'),
+ 'mediumslateblue': _rgb('#7B68EE'),
+ 'lawngreen': _rgb('#7CFC00'),
+ 'chartreuse': _rgb('#7FFF00'),
+ 'aquamarine': _rgb('#7FFFD4'),
+ 'grey': _rgb('#808080'),
+ 'skyblue': _rgb('#87CEEB'),
+ 'lightskyblue': _rgb('#87CEFA'),
+ 'blueviolet': _rgb('#8A2BE2'),
+ 'darkred': _rgb('#8B0000'),
+ 'darkmagenta': _rgb('#8B008B'),
+ 'saddlebrown': _rgb('#8B4513'),
+ 'darkseagreen': _rgb('#8FBC8F'),
+ 'lightgreen': _rgb('#90EE90'),
+ 'mediumpurple': _rgb('#9370DB'),
+ 'darkviolet': _rgb('#9400D3'),
+ 'palegreen': _rgb('#98FB98'),
+ 'darkorchid': _rgb('#9932CC'),
+ 'yellowgreen': _rgb('#9ACD32'),
+ 'sienna': _rgb('#A0522D'),
+ 'brown': _rgb('#A52A2A'),
+ 'darkgray': _rgb('#A9A9A9'),
+ 'darkgrey': _rgb('#A9A9A9'),
+ 'lightblue': _rgb('#ADD8E6'),
+ 'greenyellow': _rgb('#ADFF2F'),
+ 'paleturquoise': _rgb('#AFEEEE'),
+ 'lightsteelblue': _rgb('#B0C4DE'),
+ 'powderblue': _rgb('#B0E0E6'),
+ 'firebrick': _rgb('#B22222'),
+ 'darkgoldenrod': _rgb('#B8860B'),
+ 'mediumorchid': _rgb('#BA55D3'),
+ 'rosybrown': _rgb('#BC8F8F'),
+ 'darkkhaki': _rgb('#BDB76B'),
+ 'mediumvioletred': _rgb('#C71585'),
+ 'indianred': _rgb('#CD5C5C'),
+ 'peru': _rgb('#CD853F'),
+ 'chocolate': _rgb('#D2691E'),
+ 'tan': _rgb('#D2B48C'),
+ 'lightgray': _rgb('#D3D3D3'),
+ 'lightgrey': _rgb('#D3D3D3'),
+ 'thistle': _rgb('#D8BFD8'),
+ 'orchid': _rgb('#DA70D6'),
+ 'goldenrod': _rgb('#DAA520'),
+ 'palevioletred': _rgb('#DB7093'),
+ 'crimson': _rgb('#DC143C'),
+ 'gainsboro': _rgb('#DCDCDC'),
+ 'plum': _rgb('#DDA0DD'),
+ 'burlywood': _rgb('#DEB887'),
+ 'lightcyan': _rgb('#E0FFFF'),
+ 'lavender': _rgb('#E6E6FA'),
+ 'darksalmon': _rgb('#E9967A'),
+ 'violet': _rgb('#EE82EE'),
+ 'palegoldenrod': _rgb('#EEE8AA'),
+ 'lightcoral': _rgb('#F08080'),
+ 'khaki': _rgb('#F0E68C'),
+ 'aliceblue': _rgb('#F0F8FF'),
+ 'honeydew': _rgb('#F0FFF0'),
+ 'azure': _rgb('#F0FFFF'),
+ 'sandybrown': _rgb('#F4A460'),
+ 'wheat': _rgb('#F5DEB3'),
+ 'beige': _rgb('#F5F5DC'),
+ 'whitesmoke': _rgb('#F5F5F5'),
+ 'mintcream': _rgb('#F5FFFA'),
+ 'ghostwhite': _rgb('#F8F8FF'),
+ 'salmon': _rgb('#FA8072'),
+ 'antiquewhite': _rgb('#FAEBD7'),
+ 'linen': _rgb('#FAF0E6'),
+ 'lightgoldenrodyellow': _rgb('#FAFAD2'),
+ 'oldlace': _rgb('#FDF5E6'),
+ 'magenta': _rgb('#FF00FF'),
+ 'deeppink': _rgb('#FF1493'),
+ 'orangered': _rgb('#FF4500'),
+ 'tomato': _rgb('#FF6347'),
+ 'hotpink': _rgb('#FF69B4'),
+ 'coral': _rgb('#FF7F50'),
+ 'darkorange': _rgb('#FF8C00'),
+ 'lightsalmon': _rgb('#FFA07A'),
+ 'lightpink': _rgb('#FFB6C1'),
+ 'pink': _rgb('#FFC0CB'),
+ 'gold': _rgb('#FFD700'),
+ 'peachpuff': _rgb('#FFDAB9'),
+ 'navajowhite': _rgb('#FFDEAD'),
+ 'moccasin': _rgb('#FFE4B5'),
+ 'bisque': _rgb('#FFE4C4'),
+ 'mistyrose': _rgb('#FFE4E1'),
+ 'blanchedalmond': _rgb('#FFEBCD'),
+ 'papayawhip': _rgb('#FFEFD5'),
+ 'lavenderblush': _rgb('#FFF0F5'),
+ 'seashell': _rgb('#FFF5EE'),
+ 'cornsilk': _rgb('#FFF8DC'),
+ 'lemonchiffon': _rgb('#FFFACD'),
+ 'floralwhite': _rgb('#FFFAF0'),
+ 'snow': _rgb('#FFFAFA'),
+ 'lightyellow': _rgb('#FFFFE0'),
+ 'ivory': _rgb('#FFFFF0')}
+
+ def get(self, name):
+ try:
+ return self.__colors[name]
+ except:
+ return super().get(name)
+
+class CSS4NamedColors(CSS3NamedColors):
+ """ Named colors from CSS Color Module Level 4:
+ https://drafts.csswg.org/css-color/ """
+
+ __colors = {
+ 'rebeccapurple': _rgb('#663399')}
+
+ def get(self, name):
+ try:
+ return self.__colors[name]
+ except:
+ return super().get(name)
+
+# End of file.
diff --git a/thcolor/_color.py b/thcolor/_color.py
new file mode 100755
index 0000000..e10a719
--- /dev/null
+++ b/thcolor/_color.py
@@ -0,0 +1,650 @@
+#!/usr/bin/env python3
+#******************************************************************************
+# Copyright (C) 2019 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
+# This file is part of the thcolor 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 enum import Enum as _Enum
+
+from ._named import NamedColors as _NamedColors
+from ._sys import hls_to_rgb as _hls_to_rgb, hwb_to_rgb as _hwb_to_rgb
+
+__all__ = ["Color"]
+
+# ---
+# Color decoding elements.
+# ---
+
+_cr = _re.compile(r"""
+ rgba?\s*\(
+ \s* ((?P<rgb_r>[0-9]{1,3})
+ |(?P<rgb_r_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s]
+ \s* ((?P<rgb_g>[0-9]{1,3})
+ |(?P<rgb_g_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s]
+ \s* ((?P<rgb_b>[0-9]{1,3})
+ |(?P<rgb_b_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s/]
+ \s* ((?P<rgb_a_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*%
+ |(?P<rgb_a_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*
+ )?)?)?
+ \)|
+ rbga?\s*\(
+ \s* ((?P<rbg_r>[0-9]{1,3})
+ |(?P<rbg_r_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s]
+ \s* ((?P<rbg_b>[0-9]{1,3})
+ |(?P<rbg_b_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s]
+ \s* ((?P<rbg_g>[0-9]{1,3})
+ |(?P<rbg_g_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s/]
+ \s* ((?P<rbg_a_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*%
+ |(?P<rbg_a_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*
+ )?)?)?
+ \)|
+ brga?\s*\(
+ \s* ((?P<brg_b>[0-9]{1,3})
+ |(?P<brg_b_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s]
+ \s* ((?P<brg_r>[0-9]{1,3})
+ |(?P<brg_r_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s]
+ \s* ((?P<brg_g>[0-9]{1,3})
+ |(?P<brg_g_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s/]
+ \s* ((?P<brg_a_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*%
+ |(?P<brg_a_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*
+ )?)?)?
+ \)|
+ bgra?\s*\(
+ \s* ((?P<bgr_b>[0-9]{1,3})
+ |(?P<bgr_b_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s]
+ \s* ((?P<bgr_g>[0-9]{1,3})
+ |(?P<bgr_g_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s]
+ \s* ((?P<bgr_r>[0-9]{1,3})
+ |(?P<bgr_r_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s/]
+ \s* ((?P<bgr_a_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*%
+ |(?P<bgr_a_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*
+ )?)?)?
+ \)|
+ grba?\s*\(
+ \s* ((?P<grb_g>[0-9]{1,3})
+ |(?P<grb_g_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s]
+ \s* ((?P<grb_r>[0-9]{1,3})
+ |(?P<grb_r_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s]
+ \s* ((?P<grb_b>[0-9]{1,3})
+ |(?P<grb_b_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s/]
+ \s* ((?P<grb_a_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*%
+ |(?P<grb_a_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*
+ )?)?)?
+ \)|
+ gbra?\s*\(
+ \s* ((?P<gbr_g>[0-9]{1,3})
+ |(?P<gbr_g_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s]
+ \s* ((?P<gbr_b>[0-9]{1,3})
+ |(?P<gbr_b_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s]
+ \s* ((?P<gbr_r>[0-9]{1,3})
+ |(?P<gbr_r_per> ([0-9]+\.?|[0-9]*\.[0-9]+)) \s*%) \s* ([,\\s/]
+ \s* ((?P<gbr_a_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*%
+ |(?P<gbr_a_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*
+ )?)?)?
+ \)|
+ lab\s*\(
+ \s* (?P<lab_l>-?[0-9]{1,3}) \s* [,\\s]
+ \s* (?P<lab_a>-?[0-9]{1,3}) \s* [,\\s]
+ \s* (?P<lab_b>-?[0-9]{1,3}) \s* ([,\\s/]
+ \s* ((?P<lab_a_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*%
+ |(?P<lab_a_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*
+ )?
+ \)|
+ lch\s*\(
+ \s* (?P<lch_l>-?[0-9]{1,3}) \s* [,\\s]
+ \s* (?P<lch_ch>-?[0-9]{1,3}) \s* [,\\s]
+ \s* (?P<lch_hue>-? ([0-9]+\.?|[0-9]*\.[0-9]+) )
+ (?P<lch_agl>deg|grad|rad|turn|) \s* ([,\\s/]
+ \s* ((?P<lch_a_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*%
+ |(?P<lch_a_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*
+ )?
+ \)|
+ gray\s*\(
+ \s* (?P<gray_l>-?[0-9]{1,3}) \s* ([,\\s/]
+ \s* ((?P<gray_a_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*%
+ |(?P<gray_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)
+
+_rgb = ('rgb', 'rbg', 'brg', 'bgr', 'grb', 'gbr')
+
+# ---
+# Formats.
+# ---
+
+def _byte(name, value):
+ try:
+ assert value == int(value)
+ assert 0 <= value < 256
+ except (AssertionError, TypeError, ValueError):
+ raise ValueError(f"{name} should be a byte between 0 and 255") \
+ from None
+
+ return value
+
+def _percentage(name, value):
+ try:
+ assert value == float(value)
+ assert 0.0 <= value <= 1.0
+ except (AssertionError, TypeError, ValueError):
+ raise ValueError(f"{name} should be a proportion between 0 " \
+ "and 1.0") from None
+
+ return value
+
+def _hue(name, value):
+ raise ValueError(f"{name} is not a valid hue") from None
+
+# ---
+# Color class definition.
+# ---
+
+class _ColorType(_Enum):
+ """ The color type. """
+
+ """ Invalid color. """
+ INVALID = 0
+
+ """ RGB/A color. """
+ RGB = 1
+
+ """ HSL/A color. """
+ HSL = 2
+
+ """ HWB/A color. """
+ HWB = 3
+
+class Color:
+ """ Represent a color with all of its available formats. """
+
+ # Properties to work with:
+ #
+ # `_type`: the type as one of the Color.Type constants.
+ # `_alpha`: alpha value.
+ # `_r`, `_g`, `_b`: rgb components, as bytes.
+ # `_hue`: hue for HSL and HWB notations.
+ # `_sat`, `_lgt`: saturation and light for HSL.
+ # `_wht`, `_blk`: whiteness and blackness for HWB.
+
+ Type = _ColorType
+
+ def __init__(self, *args, **kwargs):
+ self._type = Color.Type.INVALID
+ self.set(*args, **kwargs)
+
+ def __repr__(self):
+ args = (('type', self._type),)
+ if self._type == Color.Type.RGB:
+ args += (('red', self._r), ('green', self._g), ('blue', self._b))
+ elif self._type == Color.Type.HSL:
+ args += (('hue', self._hue), ('saturation', self._sat),
+ ('lightness', self._lgt))
+ elif self._type == Color.Type.HWB:
+ args += (('hue', self._hue), ('whiteness', self._wht),
+ ('blackness', self._blk))
+
+ argtext = ', '.join(f'{key}: {repr(value)}' for key, value in args)
+ return f"{self.__class__.__name__}({argtext})"
+
+ # ---
+ # Management methods.
+ # ---
+
+ def set(self, *args, **kwargs):
+ """ Set the color. """
+
+ args = list(args)
+
+ def _decode_varargs(*keys):
+ # Check for each key.
+
+ results = ()
+
+ for names, convert_func, *value in keys:
+ for name in names:
+ if name in kwargs:
+ if args:
+ raise TypeError(f"{self.__class__.__name__}() " \
+ f"got multiple values for argument {name}")
+
+ raw_result = kwargs.pop(name)
+ break
+ else:
+ name = names[0]
+ if args:
+ raw_result = args.pop(0)
+ elif value:
+ raw_result = value[0] if len(value) == 1 else value
+ else:
+ raise TypeError(f"{self.__class__.__name__}() " \
+ "missing a required positional argument: " \
+ f"{name}")
+
+ result = convert_func(name, raw_result)
+ results += (result,)
+
+ # Check for keyword arguments for which keys are not in the set.
+
+ if kwargs:
+ raise TypeError(f"{next(iter(kwargs.keys()))} is an invalid " \
+ f"keyword argument for type {type}")
+
+ return results
+
+ # ---
+ # Main function body.
+ # ---
+
+ # Check for the type.
+
+ if args:
+ try:
+ type = kwargs.pop('type')
+ except:
+ type = args.pop(0)
+ else:
+ if isinstance(args[0], Color.Type):
+ raise TypeError(f"{self.__class__.__name__}() got " \
+ "multiple values for argument 'type'")
+ else:
+ try:
+ type = kwargs.pop('type')
+ except:
+ type = self._type
+ if type == Color.Type.INVALID:
+ raise TypeError(f"{self.__class__.__name__}() missing " \
+ "required argument: 'type'")
+
+ try:
+ type = Color.Type(type)
+ except:
+ type = Color.Type.RGB
+
+ # Initialize the properties.
+
+ if type == Color.Type.RGB:
+ self._r, self._g, self._b, self._alpha = _decode_varargs(\
+ (('r', 'red'), _byte),
+ (('g', 'green'), _byte),
+ (('b', 'blue'), _byte),
+ (('a', 'alpha'), _percentage, 1.0))
+ elif type == Color.Type.HSL:
+ self._hue, self._sat, self._lig, self._alpha = _decode_varargs(\
+ (('h', 'hue'), _hue),
+ (('s', 'sat', 'saturation'), _percentage),
+ (('l', 'lig', 'light', 'lightness'), _percentage),
+ (('a', 'alpha'), _percentage, 1.0))
+ elif type == Color.Type.HWB:
+ self._hue, self._wht, self._blk, self._alpha = _decode_varargs(\
+ (('h', 'hue'), _hue),
+ (('w', 'white', 'whiteness'), _percentage),
+ (('b', 'black', 'blackness'), _percentage),
+ (('a', 'alpha'), _percentage, 1.0))
+ else:
+ raise ValueError(f"invalid color type: {type}")
+
+ # Once the arguments have been tested to be valid, we can set the
+ # type.
+
+ self._type = type
+
+ # ---
+ # Properties.
+ # ---
+
+ @property
+ def alpha(self):
+ """ Get the alpha. """
+
+ return self._alpha
+
+ @property
+ def a(self):
+ """ Alias for the `alpha` property. """
+
+ return self.alpha
+
+ @property
+ def red(self):
+ """ Get the red component. """
+
+ r, _, _ = self.rgb()
+ return r
+
+ @property
+ def r(self):
+ """ Alias for the `red` property. """
+
+ return self.r
+
+ @property
+ def green(self):
+ """ Get the green component. """
+
+ _, g, _ = self.rgb()
+ return g
+
+ @property
+ def g(self):
+ """ Alias for the `green` property. """
+
+ return self.green
+
+ @property
+ def blue(self):
+ """ Get the blue component. """
+
+ _, _, b = self.rgb()
+ return b
+
+ @property
+ def b(self):
+ """ Alias for the `blue` property. """
+
+ return self.blue
+
+ # ---
+ # Conversion methods.
+ # ---
+
+ def rgb(self):
+ """ Get the (red, green, blue) components of the color. """
+
+ if self._type == Color.Type.RGB:
+ return (self._r, self._g, self._b)
+ elif self._type == Color.Type.HSL:
+ r, g, b = _hls_to_rgb(self._hue, self._lgt, self._sat)
+ return (r, g, b)
+ elif self._type == Color.Type.HWB:
+ r, g, b = _hwb_to_rgb(self._hue, self._wht, self._blk)
+ return (r, g, b)
+
+ raise ValueError(f"color type {self._type} doesn't translate to rgb")
+
+ def rgba(self):
+ """ Get the (red, green, blue, alpha) components of the color. """
+
+ r, g, b = self.rgb()
+ alpha = self._alpha
+
+ return (r, g, b, alpha)
+
+ # ---
+ # Static methods for decoding.
+ # ---
+
+ def from_str(*args, **kwargs):
+ """ Alias for `from_text()`. """
+
+ return Color.from_text(value)
+
+ def from_string(*args, **kwargs):
+ """ Alias for `from_text()`. """
+
+ return Color.from_text(value)
+
+ def from_text(value, named = None):
+ """ Get a color from a string. """
+
+ if named is None:
+ named = _NamedColors.default()
+ if not isinstance(named, _NamedColors):
+ raise ValueError("named is not a NamedColors instance")
+
+ # Check if is a named color.
+
+ value = value.strip()
+
+ try:
+ return named.get(value)
+ except:
+ pass
+
+ # Initialize the alpha.
+
+ alpha = 1.0
+
+ # Get the match.
+
+ match = _cr.fullmatch(value)
+ if not match:
+ raise ValueError("invalid color string")
+
+ match = match.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
+
+ return Color(Color.Type.RGB, r, g, b, alpha)
+ elif any(match[key + '_r'] or match[key + '_r_per'] for key in _rgb):
+ # Extract the values.
+
+ for key in _rgb:
+ if not match[key + '_r'] and not match[key + '_r_per']:
+ continue
+
+ r = match[f'{key}_r']
+ rp = match[f'{key}_r_per']
+ g = match[f'{key}_g']
+ gp = match[f'{key}_g_per']
+ b = match[f'{key}_b']
+ bp = match[f'{key}_b_per']
+ ap = match[f'{key}_a_per']
+ af = match[f'{key}_a_flt']
+ break
+
+ r = int(r) if r else int(int(rp) * 255 / 100)
+ g = int(g) if g else int(int(gp) * 255 / 100) if gp else 0
+ b = int(b) if b else int(int(bp) * 255 / 100) if bp else 0
+
+ if ap:
+ alpha = float(ap) / 100.0
+ elif af:
+ alpha = float(af)
+
+ return Color(Color.Type.RGB, r, g, b, alpha)
+ 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
+
+ return Color(Color.Type.HSL, hue = hue,
+ saturation = sat, lightness = lgt)
+ 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
+
+ return Color(Color.Type.HWB, hue, wht, blk)
+
+ raise ValueError("unsupported format yet")
+
+# End of file.
diff --git a/thcolor/_named.py b/thcolor/_named.py
new file mode 100644
index 0000000..48150fc
--- /dev/null
+++ b/thcolor/_named.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python3
+#******************************************************************************
+# Copyright (C) 2019 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
+# This file is part of the textoutpc project, which is MIT-licensed.
+#******************************************************************************
+""" Named color reference, parent class. """
+
+__all__ = ["NamedColors"]
+
+_default_named_colors = None
+
+class NamedColors:
+ """ Color reference for named colors. """
+
+ def __init__(self):
+ pass
+
+ def get(self, name):
+ raise KeyError(f"{name}: no such color") from None
+
+ def default():
+ global _default_named_colors
+
+ if _default_named_colors is not None:
+ return _default_named_colors
+
+ from ._builtin import CSS4NamedColors
+ _default_named_colors = CSS4NamedColors()
+ return _default_named_colors
+
+# End of file.
diff --git a/thcolor/_sys.py b/thcolor/_sys.py
new file mode 100755
index 0000000..686c61f
--- /dev/null
+++ b/thcolor/_sys.py
@@ -0,0 +1,22 @@
+#!/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.