PortalAuth, Papers, and CursedScreech Updates (#87)
* Version 1.9 * Version 2.0 * Version 1.6 * Updated Papers to v2.0 * Replaced readKeys.sh with cfgNginx.py * Fixed PKCS12 export bug Co-authored-by: combsn <combsn@usc.edu>master
parent
f1ca07b311
commit
aa43cb5e23
|
@ -313,7 +313,7 @@ class CursedScreech extends Module {
|
||||||
$data = array();
|
$data = array();
|
||||||
exec("pgrep -lf " . $procName, $data);
|
exec("pgrep -lf " . $procName, $data);
|
||||||
$output = explode(" ", $data[0]);
|
$output = explode(" ", $data[0]);
|
||||||
if (strpos($output[2], $procName) !== False) {
|
if (strpos($output[1], "python") !== False) {
|
||||||
return $output[0];
|
return $output[0];
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -651,4 +651,4 @@ class CursedScreech extends Module {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
December 26, 2019
|
||||||
|
<br /><br />
|
||||||
|
- Fixed bug in latest firmware that saved module settings in an invalid state causing issues when running Sein.<br />
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
kuro_key=
|
kuro_key=
|
||||||
target_key=
|
target_key=
|
||||||
client_serial=File does not exist
|
client_serial=File does not exist
|
||||||
iface_name=br-lan
|
iface_name=wlan2
|
||||||
iface_ip=172.16.42.1
|
iface_ip=192.168.0.138
|
||||||
mcast_group=231.253.78.29
|
mcast_group=231.253.78.29
|
||||||
mcast_port=19578
|
mcast_port=19578
|
||||||
target_list=/pineapple/modules/CursedScreech/includes/forest/targets.log
|
target_list=/pineapple/modules/CursedScreech/includes/forest/targets.log
|
||||||
|
|
|
@ -6,4 +6,4 @@ if [ $# -lt 1 ]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
ifconfig $1 | grep inet | awk '{split($2,a,":"); print a[2]}'
|
ifconfig $1 | grep inet | awk '{split($2,a,":"); print a[2]}' | tr -d '\n'
|
||||||
|
|
|
@ -6,5 +6,5 @@
|
||||||
"tetra"
|
"tetra"
|
||||||
],
|
],
|
||||||
"title": "CursedScreech",
|
"title": "CursedScreech",
|
||||||
"version": "1.5"
|
"version": "1.6"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
.vscode
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Papers
|
||||||
|
This is a module for the WiFi Pineaple NANO and TETRA that allows you to create and manage TLS/SSL certificates with ease. It will also update the nginx configuration on the Pineapple to use/remove SSL automatically.
|
||||||
|
|
||||||
|
Check out the [tutorial video](https://www.youtube.com/watch?v=XQ6qmouMtS4) for Papers.
|
|
@ -83,7 +83,10 @@ class Papers extends Module
|
||||||
break;
|
break;
|
||||||
case 'loadCertProps':
|
case 'loadCertProps':
|
||||||
$this->loadCertificateProperties($this->request->certName);
|
$this->loadCertificateProperties($this->request->certName);
|
||||||
break;
|
break;
|
||||||
|
case 'loadSSHKeys':
|
||||||
|
$this->loadSSHKeys($this->request->keyName);
|
||||||
|
break;
|
||||||
case 'downloadKeys':
|
case 'downloadKeys':
|
||||||
$this->downloadKeys($this->request->parameters->name, $this->request->parameters->type);
|
$this->downloadKeys($this->request->parameters->name, $this->request->parameters->type);
|
||||||
break;
|
break;
|
||||||
|
@ -255,28 +258,19 @@ class Papers extends Module
|
||||||
$cryptInfo = array();
|
$cryptInfo = array();
|
||||||
$argString = "";
|
$argString = "";
|
||||||
|
|
||||||
$cryptInfo['-k'] = $keyName;
|
$cryptInfo['-k'] = "{$keyName}.key";
|
||||||
|
$cryptInfo['-a'] = $params['algo'];
|
||||||
|
|
||||||
// Check if the certificate should be encrypted
|
// Check if the certificate should be encrypted
|
||||||
if (array_key_exists('encrypt', $params)) {
|
if (array_key_exists('encrypt', $params)) {
|
||||||
$argString = "--encrypt ";
|
$argString = "--encrypt ";
|
||||||
|
|
||||||
$cryptInfo['-a'] = (array_key_exists('algo', $params)) ? $params['algo'] : False;
|
|
||||||
$cryptInfo['-p'] = (array_key_exists('pkey_pass', $params)) ? $params['pkey_pass'] : False;
|
|
||||||
|
|
||||||
if (!$cryptInfo['-a'] || !$cryptInfo['-p']) {
|
|
||||||
$this->logError("Build Certificate Error", "The public and private keys were generated successfully but an algorithm or password were not supplied for encryption. The certs can still be found in your SSL store.");
|
|
||||||
$this->respond(false, "Build finished with errors. Check the logs for details.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Check if the certificates should be placed into an encrypted container
|
// Check if the certificates should be placed into an encrypted container
|
||||||
if (array_key_exists('container', $params)) {
|
if (array_key_exists('container', $params)) {
|
||||||
|
$cryptInfo['--pubkey'] = "{$keyName}.cer";
|
||||||
$cryptInfo['-c'] = (array_key_exists('container', $params)) ? $params['container'] : False;
|
$cryptInfo['-c'] = (array_key_exists('container', $params)) ? $params['container'] : False;
|
||||||
$cryptInfo['-calgo'] = (array_key_exists('c_algo', $params)) ? $params['c_algo'] : False;
|
|
||||||
$cryptInfo['-cpass'] = (array_key_exists('c_pass', $params)) ? $params['c_pass'] : False;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build an argument string with all available arguments
|
// Build an argument string with all available arguments
|
||||||
foreach ($cryptInfo as $k => $v) {
|
foreach ($cryptInfo as $k => $v) {
|
||||||
if (!$v) {continue;}
|
if (!$v) {continue;}
|
||||||
|
@ -284,11 +278,11 @@ class Papers extends Module
|
||||||
}
|
}
|
||||||
$argString = rtrim($argString);
|
$argString = rtrim($argString);
|
||||||
|
|
||||||
// Execute encryptKeys.sh with the parameters and check for errors
|
// Execute encryptRSAKeys.sh with the parameters and check for errors
|
||||||
$retData = array();
|
$retData = array();
|
||||||
exec(__SCRIPTS__ . "encryptKeys.sh " . $argString, $retData);
|
exec("echo " . escapeshellcmd($params['pkey_pass']) . " | " . __SCRIPTS__ . "encryptRSAKeys.sh {$argString}", $retData);
|
||||||
$res = implode("\n", $retData);
|
if (end($retData) != "Complete") {
|
||||||
if ($res != "Complete") {
|
$res = implode("\n", $retData);
|
||||||
$this->logError("Certificate Encryption Error", "The public and private keys were generated successfully but encryption failed with the following error:\n\n" . $res);
|
$this->logError("Certificate Encryption Error", "The public and private keys were generated successfully but encryption failed with the following error:\n\n" . $res);
|
||||||
$this->respond(false, "Build finished with errors. Check the logs for details.");
|
$this->respond(false, "Build finished with errors. Check the logs for details.");
|
||||||
return;
|
return;
|
||||||
|
@ -298,16 +292,16 @@ class Papers extends Module
|
||||||
}
|
}
|
||||||
|
|
||||||
private function encryptKey($keyName, $keyType, $algo, $pass) {
|
private function encryptKey($keyName, $keyType, $algo, $pass) {
|
||||||
$retData = array();
|
$retData = array();
|
||||||
$argString = "encryptKeys.sh --encrypt -k " . $keyName . " -a " . $algo . " -p " . $pass;
|
$cmdString = "encryptRSAKeys.sh --encrypt -k {$keyName}.key -a {$algo}";
|
||||||
|
|
||||||
if ($keyType == "SSH") {
|
if ($keyType == "SSH") {
|
||||||
$argString .= " --ssh";
|
$cmdString = "encryptSSHKey.sh -k {$keyName}.key";
|
||||||
}
|
}
|
||||||
|
|
||||||
exec(__SCRIPTS__ . $argString, $retData);
|
exec("echo " . escapeshellcmd($pass) . " | " . __SCRIPTS__ . $cmdString, $retData);
|
||||||
$res = implode("\n", $retData);
|
if (end($retData) != "Complete") {
|
||||||
if ($res != "Complete") {
|
$res = implode("\n", $retData);
|
||||||
$this->logError("Key Encryption Error", "The following error occurred:\n\n" . $res);
|
$this->logError("Key Encryption Error", "The following error occurred:\n\n" . $res);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -315,16 +309,16 @@ class Papers extends Module
|
||||||
}
|
}
|
||||||
|
|
||||||
private function decryptKey($keyName, $keyType, $pass) {
|
private function decryptKey($keyName, $keyType, $pass) {
|
||||||
$retData = array();
|
$retData = array();
|
||||||
$argString = "decryptKeys.sh -k " . $keyName . " -p " . $pass;
|
$cmdString = "decryptRSAKeys.sh -k {$keyName}.key";
|
||||||
|
|
||||||
|
if ($keyType == "SSH") {
|
||||||
|
$cmdString = "decryptSSHKey.sh -k {$keyName}.key";
|
||||||
|
}
|
||||||
|
|
||||||
if ($keyType == "SSH") {
|
exec("echo " . escapeshellcmd($pass) . " | " . __SCRIPTS__ . $cmdString, $retData);
|
||||||
$argString .= " --ssh";
|
if (end($retData) != "Complete") {
|
||||||
}
|
$res = implode("\n", $retData);
|
||||||
|
|
||||||
exec(__SCRIPTS__ . $argString, $retData);
|
|
||||||
$res = implode("\n", $retData);
|
|
||||||
if ($res != "Complete") {
|
|
||||||
$this->logError("Key Decryption Error", "The following error occurred:\n\n" . $res);
|
$this->logError("Key Decryption Error", "The following error occurred:\n\n" . $res);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -374,7 +368,7 @@ class Papers extends Module
|
||||||
$retData = array();
|
$retData = array();
|
||||||
$res = [];
|
$res = [];
|
||||||
|
|
||||||
exec(__SCRIPTS__ . "getCertInfo.sh -k " . $cert, $retData);
|
exec(__SCRIPTS__ . "getCertInfo.sh -k {$cert}.cer", $retData);
|
||||||
if (count($retData) == 0) {
|
if (count($retData) == 0) {
|
||||||
$this->respond(false);
|
$this->respond(false);
|
||||||
return false;
|
return false;
|
||||||
|
@ -386,12 +380,23 @@ class Papers extends Module
|
||||||
$key = $parts[0];
|
$key = $parts[0];
|
||||||
$val = $parts[1];
|
$val = $parts[1];
|
||||||
$res[$key] = $val;
|
$res[$key] = $val;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$res['privkey'] = file_get_contents(__SSLSTORE__ . "{$cert}.key");
|
||||||
|
$res['certificate'] = file_get_contents(__SSLSTORE__ . "{$cert}.cer");
|
||||||
|
|
||||||
// Return success and the contents of the tmp file
|
// Return success and the contents of the tmp file
|
||||||
$this->respond(true, null, $res);
|
$this->respond(true, null, $res);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function loadSSHKeys($name) {
|
||||||
|
$this->respond(true, null, array(
|
||||||
|
"privkey" => file_get_contents(__SSHSTORE__ . "{$name}.key"),
|
||||||
|
"pubkey" => file_get_contents(__SSHSTORE__ . "{$name}.pub"))
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private function getKeys($dir) {
|
private function getKeys($dir) {
|
||||||
$keyType = ($dir == __SSLSTORE__) ? "TLS/SSL" : "SSH";
|
$keyType = ($dir == __SSLSTORE__) ? "TLS/SSL" : "SSH";
|
||||||
|
@ -436,12 +441,13 @@ class Papers extends Module
|
||||||
|
|
||||||
private function keyIsEncrypted($keyName, $keyType) {
|
private function keyIsEncrypted($keyName, $keyType) {
|
||||||
$data = array();
|
$data = array();
|
||||||
$keyDir = ($keyType == "SSH") ? __SSHSTORE__ : __SSLSTORE__;
|
$keyDir = ($keyType == "SSH") ? __SSHSTORE__ : __SSLSTORE__;
|
||||||
exec(__SCRIPTS__ . "testEncrypt.sh -k " . $keyName . " -d " . $keyDir . " 2>&1", $data);
|
$type = ($keyType == "SSH") ? "SSH" : "RSA";
|
||||||
if ($data[0] == "writing RSA key") {
|
exec(__SCRIPTS__ . "isEncrypted.sh -k {$keyName}.key -d {$keyDir} -t {$type} 2>&1", $data);
|
||||||
return false;
|
if ($data[0] == "true") {
|
||||||
} else if ($data[0] == "unable to load Private Key") {
|
|
||||||
return true;
|
return true;
|
||||||
|
} else if ($data[0] == "false") {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -536,7 +542,7 @@ class Papers extends Module
|
||||||
|
|
||||||
private function checkSSLConfig() {
|
private function checkSSLConfig() {
|
||||||
$retData = array();
|
$retData = array();
|
||||||
exec(__SCRIPTS__ . "readKeys.sh", $retData);
|
exec(__SCRIPTS__ . "cfgNginx.py --getSSLCerts", $retData);
|
||||||
return implode(" ", $retData);
|
return implode(" ", $retData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
December 26, 2019<br /></br >
|
||||||
|
- Fixed bug in latest firmware that prevented the module from detecting whether a key is encrypted.<br />
|
||||||
|
- Fixed bug in latest firmware that broke the nginx config when removing SSL certs.<br />
|
||||||
|
- Added requirement to remove SSL certs before uninstalling dependencies due to a change made by adde88 that swaps nginx/nginx-ssl as dependencies in the latest firmware.<br />
|
||||||
|
- Added requirement to install dependencies (nginx-ssl) before gaining the ability to install SSL certificates.<br />
|
||||||
|
- Updated the Status section help file.<br />
|
|
@ -0,0 +1,5 @@
|
||||||
|
July 17, 2020<br /></br >
|
||||||
|
- Fixed SSH key encryption to work with new OpenSSH format.<br />
|
||||||
|
- Added raw key contents to key view modal for easy copy/paste.<br />
|
||||||
|
- Merged encryption passphrase section with PKCS#12 export. The same password is used for both operations.<br />
|
||||||
|
- Added coreutils-base64 dependency to help determine if an SSH key is encrypted.
|
|
@ -3,8 +3,11 @@ Displays the keys currently being used by nginx on your Pineapple. These values
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
||||||
<strong>Dependencies</strong><br />
|
<strong>Dependencies</strong><br />
|
||||||
The only dependencies for Papers are zip and unzip which are downloaded via opkg. They are used to pack, and unpack, certificate archives for download/upload.
|
zip and unzip - used to pack and unpack certificate archives for download/upload.<br />
|
||||||
<br /><br />
|
coreutils-base64 - Used in the process to determine whether an SSH key is encrypted.<br />
|
||||||
|
nginx-ssl - Replaces the base nginx application.<br />
|
||||||
|
<p style="color: red">Since nginx is replaced with nginx-ssl in the installation process you may need to refresh your browser after installation if the button doesn't change to 'Uninstall'</p>
|
||||||
|
<p style="color: red">In order to uninstall dependencies you must first remove the SSL certificates from your Pineapple.</p>
|
||||||
|
|
||||||
<strong>Remove SSL</strong><br />
|
<strong>Remove SSL</strong><br />
|
||||||
This reverts the Pineapple back to its original web configuration and removes all traces of SSL from the nginx config. This does not affect the certificate store or any configuration in the Papers module.
|
This reverts the Pineapple back to its original web configuration and removes all traces of SSL from the nginx config. This does not affect the certificate store or any configuration in the Papers module.
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
user root root;
|
user root root;
|
||||||
worker_processes 1;
|
worker_processes 1;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
events {
|
events {
|
||||||
worker_connections 1024;
|
worker_connections 1024;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
http {
|
http {
|
||||||
include mime.types;
|
include mime.types;
|
||||||
index index.php index.html index.htm;
|
index index.php index.html index.htm;
|
||||||
|
@ -24,8 +21,8 @@ http {
|
||||||
gzip_types text/plain application/x-javascript text/css application/xml;
|
gzip_types text/plain application/x-javascript text/css application/xml;
|
||||||
gzip_vary on;
|
gzip_vary on;
|
||||||
server {
|
server {
|
||||||
listen 80; # Port, make sure it is not in conflict with another http daemon.
|
listen 80; # Port, make sure it is not in conflict with another http daemon.
|
||||||
server_name www; # Change this, reference -> http://nginx.org/en/docs/http/server_names.html
|
server_name www; # Change this, reference -> http://nginx.org/en/docs/http/server_names.html
|
||||||
error_page 404 =200 /index.php;
|
error_page 404 =200 /index.php;
|
||||||
error_log /dev/null;
|
error_log /dev/null;
|
||||||
access_log /dev/null;
|
access_log /dev/null;
|
||||||
|
@ -51,16 +48,15 @@ http {
|
||||||
|
|
||||||
if (-f $request_filename) {
|
if (-f $request_filename) {
|
||||||
# Only throw it at PHP-FPM if the file exists (prevents some PHP exploits)
|
# Only throw it at PHP-FPM if the file exists (prevents some PHP exploits)
|
||||||
fastcgi_pass unix:/var/run/php5-fpm.sock; # The upstream determined above
|
fastcgi_pass unix:/var/run/php7-fpm.sock; # The upstream determined above
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
error_page 404 =200 /index.php;
|
error_page 404 =200 /index.php;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 1471;
|
listen 1471; # Port, make sure it is not in conflict with another http daemon.
|
||||||
server_name pineapple; # Change this, reference -> http://nginx.org/en/docs/http/server_names.html
|
server_name pineapple; # Change this, reference -> http://nginx.org/en/docs/http/server_names.html
|
||||||
error_page 404 =200 /index.php;
|
error_page 404 =200 /index.php;
|
||||||
error_log /dev/null;
|
error_log /dev/null;
|
||||||
access_log /dev/null;
|
access_log /dev/null;
|
||||||
|
@ -87,9 +83,8 @@ http {
|
||||||
|
|
||||||
if (-f $request_filename) {
|
if (-f $request_filename) {
|
||||||
# Only throw it at PHP-FPM if the file exists (prevents some PHP exploits)
|
# Only throw it at PHP-FPM if the file exists (prevents some PHP exploits)
|
||||||
fastcgi_pass unix:/var/run/php5-fpm.sock; # The upstream determined above
|
fastcgi_pass unix:/var/run/php7-fpm.sock; # The upstream determined above
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,14 +2,19 @@
|
||||||
|
|
||||||
testZip=$(opkg list-installed | grep -w 'zip')
|
testZip=$(opkg list-installed | grep -w 'zip')
|
||||||
testUnzip=$(opkg list-installed | grep -w 'unzip')
|
testUnzip=$(opkg list-installed | grep -w 'unzip')
|
||||||
|
testBase64=$(opkg list-installed | grep -w 'coreutils-base64')
|
||||||
testNginxssl=$(opkg list-installed | grep -w 'nginx-ssl')
|
testNginxssl=$(opkg list-installed | grep -w 'nginx-ssl')
|
||||||
|
|
||||||
if [ -z "$testZip" -a -z "$testNginxssl" ]; then
|
if [ -z "$testBase64" ]; then
|
||||||
echo "Not Installed";
|
echo "Not Installed";
|
||||||
else
|
else
|
||||||
if [ -z "$testUnzip" ]; then
|
if [ -z "$testZip" -a -z "$testNginxssl" ]; then
|
||||||
echo "Not Installed";
|
echo "Not Installed";
|
||||||
else
|
else
|
||||||
echo "Installed";
|
if [ -z "$testUnzip" ]; then
|
||||||
fi
|
echo "Not Installed";
|
||||||
|
else
|
||||||
|
echo "Installed";
|
||||||
|
fi
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Author: sud0nick
|
||||||
|
# Date: Dec 2018
|
||||||
|
|
||||||
|
# Location of SSL keys
|
||||||
|
SSL_STORE="/pineapple/modules/Papers/includes/ssl/";
|
||||||
|
|
||||||
|
help() {
|
||||||
|
echo "Decryption script for OpenSSL keys";
|
||||||
|
echo "Usage: ./decryptRSAKeys.sh <opts>";
|
||||||
|
echo "Use './decryptRSAKeys.sh --examples' to see example commands";
|
||||||
|
echo '';
|
||||||
|
echo 'NOTE:';
|
||||||
|
echo "Current SSL store is at $SSL_STORE";
|
||||||
|
echo '';
|
||||||
|
echo 'Parameters:';
|
||||||
|
echo '';
|
||||||
|
echo -e '\t-k:\tFile name of key to be decrypted';
|
||||||
|
echo -e '\t-p:\tPassword to use to unlock the key';
|
||||||
|
echo -e '\t-s:\tKey store to use other than default.'
|
||||||
|
echo -e '\t--help:\tDisplays this help info';
|
||||||
|
echo '';
|
||||||
|
}
|
||||||
|
|
||||||
|
examples() {
|
||||||
|
echo '';
|
||||||
|
echo 'Examples:';
|
||||||
|
echo 'Decrypt private key:';
|
||||||
|
echo './decryptRSAKeys.sh -k keyName -p password';
|
||||||
|
echo '';
|
||||||
|
echo '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$#" -lt 1 ]; then
|
||||||
|
help;
|
||||||
|
exit;
|
||||||
|
fi
|
||||||
|
|
||||||
|
KEYDIR=$SSL_STORE
|
||||||
|
read PASS
|
||||||
|
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
|
||||||
|
if [[ "$1" == "--examples" ]]; then
|
||||||
|
examples;
|
||||||
|
exit;
|
||||||
|
fi
|
||||||
|
if [[ "$1" == "--help" ]]; then
|
||||||
|
help;
|
||||||
|
exit;
|
||||||
|
fi
|
||||||
|
if [[ "$1" == "-k" ]]; then
|
||||||
|
KEY="$2";
|
||||||
|
fi
|
||||||
|
if [[ "$1" == "-p" ]]; then
|
||||||
|
PASS="$2";
|
||||||
|
fi
|
||||||
|
if [[ "$1" == "-s" ]]; then
|
||||||
|
KEYDIR="$2"
|
||||||
|
fi
|
||||||
|
|
||||||
|
shift
|
||||||
|
done;
|
||||||
|
|
||||||
|
# Generate a password on the private key
|
||||||
|
openssl rsa -in $KEYDIR/$KEY -out $KEYDIR/$KEY -passin pass:"$PASS" 2>&1 > /dev/null;
|
||||||
|
if [[ $? != 0 ]]; then
|
||||||
|
echo "Bad Password";
|
||||||
|
exit;
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Complete"
|
|
@ -0,0 +1,55 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Author: sud0nick
|
||||||
|
# Date: July 2020
|
||||||
|
|
||||||
|
# Location of SSH keys
|
||||||
|
SSH_STORE="/pineapple/modules/Papers/includes/ssh/";
|
||||||
|
|
||||||
|
help() {
|
||||||
|
echo "Encrypt OpenSSH private keys";
|
||||||
|
echo "Usage: ./encryptSSHKey.sh <opts>";
|
||||||
|
echo '';
|
||||||
|
echo 'NOTE:';
|
||||||
|
echo "Current SSH store is at $SSH_STORE";
|
||||||
|
echo '';
|
||||||
|
echo 'Parameters:';
|
||||||
|
echo '';
|
||||||
|
echo -e '\t-k:\tFile name of key to be encrypted';
|
||||||
|
echo '';
|
||||||
|
echo 'Options:';
|
||||||
|
echo '';
|
||||||
|
echo -e "\t-s:\t\tUse an SSH store other than the default."
|
||||||
|
echo '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$#" -lt 1 ]; then
|
||||||
|
help;
|
||||||
|
exit;
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Read password from pipe input
|
||||||
|
read PASS
|
||||||
|
|
||||||
|
# Fetch arguments from command line
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
|
||||||
|
if [[ "$1" == "-k" ]]; then
|
||||||
|
KEY="$2";
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$1" == "-s" ]]; then
|
||||||
|
SSH_STORE="$2";
|
||||||
|
fi
|
||||||
|
|
||||||
|
shift
|
||||||
|
done;
|
||||||
|
|
||||||
|
# Decrypt the key
|
||||||
|
ssh-keygen -o -p -P "$PASS" -N "" -q -f $SSH_STORE/$KEY 2>&1 > /dev/null
|
||||||
|
|
||||||
|
if [[ "$?" == "0" ]]; then
|
||||||
|
echo "Complete"
|
||||||
|
else
|
||||||
|
echo "false"
|
||||||
|
fi
|
|
@ -0,0 +1,97 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Author: sud0nick
|
||||||
|
# Date: Jan 2016
|
||||||
|
|
||||||
|
# Location of SSL keys
|
||||||
|
SSL_STORE="/pineapple/modules/Papers/includes/ssl/";
|
||||||
|
|
||||||
|
help() {
|
||||||
|
echo "Encryption/Export script for OpenSSL certificates";
|
||||||
|
echo "Usage: ./encryptRSAKeys.sh <opts>";
|
||||||
|
echo "Use './encryptRSAKeys.sh --examples' to see example commands";
|
||||||
|
echo '';
|
||||||
|
echo 'NOTE:';
|
||||||
|
echo "Current SSL store is at $SSL_STORE";
|
||||||
|
echo '';
|
||||||
|
echo 'Parameters:';
|
||||||
|
echo '';
|
||||||
|
echo -e '\t-k:\tFile name of key to be encrypted';
|
||||||
|
echo '';
|
||||||
|
echo 'Encryption Options:';
|
||||||
|
echo '';
|
||||||
|
echo -e '\t--encrypt:\tMust be supplied to encrypt keys';
|
||||||
|
echo -e '\t-a:\t\tAlgorithm to use for key encryption (aes256, 3des, camellia256, etc)';
|
||||||
|
echo '';
|
||||||
|
echo 'Container Options:';
|
||||||
|
echo '';
|
||||||
|
echo -e '\t-c:\t\tContainer type (pkcs12, pkcs8)';
|
||||||
|
echo -e '\t--pubkey:\tFile name of public key. Must be in selected key store.';
|
||||||
|
echo '';
|
||||||
|
}
|
||||||
|
|
||||||
|
examples() {
|
||||||
|
echo '';
|
||||||
|
echo 'Examples:';
|
||||||
|
echo 'Encrypt private key:';
|
||||||
|
echo 'echo $pass | ./encryptRSAKeys.sh -k keyName.key --encrypt -a aes256';
|
||||||
|
echo '';
|
||||||
|
echo 'Export keys to PKCS#12 container:';
|
||||||
|
echo 'echo $pass | ./encryptRSAKeys.sh -k keyName.key -c pkcs12 -a aes256';
|
||||||
|
echo '';
|
||||||
|
echo 'Encrypt private key and export to PKCS#12 container using same algo and pass:';
|
||||||
|
echo './encryptRSAKeys.sh -k keyName.key --encrypt -a aes256 -c pkcs12';
|
||||||
|
echo '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$#" -lt 1 ]; then
|
||||||
|
help;
|
||||||
|
exit;
|
||||||
|
fi
|
||||||
|
|
||||||
|
ENCRYPT_KEYS=false;
|
||||||
|
KEYDIR=$SSL_STORE;
|
||||||
|
read PASS
|
||||||
|
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
|
||||||
|
if [[ "$1" == "--examples" ]]; then
|
||||||
|
examples;
|
||||||
|
exit;
|
||||||
|
fi
|
||||||
|
if [[ "$1" == "--encrypt" ]]; then
|
||||||
|
ENCRYPT_KEYS=true;
|
||||||
|
fi
|
||||||
|
if [[ "$1" == "-a" ]]; then
|
||||||
|
ALGO="$2";
|
||||||
|
fi
|
||||||
|
if [[ "$1" == "-k" ]]; then
|
||||||
|
KEY="$2";
|
||||||
|
fi
|
||||||
|
if [[ "$1" == "-c" ]]; then
|
||||||
|
CONTAINER="$2";
|
||||||
|
fi
|
||||||
|
if [[ "$1" == "-s" ]]; then
|
||||||
|
KEYDIR="$2"
|
||||||
|
fi
|
||||||
|
if [[ "$1" == "--pubkey" ]]; then
|
||||||
|
PUBKEY="$2"
|
||||||
|
fi
|
||||||
|
|
||||||
|
shift
|
||||||
|
done;
|
||||||
|
|
||||||
|
# Generate a password on the private key
|
||||||
|
if [ $ENCRYPT_KEYS = true ]; then
|
||||||
|
openssl rsa -$ALGO -in $KEYDIR/$KEY -out $KEYDIR/$KEY -passout pass:"$PASS" 2>&1 > /dev/null;
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If a container type is present but not an algo or pass then use
|
||||||
|
# the same algo and pass from the private key
|
||||||
|
if [ -n "$CONTAINER" ]; then
|
||||||
|
|
||||||
|
# Generate a container for the public and private keys
|
||||||
|
openssl $CONTAINER -$ALGO -export -nodes -out $KEYDIR/${KEY%%.*}.pfx -inkey $KEYDIR/$KEY -in $KEYDIR/$PUBKEY -passin pass:"$PASS" -passout pass:"$PASS" 2>&1 > /dev/null;
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Complete"
|
|
@ -0,0 +1,55 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Author: sud0nick
|
||||||
|
# Date: July 2020
|
||||||
|
|
||||||
|
# Location of SSH keys
|
||||||
|
SSH_STORE="/pineapple/modules/Papers/includes/ssh/";
|
||||||
|
|
||||||
|
help() {
|
||||||
|
echo "Encrypt OpenSSH private keys";
|
||||||
|
echo "Usage: ./encryptSSHKey.sh <opts>";
|
||||||
|
echo '';
|
||||||
|
echo 'NOTE:';
|
||||||
|
echo "Current SSH store is at $SSH_STORE";
|
||||||
|
echo '';
|
||||||
|
echo 'Parameters:';
|
||||||
|
echo '';
|
||||||
|
echo -e '\t-k:\tFile name of key to be encrypted';
|
||||||
|
echo '';
|
||||||
|
echo 'Options:';
|
||||||
|
echo '';
|
||||||
|
echo -e "\t-s:\t\tUse an SSH store other than the default."
|
||||||
|
echo '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$#" -lt 1 ]; then
|
||||||
|
help;
|
||||||
|
exit;
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Read password from pipe input
|
||||||
|
read PASS
|
||||||
|
|
||||||
|
# Fetch arguments from command line
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
|
||||||
|
if [[ "$1" == "-k" ]]; then
|
||||||
|
KEY="$2";
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$1" == "-s" ]]; then
|
||||||
|
SSH_STORE="$2";
|
||||||
|
fi
|
||||||
|
|
||||||
|
shift
|
||||||
|
done;
|
||||||
|
|
||||||
|
# Encrypt the key
|
||||||
|
ssh-keygen -o -p -N "$PASS" -q -f $SSH_STORE/$KEY 2>&1 > /dev/null
|
||||||
|
|
||||||
|
if [[ "$?" == "0" ]]; then
|
||||||
|
echo "Complete"
|
||||||
|
else
|
||||||
|
echo "false"
|
||||||
|
fi
|
|
@ -4,14 +4,14 @@
|
||||||
# Date: Dec 2018
|
# Date: Dec 2018
|
||||||
|
|
||||||
# Location of SSL keys
|
# Location of SSL keys
|
||||||
ssl_store="/pineapple/modules/Papers/includes/ssl/";
|
SSL_STORE="/pineapple/modules/Papers/includes/ssl/";
|
||||||
|
|
||||||
help() {
|
help() {
|
||||||
echo "Get certificate properties via OpenSSL";
|
echo "Get certificate properties via OpenSSL";
|
||||||
echo "Usage: ./getCertInfo.sh <opts>";
|
echo "Usage: ./getCertInfo.sh <opts>";
|
||||||
echo '';
|
echo '';
|
||||||
echo 'NOTE:';
|
echo 'NOTE:';
|
||||||
echo "Current SSL store is at $ssl_store";
|
echo "Current SSL store is at $SSL_STORE";
|
||||||
echo '';
|
echo '';
|
||||||
echo 'Parameters:';
|
echo 'Parameters:';
|
||||||
echo '';
|
echo '';
|
||||||
|
@ -28,7 +28,7 @@ while [ "$#" -gt 0 ]
|
||||||
do
|
do
|
||||||
|
|
||||||
if [[ "$1" == "-k" ]]; then
|
if [[ "$1" == "-k" ]]; then
|
||||||
KEY="$ssl_store$2.cer";
|
KEY="$SSL_STORE/$2";
|
||||||
fi
|
fi
|
||||||
|
|
||||||
shift
|
shift
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
# Author: sud0nick & adde88
|
# Author: sud0nick & adde88
|
||||||
# Date: 18.10.2019
|
# Date: July 17, 2020
|
||||||
|
|
||||||
opkg update > /dev/null;
|
opkg update > /dev/null;
|
||||||
/etc/init.d/nginx stop > /dev/null;
|
|
||||||
opkg remove nginx > /dev/null;
|
opkg remove nginx > /dev/null;
|
||||||
opkg install zip unzip nginx-ssl > /dev/null;
|
opkg install zip unzip coreutils-base64 nginx-ssl > /dev/null;
|
||||||
/etc/init.d/nginx restart > /dev/null;
|
|
||||||
echo "Complete"
|
echo "Complete"
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
SSL_STORE="/pineapple/modules/Papers/includes/ssl/";
|
||||||
|
SSH_STORE="/pineapple/modules/Papers/includes/ssh/";
|
||||||
|
|
||||||
|
help() {
|
||||||
|
echo "Usage: ./testEncrypt.sh <opts>";
|
||||||
|
echo '';
|
||||||
|
echo 'NOTE:';
|
||||||
|
echo "Current SSL store is at $SSL_STORE";
|
||||||
|
echo "Current SSH store is at $SSH_STORE";
|
||||||
|
echo '';
|
||||||
|
echo 'Parameters:';
|
||||||
|
echo '';
|
||||||
|
echo -e '\t-k:\tName of key to test.';
|
||||||
|
echo -e '\t-t:\tType of key: RSA|SSH.';
|
||||||
|
echo -e "\t-s:\tKey store to use other than default."
|
||||||
|
echo '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$#" -lt 2 ]; then
|
||||||
|
help;
|
||||||
|
exit;
|
||||||
|
fi
|
||||||
|
|
||||||
|
KEYDIR=''
|
||||||
|
|
||||||
|
# Get arguments
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
|
||||||
|
if [[ "$1" == "-k" ]]; then
|
||||||
|
KEY="$2"
|
||||||
|
fi
|
||||||
|
if [[ "$1" == "-s" ]]; then
|
||||||
|
KEYDIR="$2"
|
||||||
|
fi
|
||||||
|
if [[ "$1" == "-t" ]]; then
|
||||||
|
TYPE="$2"
|
||||||
|
fi
|
||||||
|
|
||||||
|
shift
|
||||||
|
done;
|
||||||
|
|
||||||
|
# If the type selected is SSH...
|
||||||
|
if [[ "$TYPE" == "SSH" ]]; then
|
||||||
|
|
||||||
|
if [[ "$KEYDIR" == "" ]]; then
|
||||||
|
KEYDIR=$SSH_STORE
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pull the header from the key file
|
||||||
|
HEADER=$(sed '1d;$d' $KEYDIR/$KEY | base64 -d | head -c 32)
|
||||||
|
FORMAT=$(echo $HEADER | cut -c 0-14)
|
||||||
|
ENC=$(echo $HEADER | cut -c 16-19)
|
||||||
|
|
||||||
|
# Ensure the key is in OpenSSH private key format
|
||||||
|
if [[ "$FORMAT" == "openssh-key-v1" ]]; then
|
||||||
|
|
||||||
|
# Check if the key is encrypted
|
||||||
|
if [[ "$ENC" == "none" ]]; then
|
||||||
|
echo "false"
|
||||||
|
else
|
||||||
|
echo "true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
else
|
||||||
|
# This should never happen...
|
||||||
|
echo "Invalid OpenSSH key"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [[ "$TYPE" == "RSA" ]]; then
|
||||||
|
|
||||||
|
if [[ "$KEYDIR" == "" ]]; then
|
||||||
|
KEYDIR=$SSL_STORE
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the RSA key is encrypted
|
||||||
|
RES=$(openssl rsa -in $KEYDIR/$KEY -passin pass:_ 2>&1 > /dev/null)
|
||||||
|
|
||||||
|
if [[ "$?" == "1" ]]; then
|
||||||
|
echo "true"
|
||||||
|
else
|
||||||
|
echo "false"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# This should never happen when called from the module.
|
||||||
|
echo "Invalid option: $TYPE"
|
||||||
|
fi
|
||||||
|
fi
|
|
@ -1,18 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Author: sud0nick
|
|
||||||
# Date: April 6, 2016
|
|
||||||
|
|
||||||
IN_SERVER_BLOCK=false;
|
|
||||||
|
|
||||||
while read p; do
|
|
||||||
if [[ $IN_SERVER_BLOCK == false ]]; then
|
|
||||||
if [[ $p == *"listen"* && $p == *"1471"* ]]; then
|
|
||||||
IN_SERVER_BLOCK=true;
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if [[ $p == *".cer;" || $p == *".key;" ]]; then
|
|
||||||
echo $p | cut -d '/' -f 5 | tr -d ';';
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done < /etc/nginx/nginx.conf
|
|
|
@ -1,8 +1,8 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
# Author: sud0nick & adde88
|
# Author: sud0nick & adde88
|
||||||
# Date: 18.10.2019
|
# Date: July 17, 2020
|
||||||
|
|
||||||
/etc/init.d/nginx stop > /dev/null;
|
opkg update > /dev/null;
|
||||||
opkg remove zip unzip nginx-ssl > /dev/null;
|
opkg remove zip unzip coreutils-base64 nginx-ssl > /dev/null;
|
||||||
opkg install nginx > /dev/null;
|
opkg install nginx > /dev/null;
|
||||||
|
|
|
@ -32,4 +32,4 @@ fi
|
||||||
shift
|
shift
|
||||||
done;
|
done;
|
||||||
|
|
||||||
openssl rsa -in $KEYDIR$KEY -passin pass: | awk 'NR==0;'
|
openssl rsa -in $KEYDIR$KEY -passin pass:_ | awk 'NR==0;'
|
||||||
|
|
|
@ -18,8 +18,6 @@ registerController('PapersController', ['$api', '$scope', '$sce', '$http', funct
|
||||||
$scope.certEncryptAlgo = "aes256";
|
$scope.certEncryptAlgo = "aes256";
|
||||||
$scope.certEncryptPassword = "";
|
$scope.certEncryptPassword = "";
|
||||||
$scope.certExportPKCS12 = false;
|
$scope.certExportPKCS12 = false;
|
||||||
$scope.certEncryptPKCS12Algo = "aes256";
|
|
||||||
$scope.certContainerPassword = "";
|
|
||||||
$scope.certificates = "";
|
$scope.certificates = "";
|
||||||
$scope.SSLStatus = ['Loading...'];
|
$scope.SSLStatus = ['Loading...'];
|
||||||
$scope.showCertThrobber = false;
|
$scope.showCertThrobber = false;
|
||||||
|
@ -41,6 +39,14 @@ registerController('PapersController', ['$api', '$scope', '$sce', '$http', funct
|
||||||
$scope.viewCert = '';
|
$scope.viewCert = '';
|
||||||
$scope.selectedCert = '';
|
$scope.selectedCert = '';
|
||||||
$scope.loadingCert = false;
|
$scope.loadingCert = false;
|
||||||
|
$scope.certsInstalled = true;
|
||||||
|
$scope.selectedSSHKeys = '';
|
||||||
|
$scope.loadingSSHKeys = false;
|
||||||
|
$scope.sshPrivKey = '';
|
||||||
|
$scope.sshPubKey = '';
|
||||||
|
$scope.sslPrivKey = '';
|
||||||
|
$scope.sslCert = '';
|
||||||
|
|
||||||
|
|
||||||
$scope.checkDepends = (function(){
|
$scope.checkDepends = (function(){
|
||||||
$api.request({
|
$api.request({
|
||||||
|
@ -61,10 +67,10 @@ registerController('PapersController', ['$api', '$scope', '$sce', '$http', funct
|
||||||
module: 'Papers',
|
module: 'Papers',
|
||||||
action: 'installDepends'
|
action: 'installDepends'
|
||||||
},function(response){
|
},function(response){
|
||||||
$scope.checkDepends();
|
|
||||||
if (response.success === false) {
|
if (response.success === false) {
|
||||||
alert("Failed to install dependencies. Make sure you are connected to the internet.");
|
alert("Failed to install dependencies. Make sure you are connected to the internet.");
|
||||||
}
|
}
|
||||||
|
$scope.checkDepends();
|
||||||
$scope.dependsProcessing = false;
|
$scope.dependsProcessing = false;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -151,14 +157,14 @@ registerController('PapersController', ['$api', '$scope', '$sce', '$http', funct
|
||||||
params['pkey_pass'] = $scope.certEncryptPassword;
|
params['pkey_pass'] = $scope.certEncryptPassword;
|
||||||
}
|
}
|
||||||
if ($scope.certExportPKCS12 === true) {
|
if ($scope.certExportPKCS12 === true) {
|
||||||
params['container'] = "pkcs12";
|
params['container'] = "pkcs12";
|
||||||
params['c_algo'] = $scope.certEncryptPKCS12Algo;
|
params['algo'] = $scope.certEncryptAlgo;
|
||||||
if (!$scope.certContainerPassword) {
|
if (!$scope.certEncryptPassword) {
|
||||||
alert("You must enter a password for the exported container!");
|
alert("You must set a password for the private key!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
params['c_pass'] = $scope.certContainerPassword;
|
params['pkey_pass'] = $scope.certEncryptPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.showBuildThrobber = true;
|
$scope.showBuildThrobber = true;
|
||||||
$api.request({
|
$api.request({
|
||||||
|
@ -173,7 +179,29 @@ registerController('PapersController', ['$api', '$scope', '$sce', '$http', funct
|
||||||
$api.reloadNavbar();
|
$api.reloadNavbar();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$scope.loadSSHKeys = (function(key){
|
||||||
|
|
||||||
|
$scope.loadingSSHKeys = true;
|
||||||
|
$scope.sshPrivKey = '';
|
||||||
|
$scope.sshPubKey = '';
|
||||||
|
$scope.selectedSSHKeys = key;
|
||||||
|
|
||||||
|
$api.request({
|
||||||
|
module: 'Papers',
|
||||||
|
action: 'loadSSHKeys',
|
||||||
|
keyName: key
|
||||||
|
},function(response){
|
||||||
|
$scope.loadingSSHKeys = false;
|
||||||
|
if (response === false) {
|
||||||
|
$('#viewSSHKeys').modal('hide');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$scope.sshPrivKey = $sce.trustAsHtml(response.data.privkey);
|
||||||
|
$scope.sshPubKey = $sce.trustAsHtml(response.data.pubkey);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$scope.loadCertProps = (function(cert){
|
$scope.loadCertProps = (function(cert){
|
||||||
|
|
||||||
|
@ -191,7 +219,9 @@ registerController('PapersController', ['$api', '$scope', '$sce', '$http', funct
|
||||||
$('#viewCert').modal('hide');
|
$('#viewCert').modal('hide');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$scope.viewCert = response.data;
|
$scope.viewCert = response.data;
|
||||||
|
$scope.sslPrivKey = $sce.trustAsHtml($scope.viewCert.privkey);
|
||||||
|
$scope.sslCert = $sce.trustAsHtml($scope.viewCert.certificate);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -264,24 +294,22 @@ registerController('PapersController', ['$api', '$scope', '$sce', '$http', funct
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.clearForm = (function() {
|
$scope.clearForm = (function() {
|
||||||
$scope.certKeyType = "tls_ssl";
|
$scope.certKeyType = "tls_ssl";
|
||||||
$scope.certDaysValid = "365";
|
$scope.certDaysValid = "365";
|
||||||
$scope.certBitSize = "2048";
|
$scope.certBitSize = "2048";
|
||||||
$scope.certSigAlgo = "sha256";
|
$scope.certSigAlgo = "sha256";
|
||||||
$scope.certSANs = "";
|
$scope.certSANs = "";
|
||||||
$scope.certKeyName = "";
|
$scope.certKeyName = "";
|
||||||
$scope.certInfoCountry = "";
|
$scope.certInfoCountry = "";
|
||||||
$scope.certInfoState = "";
|
$scope.certInfoState = "";
|
||||||
$scope.certInfoLocality = "";
|
$scope.certInfoLocality = "";
|
||||||
$scope.certInfoOrganization = "";
|
$scope.certInfoOrganization = "";
|
||||||
$scope.certInfoSection = "";
|
$scope.certInfoSection = "";
|
||||||
$scope.certInfoCN = "";
|
$scope.certInfoCN = "";
|
||||||
$scope.certEncryptKeysBool = false;
|
$scope.certEncryptKeysBool = false;
|
||||||
$scope.certEncryptAlgo = "aes256";
|
$scope.certEncryptAlgo = "aes256";
|
||||||
$scope.certEncryptPassword = "";
|
$scope.certEncryptPassword = "";
|
||||||
$scope.certExportPKCS12 = false;
|
$scope.certExportPKCS12 = false;
|
||||||
$scope.certEncryptPKCS12Algo = "aes256";
|
|
||||||
$scope.certContainerPassword = "";
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.loadCertificates = (function() {
|
$scope.loadCertificates = (function() {
|
||||||
|
@ -291,7 +319,7 @@ registerController('PapersController', ['$api', '$scope', '$sce', '$http', funct
|
||||||
},function(response){
|
},function(response){
|
||||||
if (response.success === true) {
|
if (response.success === true) {
|
||||||
// Display certificate information
|
// Display certificate information
|
||||||
$scope.certificates = response.data;
|
$scope.certificates = response.data;
|
||||||
} else {
|
} else {
|
||||||
// Display error
|
// Display error
|
||||||
console.log("Failed to load certificates.");
|
console.log("Failed to load certificates.");
|
||||||
|
@ -410,8 +438,10 @@ registerController('PapersController', ['$api', '$scope', '$sce', '$http', funct
|
||||||
action: 'getNginxSSLCerts'
|
action: 'getNginxSSLCerts'
|
||||||
},function(response){
|
},function(response){
|
||||||
if (response.success === true) {
|
if (response.success === true) {
|
||||||
|
$scope.certsInstalled = true;
|
||||||
$scope.SSLStatus = response.data;
|
$scope.SSLStatus = response.data;
|
||||||
} else {
|
} else {
|
||||||
|
$scope.certsInstalled = false;
|
||||||
$scope.SSLStatus = response.message;
|
$scope.SSLStatus = response.message;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -467,10 +497,10 @@ registerController('PapersController', ['$api', '$scope', '$sce', '$http', funct
|
||||||
|
|
||||||
$scope.refresh = (function(){
|
$scope.refresh = (function(){
|
||||||
$scope.getLogs();
|
$scope.getLogs();
|
||||||
$scope.getChangeLogs();
|
|
||||||
$scope.clearDownloadArchive();
|
$scope.clearDownloadArchive();
|
||||||
$scope.getNginxSSLCerts();
|
$scope.getNginxSSLCerts();
|
||||||
$scope.loadCertificates();
|
$scope.checkDepends();
|
||||||
|
$scope.loadCertificates();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Upload functions
|
// Upload functions
|
||||||
|
@ -531,6 +561,6 @@ registerController('PapersController', ['$api', '$scope', '$sce', '$http', funct
|
||||||
|
|
||||||
// Init
|
// Init
|
||||||
$scope.init();
|
$scope.init();
|
||||||
$scope.checkDepends();
|
$scope.getChangeLogs();
|
||||||
$scope.refresh();
|
$scope.refresh();
|
||||||
}])
|
}])
|
||||||
|
|
|
@ -4,6 +4,10 @@
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -67,7 +71,7 @@ $(document).on('mouseenter', '.papers_hoverDanger', function() {
|
||||||
<div>
|
<div>
|
||||||
<strong>Dependencies</strong><br />
|
<strong>Dependencies</strong><br />
|
||||||
<button type="button" class="btn btn-success" ng-show="!dependsInstalled" ng-disabled="dependsProcessing" ng-hide="dependsInstalled" ng-click="installDepends();"><img src="/modules/Papers/includes/icons/glyphicons-182-download-alt.png"/> Install</button>
|
<button type="button" class="btn btn-success" ng-show="!dependsInstalled" ng-disabled="dependsProcessing" ng-hide="dependsInstalled" ng-click="installDepends();"><img src="/modules/Papers/includes/icons/glyphicons-182-download-alt.png"/> Install</button>
|
||||||
<button type="button" class="btn papers_hoverDanger" ng-show="dependsInstalled" ng-disabled="dependsProcessing" ng-hide="!dependsInstalled" ng-click="removeDepends();"><img src="/modules/Papers/includes/icons/glyphicons-198-remove-circle.png"/> Uninstall</button>
|
<button type="button" class="btn papers_hoverDanger" ng-show="dependsInstalled" ng-disabled="dependsProcessing || certsInstalled" ng-hide="!dependsInstalled" ng-click="removeDepends();"><img src="/modules/Papers/includes/icons/glyphicons-198-remove-circle.png"/> Uninstall</button>
|
||||||
<img ng-show="dependsProcessing" ng-hide="!dependsProcessing" src='/img/throbber.gif'/>
|
<img ng-show="dependsProcessing" ng-hide="!dependsProcessing" src='/img/throbber.gif'/>
|
||||||
</div><br />
|
</div><br />
|
||||||
</div>
|
</div>
|
||||||
|
@ -90,7 +94,7 @@ $(document).on('mouseenter', '.papers_hoverDanger', function() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 10px; margin-left: 10px">
|
<div style="margin-top: 10px; margin-left: 10px">
|
||||||
<button type="button" class="btn papers_hoverInfo" data-toggle="modal" data-target="#papers_uploaderView"><img src="/modules/Papers/includes/icons/glyphicons-202-upload.png"/> Upload Keys</button>
|
<button type="button" class="btn papers_hoverInfo" ng-disabled="!dependsInstalled" data-toggle="modal" data-target="#papers_uploaderView"><img src="/modules/Papers/includes/icons/glyphicons-202-upload.png"/> Upload Keys</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-responsive table-dropdown">
|
<div class="table-responsive table-dropdown">
|
||||||
|
@ -112,7 +116,7 @@ $(document).on('mouseenter', '.papers_hoverDanger', function() {
|
||||||
<td>{{ data.Encrypted }}</td>
|
<td>{{ data.Encrypted }}</td>
|
||||||
<td>
|
<td>
|
||||||
<button type="button" class="btn btn-sm papers_hoverDanger" ng-show="data.Authorized==true" ng-click="revokeSSHKey(data.Name);"><img src="/modules/Papers/includes/icons/glyphicons-205-unlock.png"/></button>
|
<button type="button" class="btn btn-sm papers_hoverDanger" ng-show="data.Authorized==true" ng-click="revokeSSHKey(data.Name);"><img src="/modules/Papers/includes/icons/glyphicons-205-unlock.png"/></button>
|
||||||
<button type="button" class="btn btn-sm papers_hoverSuccess" ng-disabled="data.Encrypted == 'Yes' && data.KeyType == 'TLS/SSL'" ng-show="data.Authorized==false" ng-click="securePineapple(data.Name, data.KeyType);"><img src="/modules/Papers/includes/icons/glyphicons-204-lock.png"/></button>
|
<button type="button" class="btn btn-sm papers_hoverSuccess" ng-disabled="(data.Encrypted == 'Yes' || !dependsInstalled) && data.KeyType == 'TLS/SSL'" ng-show="data.Authorized==false" ng-click="securePineapple(data.Name, data.KeyType);"><img src="/modules/Papers/includes/icons/glyphicons-204-lock.png"/></button>
|
||||||
|
|
||||||
<!-- Encrypt button -->
|
<!-- Encrypt button -->
|
||||||
<button type="button" class="btn btn-sm papers_hoverSuccess" data-toggle="modal" data-target="#encryptModal" ng-show="data.Encrypted == 'No'" ng-click="selectKey(data.Name, data.KeyType);"><img src="/modules/Papers/includes/icons/glyphicons-45-keys.png"/></button>
|
<button type="button" class="btn btn-sm papers_hoverSuccess" data-toggle="modal" data-target="#encryptModal" ng-show="data.Encrypted == 'No'" ng-click="selectKey(data.Name, data.KeyType);"><img src="/modules/Papers/includes/icons/glyphicons-45-keys.png"/></button>
|
||||||
|
@ -120,7 +124,8 @@ $(document).on('mouseenter', '.papers_hoverDanger', function() {
|
||||||
<!-- Decrypt button -->
|
<!-- Decrypt button -->
|
||||||
<button type="button" class="btn btn-sm btn-success papers_hoverDanger" data-toggle="modal" data-target="#decryptModal" ng-show="data.Encrypted == 'Yes'" ng-click="selectKey(data.Name, data.KeyType);"><img src="/modules/Papers/includes/icons/glyphicons-45-keys.png"/></button>
|
<button type="button" class="btn btn-sm btn-success papers_hoverDanger" data-toggle="modal" data-target="#decryptModal" ng-show="data.Encrypted == 'Yes'" ng-click="selectKey(data.Name, data.KeyType);"><img src="/modules/Papers/includes/icons/glyphicons-45-keys.png"/></button>
|
||||||
|
|
||||||
<button type="button" class="btn btn-sm papers_hoverInfo" ng-disabled="data.KeyType == 'SSH'" data-toggle="modal" data-target="#viewCert" ng-click="loadCertProps(data.Name);"><img src="/modules/Papers/includes/icons/glyphicons-28-search.png"/></button>
|
<button type="button" class="btn btn-sm papers_hoverInfo" ng-show="data.KeyType == 'TLS/SSL'" data-toggle="modal" data-target="#viewCert" ng-click="loadCertProps(data.Name);"><img src="/modules/Papers/includes/icons/glyphicons-28-search.png"/></button>
|
||||||
|
<button type="button" class="btn btn-sm papers_hoverInfo" ng-show="data.KeyType == 'SSH'" data-toggle="modal" data-target="#viewSSHKeys" ng-click="loadSSHKeys(data.Name);"><img src="/modules/Papers/includes/icons/glyphicons-28-search.png"/></button>
|
||||||
<button type="button" class="btn btn-sm papers_hoverInfo" ng-click="downloadKeys(data.Name, data.KeyType);"><img src="/modules/Papers/includes/icons/glyphicons-201-download.png"/></button>
|
<button type="button" class="btn btn-sm papers_hoverInfo" ng-click="downloadKeys(data.Name, data.KeyType);"><img src="/modules/Papers/includes/icons/glyphicons-201-download.png"/></button>
|
||||||
<button type="button" class="btn btn-sm papers_hoverDanger" ng-click="deleteKeys(data.Name, data.KeyType);"><img src="/modules/Papers/includes/icons/glyphicons-17-bin.png"/></button>
|
<button type="button" class="btn btn-sm papers_hoverDanger" ng-click="deleteKeys(data.Name, data.KeyType);"><img src="/modules/Papers/includes/icons/glyphicons-17-bin.png"/></button>
|
||||||
</td>
|
</td>
|
||||||
|
@ -136,7 +141,7 @@ $(document).on('mouseenter', '.papers_hoverDanger', function() {
|
||||||
<div class="panel-heading pointer" data-toggle="collapse" data-target="#papers_certBuilder">
|
<div class="panel-heading pointer" data-toggle="collapse" data-target="#papers_certBuilder">
|
||||||
<table style="width: 100%">
|
<table style="width: 100%">
|
||||||
<tr><td align="left">
|
<tr><td align="left">
|
||||||
<h3 class="panel-title">Build Certificates</h3>
|
<h3 class="panel-title">Build</h3>
|
||||||
</td><td align="right">
|
</td><td align="right">
|
||||||
<span class="panel-title">
|
<span class="panel-title">
|
||||||
<img ng-show="showBuildThrobber" ng-hide="!showBuildThrobber" src='/img/throbber.gif'/>
|
<img ng-show="showBuildThrobber" ng-hide="!showBuildThrobber" src='/img/throbber.gif'/>
|
||||||
|
@ -272,8 +277,13 @@ $(document).on('mouseenter', '.papers_hoverDanger', function() {
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<label>Encrypt Private Key? <input type="checkbox" ng-model="certEncryptKeysBool"></label>
|
<label>Encrypt Private Key? <input type="checkbox" ng-model="certEncryptKeysBool"></label>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" ng-show="certKeyType=='tls_ssl'" ng-hide="certKeyType=='ssh'">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<label>Export keys to PKCS#12 container? <input type="checkbox" ng-model="certExportPKCS12"></label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body" ng-show="certEncryptKeysBool" ng-hide="!certEncryptKeysBool">
|
<div class="panel-body" ng-show="certEncryptKeysBool || certExportPKCS12" ng-hide="!certEncryptKeysBool && !certExportPKCS12">
|
||||||
<div class="form-group" ng-show="certKeyType=='tls_ssl'" ng-hide="certKeyType=='ssh'">
|
<div class="form-group" ng-show="certKeyType=='tls_ssl'" ng-hide="certKeyType=='ssh'">
|
||||||
<label class="col-md-2 control-label">Algorithm</label>
|
<label class="col-md-2 control-label">Algorithm</label>
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
|
@ -294,31 +304,6 @@ $(document).on('mouseenter', '.papers_hoverDanger', function() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div class="form-group" ng-show="certKeyType=='tls_ssl'" ng-hide="certKeyType=='ssh'">
|
|
||||||
<div class="col-md-8">
|
|
||||||
<label>Export keys to PKCS#12 container? <input type="checkbox" ng-model="certExportPKCS12"></label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body" ng-show="certExportPKCS12" ng-hide="!certExportPKCS12">
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-md-2 control-label">Container Algorithm</label>
|
|
||||||
<div class="col-md-8">
|
|
||||||
<select class="form-control" style="width:auto" name="containerAlgo" ng-model="certEncryptPKCS12Algo">
|
|
||||||
<option value="aes128">AES 128</option>
|
|
||||||
<option value="aes192">AES 192</option>
|
|
||||||
<option value="aes256">AES 256</option>
|
|
||||||
<option value="des">DES</option>
|
|
||||||
<option value="des3">3DES</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-md-2 control-label">Container Password</label>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<input type="password" class="form-control" ng-model="certContainerPassword">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
<button type="button" class="btn papers_hoverInfo" ng-click="clearForm();"><img src="/modules/Papers/includes/icons/glyphicons-198-remove-circle.png"/> Clear Form</button>
|
<button type="button" class="btn papers_hoverInfo" ng-click="clearForm();"><img src="/modules/Papers/includes/icons/glyphicons-198-remove-circle.png"/> Clear Form</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -374,8 +359,38 @@ $(document).on('mouseenter', '.papers_hoverDanger', function() {
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<div ng-show="!loadingCert">
|
||||||
|
<h3>SSL Private Key</h3>
|
||||||
|
<textarea rows="20" ng-bind-html="sslPrivKey" readonly></textarea>
|
||||||
|
|
||||||
|
<h3>SSL Certificate</h3>
|
||||||
|
<textarea rows="20" ng-bind-html="sslCert" readonly></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="viewSSHKeys" class="modal fade" role="dialog">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||||
|
<h3>SSH Keys: {{ selectedSSHKeys }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<img ng-show="loadingSSHKeys" ng-hide="!loadingCert" src='/img/throbber.gif'/>
|
||||||
|
|
||||||
|
<div ng-show="!loadingSSHKeys">
|
||||||
|
<h3>SSH Private Key</h3>
|
||||||
|
<textarea rows="20" ng-bind-html="sshPrivKey" readonly></textarea>
|
||||||
|
|
||||||
|
<h3>SSH Public Key</h3>
|
||||||
|
<textarea rows="10" ng-bind-html="sshPubKey" readonly></textarea>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -403,10 +418,12 @@ $(document).on('mouseenter', '.papers_hoverDanger', function() {
|
||||||
<h3>Key Encryption : {{ selectedKey }}</h3>
|
<h3>Key Encryption : {{ selectedKey }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|
||||||
<div class="form-horizontal">
|
<div class="form-horizontal">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-md-3 control-label">Algorithm</label>
|
<div class="col-md-12">
|
||||||
|
*Algorithm is ignored for SSH keys.
|
||||||
|
</div>
|
||||||
|
<label class="col-md-3 control-label">Algorithm</label>
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
<select class="form-control" style="width:auto" name="algo" ng-model="certEncryptAlgo">
|
<select class="form-control" style="width:auto" name="algo" ng-model="certEncryptAlgo">
|
||||||
<option value="aes128">AES 128</option>
|
<option value="aes128">AES 128</option>
|
||||||
|
@ -415,8 +432,8 @@ $(document).on('mouseenter', '.papers_hoverDanger', function() {
|
||||||
<option value="des">DES</option>
|
<option value="des">DES</option>
|
||||||
<option value="des3">3DES</option>
|
<option value="des3">3DES</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-md-3 control-label">Key Password</label>
|
<label class="col-md-3 control-label">Key Password</label>
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
|
|
|
@ -6,5 +6,5 @@
|
||||||
"tetra"
|
"tetra"
|
||||||
],
|
],
|
||||||
"title": "Papers",
|
"title": "Papers",
|
||||||
"version": "1.8"
|
"version": "2.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
Dec 26, 2019
|
||||||
|
<br /><br />
|
||||||
|
- Fixed libcurl4 dependency issue<br />
|
||||||
|
- Fixed tinycss issue (again?)<br />
|
||||||
|
- Updated jQuery to 3.4.1 for injection sets<br />
|
|
@ -1,3 +1,3 @@
|
||||||
testSite=
|
testSite=
|
||||||
dataExpected=
|
dataExpected=
|
||||||
p_archive=/root/portals/
|
p_archive=
|
||||||
|
|
|
@ -318,6 +318,6 @@ class PortalCloner:
|
||||||
epFile.write("{\"name\":\"" + self.portalName + "\",\"type\":\"basic\"}")
|
epFile.write("{\"name\":\"" + self.portalName + "\",\"type\":\"basic\"}")
|
||||||
|
|
||||||
# Copy jquery to the portal directory
|
# Copy jquery to the portal directory
|
||||||
shutil.copy(self.basePath + 'includes/scripts/jquery-2.2.1.min.js', self.portalDirectory)
|
shutil.copy(self.basePath + 'includes/scripts/jquery-3.4.1.min.js', self.portalDirectory)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ fi
|
||||||
if [[ "$1" == "-install" ]]; then
|
if [[ "$1" == "-install" ]]; then
|
||||||
opkg update > /dev/null;
|
opkg update > /dev/null;
|
||||||
opkg install curl > /dev/null;
|
opkg install curl > /dev/null;
|
||||||
|
opkg install libcurl4 > /dev/null;
|
||||||
echo "Complete"
|
echo "Complete"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<script type="text/javascript" src="jquery-2.2.1.min.js"></script>
|
<script type="text/javascript" src="jquery-3.4.1.min.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.onload = init;
|
window.onload = init;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<script type="text/javascript" src="jquery-2.2.1.min.js"></script>
|
<script type="text/javascript" src="jquery-3.4.1.min.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.onload = init;
|
window.onload = init;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<script type="text/javascript" src="jquery-2.2.1.min.js"></script>
|
<script type="text/javascript" src="jquery-3.4.1.min.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.onload = setTimeout(displayLogin, 1000);
|
window.onload = setTimeout(displayLogin, 1000);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<script type="text/javascript" src="jquery-2.2.1.min.js"></script>
|
<script type="text/javascript" src="jquery-3.4.1.min.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.onload = setTimeout(displayLogin, 1000);
|
window.onload = setTimeout(displayLogin, 1000);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<script type="text/javascript" src="jquery-2.2.1.min.js"></script>
|
<script type="text/javascript" src="jquery-3.4.1.min.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.onload = setTimeout(displayAccessKeyPanel, 1000);
|
window.onload = setTimeout(displayAccessKeyPanel, 1000);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<script type="text/javascript" src="jquery-2.2.1.min.js"></script>
|
<script type="text/javascript" src="jquery-3.4.1.min.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.onload = setTimeout(displayAccessKeyPanel, 1000);
|
window.onload = setTimeout(displayAccessKeyPanel, 1000);
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,47 @@
|
||||||
|
# coding: utf-8
|
||||||
|
"""
|
||||||
|
tinycss
|
||||||
|
-------
|
||||||
|
|
||||||
|
A CSS parser, and nothing else.
|
||||||
|
|
||||||
|
:copyright: (c) 2012 by Simon Sapin.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .version import VERSION
|
||||||
|
|
||||||
|
from .css21 import CSS21Parser
|
||||||
|
from .page3 import CSSPage3Parser
|
||||||
|
from .fonts3 import CSSFonts3Parser
|
||||||
|
|
||||||
|
|
||||||
|
__version__ = VERSION
|
||||||
|
|
||||||
|
PARSER_MODULES = {
|
||||||
|
'page3': CSSPage3Parser,
|
||||||
|
'fonts3': CSSFonts3Parser,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def make_parser(*features, **kwargs):
|
||||||
|
"""Make a parser object with the chosen features.
|
||||||
|
|
||||||
|
:param features:
|
||||||
|
Positional arguments are base classes the new parser class will extend.
|
||||||
|
The string ``'page3'`` is accepted as short for
|
||||||
|
:class:`~page3.CSSPage3Parser`.
|
||||||
|
The string ``'fonts3'`` is accepted as short for
|
||||||
|
:class:`~fonts3.CSSFonts3Parser`.
|
||||||
|
:param kwargs:
|
||||||
|
Keyword arguments are passed to the parser’s constructor.
|
||||||
|
:returns:
|
||||||
|
An instance of a new subclass of :class:`CSS21Parser`
|
||||||
|
|
||||||
|
"""
|
||||||
|
if features:
|
||||||
|
bases = tuple(PARSER_MODULES.get(f, f) for f in features)
|
||||||
|
parser_class = type('CustomCSSParser', bases + (CSS21Parser,), {})
|
||||||
|
else:
|
||||||
|
parser_class = CSS21Parser
|
||||||
|
return parser_class(**kwargs)
|
|
@ -0,0 +1,383 @@
|
||||||
|
# coding: utf-8
|
||||||
|
"""
|
||||||
|
tinycss.colors3
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Parser for CSS 3 color values
|
||||||
|
http://www.w3.org/TR/css3-color/
|
||||||
|
|
||||||
|
This module does not provide anything that integrates in a parser class,
|
||||||
|
only functions that parse single tokens from (eg.) a property value.
|
||||||
|
|
||||||
|
:copyright: (c) 2012 by Simon Sapin.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import division, unicode_literals
|
||||||
|
|
||||||
|
import collections
|
||||||
|
import itertools
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .tokenizer import tokenize_grouped
|
||||||
|
|
||||||
|
|
||||||
|
class RGBA(collections.namedtuple('RGBA', ['red', 'green', 'blue', 'alpha'])):
|
||||||
|
"""An RGBA color.
|
||||||
|
|
||||||
|
A tuple of four floats in the 0..1 range: ``(r, g, b, a)``.
|
||||||
|
Also has ``red``, ``green``, ``blue`` and ``alpha`` attributes to access
|
||||||
|
the same values.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def parse_color_string(css_string):
|
||||||
|
"""Parse a CSS string as a color value.
|
||||||
|
|
||||||
|
This is a convenience wrapper around :func:`parse_color` in case you
|
||||||
|
have a string that is not from a CSS stylesheet.
|
||||||
|
|
||||||
|
:param css_string:
|
||||||
|
An unicode string in CSS syntax.
|
||||||
|
:returns:
|
||||||
|
Same as :func:`parse_color`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
tokens = list(tokenize_grouped(css_string.strip()))
|
||||||
|
if len(tokens) == 1:
|
||||||
|
return parse_color(tokens[0])
|
||||||
|
|
||||||
|
|
||||||
|
def parse_color(token):
|
||||||
|
"""Parse single token as a color value.
|
||||||
|
|
||||||
|
:param token:
|
||||||
|
A single :class:`~.token_data.Token` or
|
||||||
|
:class:`~.token_data.ContainerToken`, as found eg. in a
|
||||||
|
property value.
|
||||||
|
:returns:
|
||||||
|
* ``None``, if the token is not a valid CSS 3 color value.
|
||||||
|
(No exception is raised.)
|
||||||
|
* For the *currentColor* keyword: the string ``'currentColor'``
|
||||||
|
* Every other values (including keywords, HSL and HSLA) is converted
|
||||||
|
to RGBA and returned as an :class:`RGBA` object (a 4-tuple with
|
||||||
|
attribute access).
|
||||||
|
The alpha channel is clipped to [0, 1], but R, G, or B can be
|
||||||
|
out of range (eg. ``rgb(-51, 306, 0)`` is represented as
|
||||||
|
``(-.2, 1.2, 0, 1)``.)
|
||||||
|
|
||||||
|
"""
|
||||||
|
if token.type == 'IDENT':
|
||||||
|
return COLOR_KEYWORDS.get(token.value.lower())
|
||||||
|
elif token.type == 'HASH':
|
||||||
|
for multiplier, regexp in HASH_REGEXPS:
|
||||||
|
match = regexp(token.value)
|
||||||
|
if match:
|
||||||
|
r, g, b = [int(group * multiplier, 16) / 255
|
||||||
|
for group in match.groups()]
|
||||||
|
return RGBA(r, g, b, 1.)
|
||||||
|
elif token.type == 'FUNCTION':
|
||||||
|
args = parse_comma_separated(token.content)
|
||||||
|
if args:
|
||||||
|
name = token.function_name.lower()
|
||||||
|
if name == 'rgb':
|
||||||
|
return parse_rgb(args, alpha=1.)
|
||||||
|
elif name == 'rgba':
|
||||||
|
alpha = parse_alpha(args[3:])
|
||||||
|
if alpha is not None:
|
||||||
|
return parse_rgb(args[:3], alpha)
|
||||||
|
elif name == 'hsl':
|
||||||
|
return parse_hsl(args, alpha=1.)
|
||||||
|
elif name == 'hsla':
|
||||||
|
alpha = parse_alpha(args[3:])
|
||||||
|
if alpha is not None:
|
||||||
|
return parse_hsl(args[:3], alpha)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_alpha(args):
|
||||||
|
"""
|
||||||
|
If args is a list of a single INTEGER or NUMBER token,
|
||||||
|
retur its value clipped to the 0..1 range
|
||||||
|
Otherwise, return None.
|
||||||
|
"""
|
||||||
|
if len(args) == 1 and args[0].type in ('NUMBER', 'INTEGER'):
|
||||||
|
return min(1, max(0, args[0].value))
|
||||||
|
|
||||||
|
|
||||||
|
def parse_rgb(args, alpha):
|
||||||
|
"""
|
||||||
|
If args is a list of 3 INTEGER tokens or 3 PERCENTAGE tokens,
|
||||||
|
return RGB values as a tuple of 3 floats in 0..1.
|
||||||
|
Otherwise, return None.
|
||||||
|
"""
|
||||||
|
types = [arg.type for arg in args]
|
||||||
|
if types == ['INTEGER', 'INTEGER', 'INTEGER']:
|
||||||
|
r, g, b = [arg.value / 255 for arg in args[:3]]
|
||||||
|
return RGBA(r, g, b, alpha)
|
||||||
|
elif types == ['PERCENTAGE', 'PERCENTAGE', 'PERCENTAGE']:
|
||||||
|
r, g, b = [arg.value / 100 for arg in args[:3]]
|
||||||
|
return RGBA(r, g, b, alpha)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_hsl(args, alpha):
|
||||||
|
"""
|
||||||
|
If args is a list of 1 INTEGER token and 2 PERCENTAGE tokens,
|
||||||
|
return RGB values as a tuple of 3 floats in 0..1.
|
||||||
|
Otherwise, return None.
|
||||||
|
"""
|
||||||
|
types = [arg.type for arg in args]
|
||||||
|
if types == ['INTEGER', 'PERCENTAGE', 'PERCENTAGE']:
|
||||||
|
hsl = [arg.value for arg in args[:3]]
|
||||||
|
r, g, b = hsl_to_rgb(*hsl)
|
||||||
|
return RGBA(r, g, b, alpha)
|
||||||
|
|
||||||
|
|
||||||
|
def hsl_to_rgb(hue, saturation, lightness):
|
||||||
|
"""
|
||||||
|
:param hue: degrees
|
||||||
|
:param saturation: percentage
|
||||||
|
:param lightness: percentage
|
||||||
|
:returns: (r, g, b) as floats in the 0..1 range
|
||||||
|
"""
|
||||||
|
hue = (hue / 360) % 1
|
||||||
|
saturation = min(1, max(0, saturation / 100))
|
||||||
|
lightness = min(1, max(0, lightness / 100))
|
||||||
|
|
||||||
|
# Translated from ABC: http://www.w3.org/TR/css3-color/#hsl-color
|
||||||
|
def hue_to_rgb(m1, m2, h):
|
||||||
|
if h < 0:
|
||||||
|
h += 1
|
||||||
|
if h > 1:
|
||||||
|
h -= 1
|
||||||
|
if h * 6 < 1:
|
||||||
|
return m1 + (m2 - m1) * h * 6
|
||||||
|
if h * 2 < 1:
|
||||||
|
return m2
|
||||||
|
if h * 3 < 2:
|
||||||
|
return m1 + (m2 - m1) * (2 / 3 - h) * 6
|
||||||
|
return m1
|
||||||
|
|
||||||
|
if lightness <= 0.5:
|
||||||
|
m2 = lightness * (saturation + 1)
|
||||||
|
else:
|
||||||
|
m2 = lightness + saturation - lightness * saturation
|
||||||
|
m1 = lightness * 2 - m2
|
||||||
|
return (
|
||||||
|
hue_to_rgb(m1, m2, hue + 1 / 3),
|
||||||
|
hue_to_rgb(m1, m2, hue),
|
||||||
|
hue_to_rgb(m1, m2, hue - 1 / 3),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_comma_separated(tokens):
|
||||||
|
"""Parse a list of tokens (typically the content of a function token)
|
||||||
|
as arguments made of a single token each, separated by mandatory commas,
|
||||||
|
with optional white space around each argument.
|
||||||
|
|
||||||
|
return the argument list without commas or white space;
|
||||||
|
or None if the function token content do not match the description above.
|
||||||
|
|
||||||
|
"""
|
||||||
|
tokens = [token for token in tokens if token.type != 'S']
|
||||||
|
if not tokens:
|
||||||
|
return []
|
||||||
|
if len(tokens) % 2 == 1 and all(
|
||||||
|
token.type == 'DELIM' and token.value == ','
|
||||||
|
for token in tokens[1::2]):
|
||||||
|
return tokens[::2]
|
||||||
|
|
||||||
|
|
||||||
|
HASH_REGEXPS = (
|
||||||
|
(2, re.compile('^#([\da-f])([\da-f])([\da-f])$', re.I).match),
|
||||||
|
(1, re.compile('^#([\da-f]{2})([\da-f]{2})([\da-f]{2})$', re.I).match),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# (r, g, b) in 0..255
|
||||||
|
BASIC_COLOR_KEYWORDS = [
|
||||||
|
('black', (0, 0, 0)),
|
||||||
|
('silver', (192, 192, 192)),
|
||||||
|
('gray', (128, 128, 128)),
|
||||||
|
('white', (255, 255, 255)),
|
||||||
|
('maroon', (128, 0, 0)),
|
||||||
|
('red', (255, 0, 0)),
|
||||||
|
('purple', (128, 0, 128)),
|
||||||
|
('fuchsia', (255, 0, 255)),
|
||||||
|
('green', (0, 128, 0)),
|
||||||
|
('lime', (0, 255, 0)),
|
||||||
|
('olive', (128, 128, 0)),
|
||||||
|
('yellow', (255, 255, 0)),
|
||||||
|
('navy', (0, 0, 128)),
|
||||||
|
('blue', (0, 0, 255)),
|
||||||
|
('teal', (0, 128, 128)),
|
||||||
|
('aqua', (0, 255, 255)),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# (r, g, b) in 0..255
|
||||||
|
EXTENDED_COLOR_KEYWORDS = [
|
||||||
|
('aliceblue', (240, 248, 255)),
|
||||||
|
('antiquewhite', (250, 235, 215)),
|
||||||
|
('aqua', (0, 255, 255)),
|
||||||
|
('aquamarine', (127, 255, 212)),
|
||||||
|
('azure', (240, 255, 255)),
|
||||||
|
('beige', (245, 245, 220)),
|
||||||
|
('bisque', (255, 228, 196)),
|
||||||
|
('black', (0, 0, 0)),
|
||||||
|
('blanchedalmond', (255, 235, 205)),
|
||||||
|
('blue', (0, 0, 255)),
|
||||||
|
('blueviolet', (138, 43, 226)),
|
||||||
|
('brown', (165, 42, 42)),
|
||||||
|
('burlywood', (222, 184, 135)),
|
||||||
|
('cadetblue', (95, 158, 160)),
|
||||||
|
('chartreuse', (127, 255, 0)),
|
||||||
|
('chocolate', (210, 105, 30)),
|
||||||
|
('coral', (255, 127, 80)),
|
||||||
|
('cornflowerblue', (100, 149, 237)),
|
||||||
|
('cornsilk', (255, 248, 220)),
|
||||||
|
('crimson', (220, 20, 60)),
|
||||||
|
('cyan', (0, 255, 255)),
|
||||||
|
('darkblue', (0, 0, 139)),
|
||||||
|
('darkcyan', (0, 139, 139)),
|
||||||
|
('darkgoldenrod', (184, 134, 11)),
|
||||||
|
('darkgray', (169, 169, 169)),
|
||||||
|
('darkgreen', (0, 100, 0)),
|
||||||
|
('darkgrey', (169, 169, 169)),
|
||||||
|
('darkkhaki', (189, 183, 107)),
|
||||||
|
('darkmagenta', (139, 0, 139)),
|
||||||
|
('darkolivegreen', (85, 107, 47)),
|
||||||
|
('darkorange', (255, 140, 0)),
|
||||||
|
('darkorchid', (153, 50, 204)),
|
||||||
|
('darkred', (139, 0, 0)),
|
||||||
|
('darksalmon', (233, 150, 122)),
|
||||||
|
('darkseagreen', (143, 188, 143)),
|
||||||
|
('darkslateblue', (72, 61, 139)),
|
||||||
|
('darkslategray', (47, 79, 79)),
|
||||||
|
('darkslategrey', (47, 79, 79)),
|
||||||
|
('darkturquoise', (0, 206, 209)),
|
||||||
|
('darkviolet', (148, 0, 211)),
|
||||||
|
('deeppink', (255, 20, 147)),
|
||||||
|
('deepskyblue', (0, 191, 255)),
|
||||||
|
('dimgray', (105, 105, 105)),
|
||||||
|
('dimgrey', (105, 105, 105)),
|
||||||
|
('dodgerblue', (30, 144, 255)),
|
||||||
|
('firebrick', (178, 34, 34)),
|
||||||
|
('floralwhite', (255, 250, 240)),
|
||||||
|
('forestgreen', (34, 139, 34)),
|
||||||
|
('fuchsia', (255, 0, 255)),
|
||||||
|
('gainsboro', (220, 220, 220)),
|
||||||
|
('ghostwhite', (248, 248, 255)),
|
||||||
|
('gold', (255, 215, 0)),
|
||||||
|
('goldenrod', (218, 165, 32)),
|
||||||
|
('gray', (128, 128, 128)),
|
||||||
|
('green', (0, 128, 0)),
|
||||||
|
('greenyellow', (173, 255, 47)),
|
||||||
|
('grey', (128, 128, 128)),
|
||||||
|
('honeydew', (240, 255, 240)),
|
||||||
|
('hotpink', (255, 105, 180)),
|
||||||
|
('indianred', (205, 92, 92)),
|
||||||
|
('indigo', (75, 0, 130)),
|
||||||
|
('ivory', (255, 255, 240)),
|
||||||
|
('khaki', (240, 230, 140)),
|
||||||
|
('lavender', (230, 230, 250)),
|
||||||
|
('lavenderblush', (255, 240, 245)),
|
||||||
|
('lawngreen', (124, 252, 0)),
|
||||||
|
('lemonchiffon', (255, 250, 205)),
|
||||||
|
('lightblue', (173, 216, 230)),
|
||||||
|
('lightcoral', (240, 128, 128)),
|
||||||
|
('lightcyan', (224, 255, 255)),
|
||||||
|
('lightgoldenrodyellow', (250, 250, 210)),
|
||||||
|
('lightgray', (211, 211, 211)),
|
||||||
|
('lightgreen', (144, 238, 144)),
|
||||||
|
('lightgrey', (211, 211, 211)),
|
||||||
|
('lightpink', (255, 182, 193)),
|
||||||
|
('lightsalmon', (255, 160, 122)),
|
||||||
|
('lightseagreen', (32, 178, 170)),
|
||||||
|
('lightskyblue', (135, 206, 250)),
|
||||||
|
('lightslategray', (119, 136, 153)),
|
||||||
|
('lightslategrey', (119, 136, 153)),
|
||||||
|
('lightsteelblue', (176, 196, 222)),
|
||||||
|
('lightyellow', (255, 255, 224)),
|
||||||
|
('lime', (0, 255, 0)),
|
||||||
|
('limegreen', (50, 205, 50)),
|
||||||
|
('linen', (250, 240, 230)),
|
||||||
|
('magenta', (255, 0, 255)),
|
||||||
|
('maroon', (128, 0, 0)),
|
||||||
|
('mediumaquamarine', (102, 205, 170)),
|
||||||
|
('mediumblue', (0, 0, 205)),
|
||||||
|
('mediumorchid', (186, 85, 211)),
|
||||||
|
('mediumpurple', (147, 112, 219)),
|
||||||
|
('mediumseagreen', (60, 179, 113)),
|
||||||
|
('mediumslateblue', (123, 104, 238)),
|
||||||
|
('mediumspringgreen', (0, 250, 154)),
|
||||||
|
('mediumturquoise', (72, 209, 204)),
|
||||||
|
('mediumvioletred', (199, 21, 133)),
|
||||||
|
('midnightblue', (25, 25, 112)),
|
||||||
|
('mintcream', (245, 255, 250)),
|
||||||
|
('mistyrose', (255, 228, 225)),
|
||||||
|
('moccasin', (255, 228, 181)),
|
||||||
|
('navajowhite', (255, 222, 173)),
|
||||||
|
('navy', (0, 0, 128)),
|
||||||
|
('oldlace', (253, 245, 230)),
|
||||||
|
('olive', (128, 128, 0)),
|
||||||
|
('olivedrab', (107, 142, 35)),
|
||||||
|
('orange', (255, 165, 0)),
|
||||||
|
('orangered', (255, 69, 0)),
|
||||||
|
('orchid', (218, 112, 214)),
|
||||||
|
('palegoldenrod', (238, 232, 170)),
|
||||||
|
('palegreen', (152, 251, 152)),
|
||||||
|
('paleturquoise', (175, 238, 238)),
|
||||||
|
('palevioletred', (219, 112, 147)),
|
||||||
|
('papayawhip', (255, 239, 213)),
|
||||||
|
('peachpuff', (255, 218, 185)),
|
||||||
|
('peru', (205, 133, 63)),
|
||||||
|
('pink', (255, 192, 203)),
|
||||||
|
('plum', (221, 160, 221)),
|
||||||
|
('powderblue', (176, 224, 230)),
|
||||||
|
('purple', (128, 0, 128)),
|
||||||
|
('red', (255, 0, 0)),
|
||||||
|
('rosybrown', (188, 143, 143)),
|
||||||
|
('royalblue', (65, 105, 225)),
|
||||||
|
('saddlebrown', (139, 69, 19)),
|
||||||
|
('salmon', (250, 128, 114)),
|
||||||
|
('sandybrown', (244, 164, 96)),
|
||||||
|
('seagreen', (46, 139, 87)),
|
||||||
|
('seashell', (255, 245, 238)),
|
||||||
|
('sienna', (160, 82, 45)),
|
||||||
|
('silver', (192, 192, 192)),
|
||||||
|
('skyblue', (135, 206, 235)),
|
||||||
|
('slateblue', (106, 90, 205)),
|
||||||
|
('slategray', (112, 128, 144)),
|
||||||
|
('slategrey', (112, 128, 144)),
|
||||||
|
('snow', (255, 250, 250)),
|
||||||
|
('springgreen', (0, 255, 127)),
|
||||||
|
('steelblue', (70, 130, 180)),
|
||||||
|
('tan', (210, 180, 140)),
|
||||||
|
('teal', (0, 128, 128)),
|
||||||
|
('thistle', (216, 191, 216)),
|
||||||
|
('tomato', (255, 99, 71)),
|
||||||
|
('turquoise', (64, 224, 208)),
|
||||||
|
('violet', (238, 130, 238)),
|
||||||
|
('wheat', (245, 222, 179)),
|
||||||
|
('white', (255, 255, 255)),
|
||||||
|
('whitesmoke', (245, 245, 245)),
|
||||||
|
('yellow', (255, 255, 0)),
|
||||||
|
('yellowgreen', (154, 205, 50)),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# (r, g, b, a) in 0..1 or a string marker
|
||||||
|
SPECIAL_COLOR_KEYWORDS = {
|
||||||
|
'currentcolor': 'currentColor',
|
||||||
|
'transparent': RGBA(0., 0., 0., 0.),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# RGBA namedtuples of (r, g, b, a) in 0..1 or a string marker
|
||||||
|
COLOR_KEYWORDS = SPECIAL_COLOR_KEYWORDS.copy()
|
||||||
|
COLOR_KEYWORDS.update(
|
||||||
|
# 255 maps to 1, 0 to 0, the rest is linear.
|
||||||
|
(keyword, RGBA(r / 255., g / 255., b / 255., 1.))
|
||||||
|
for keyword, (r, g, b) in itertools.chain(
|
||||||
|
BASIC_COLOR_KEYWORDS, EXTENDED_COLOR_KEYWORDS))
|
|
@ -0,0 +1,823 @@
|
||||||
|
# coding: utf-8
|
||||||
|
"""
|
||||||
|
tinycss.css21
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Parser for CSS 2.1
|
||||||
|
http://www.w3.org/TR/CSS21/syndata.html
|
||||||
|
|
||||||
|
:copyright: (c) 2012 by Simon Sapin.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from itertools import chain, islice
|
||||||
|
|
||||||
|
from .decoding import decode
|
||||||
|
from .parsing import (
|
||||||
|
ParseError, remove_whitespace, split_on_comma, strip_whitespace,
|
||||||
|
validate_any, validate_value)
|
||||||
|
from .token_data import TokenList
|
||||||
|
from .tokenizer import tokenize_grouped
|
||||||
|
|
||||||
|
|
||||||
|
# stylesheet : [ CDO | CDC | S | statement ]*;
|
||||||
|
# statement : ruleset | at-rule;
|
||||||
|
# at-rule : ATKEYWORD S* any* [ block | ';' S* ];
|
||||||
|
# block : '{' S* [ any | block | ATKEYWORD S* | ';' S* ]* '}' S*;
|
||||||
|
# ruleset : selector? '{' S* declaration? [ ';' S* declaration? ]* '}' S*;
|
||||||
|
# selector : any+;
|
||||||
|
# declaration : property S* ':' S* value;
|
||||||
|
# property : IDENT;
|
||||||
|
# value : [ any | block | ATKEYWORD S* ]+;
|
||||||
|
# any : [ IDENT | NUMBER | PERCENTAGE | DIMENSION | STRING
|
||||||
|
# | DELIM | URI | HASH | UNICODE-RANGE | INCLUDES
|
||||||
|
# | DASHMATCH | ':' | FUNCTION S* [any|unused]* ')'
|
||||||
|
# | '(' S* [any|unused]* ')' | '[' S* [any|unused]* ']'
|
||||||
|
# ] S*;
|
||||||
|
# unused : block | ATKEYWORD S* | ';' S* | CDO S* | CDC S*;
|
||||||
|
|
||||||
|
|
||||||
|
class Stylesheet(object):
|
||||||
|
"""
|
||||||
|
A parsed CSS stylesheet.
|
||||||
|
|
||||||
|
.. attribute:: rules
|
||||||
|
|
||||||
|
A mixed list, in source order, of :class:`RuleSet` and various
|
||||||
|
at-rules such as :class:`ImportRule`, :class:`MediaRule`
|
||||||
|
and :class:`PageRule`.
|
||||||
|
Use their :obj:`at_keyword` attribute to distinguish them.
|
||||||
|
|
||||||
|
.. attribute:: errors
|
||||||
|
|
||||||
|
A list of :class:`~.parsing.ParseError`. Invalid rules and declarations
|
||||||
|
are ignored, with the details logged in this list.
|
||||||
|
|
||||||
|
.. attribute:: encoding
|
||||||
|
|
||||||
|
The character encoding that was used to decode the stylesheet
|
||||||
|
from bytes, or ``None`` for Unicode stylesheets.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, rules, errors, encoding):
|
||||||
|
self.rules = rules
|
||||||
|
self.errors = errors
|
||||||
|
self.encoding = encoding
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<{0.__class__.__name__} {1} rules {2} errors>'.format(
|
||||||
|
self, len(self.rules), len(self.errors))
|
||||||
|
|
||||||
|
|
||||||
|
class AtRule(object):
|
||||||
|
"""
|
||||||
|
An unparsed at-rule.
|
||||||
|
|
||||||
|
.. attribute:: at_keyword
|
||||||
|
|
||||||
|
The normalized (lower-case) at-keyword as a string. Eg: ``'@page'``
|
||||||
|
|
||||||
|
.. attribute:: head
|
||||||
|
|
||||||
|
The part of the at-rule between the at-keyword and the ``{``
|
||||||
|
marking the body, or the ``;`` marking the end of an at-rule without
|
||||||
|
a body. A :class:`~.token_data.TokenList`.
|
||||||
|
|
||||||
|
.. attribute:: body
|
||||||
|
|
||||||
|
The content of the body between ``{`` and ``}`` as a
|
||||||
|
:class:`~.token_data.TokenList`, or ``None`` if there is no body
|
||||||
|
(ie. if the rule ends with ``;``).
|
||||||
|
|
||||||
|
The head was validated against the core grammar but **not** the body,
|
||||||
|
as the body might contain declarations. In case of an error in a
|
||||||
|
declaration, parsing should continue from the next declaration.
|
||||||
|
The whole rule should not be ignored as it would be for an error
|
||||||
|
in the head.
|
||||||
|
|
||||||
|
These at-rules are expected to be parsed further before reaching
|
||||||
|
the user API.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, at_keyword, head, body, line, column):
|
||||||
|
self.at_keyword = at_keyword
|
||||||
|
self.head = TokenList(head)
|
||||||
|
self.body = TokenList(body) if body is not None else body
|
||||||
|
self.line = line
|
||||||
|
self.column = column
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return ('<{0.__class__.__name__} {0.line}:{0.column} {0.at_keyword}>'
|
||||||
|
.format(self))
|
||||||
|
|
||||||
|
|
||||||
|
class RuleSet(object):
|
||||||
|
"""A ruleset.
|
||||||
|
|
||||||
|
.. attribute:: at_keyword
|
||||||
|
|
||||||
|
Always ``None``. Helps to tell rulesets apart from at-rules.
|
||||||
|
|
||||||
|
.. attribute:: selector
|
||||||
|
|
||||||
|
The selector as a :class:`~.token_data.TokenList`.
|
||||||
|
In CSS 3, this is actually called a selector group.
|
||||||
|
|
||||||
|
``rule.selector.as_css()`` gives the selector as a string.
|
||||||
|
This string can be used with *cssselect*, see :ref:`selectors3`.
|
||||||
|
|
||||||
|
.. attribute:: declarations
|
||||||
|
|
||||||
|
The list of :class:`Declaration`, in source order.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
at_keyword = None
|
||||||
|
|
||||||
|
def __init__(self, selector, declarations, line, column):
|
||||||
|
self.selector = TokenList(selector)
|
||||||
|
self.declarations = declarations
|
||||||
|
self.line = line
|
||||||
|
self.column = column
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return ('<{0.__class__.__name__} at {0.line}:{0.column} {1}>'
|
||||||
|
.format(self, self.selector.as_css()))
|
||||||
|
|
||||||
|
|
||||||
|
class Declaration(object):
|
||||||
|
"""A property declaration.
|
||||||
|
|
||||||
|
.. attribute:: name
|
||||||
|
|
||||||
|
The property name as a normalized (lower-case) string.
|
||||||
|
|
||||||
|
.. attribute:: value
|
||||||
|
|
||||||
|
The property value as a :class:`~.token_data.TokenList`.
|
||||||
|
|
||||||
|
The value is not parsed. UAs using tinycss may only support
|
||||||
|
some properties or some values and tinycss does not know which.
|
||||||
|
They need to parse values themselves and ignore declarations with
|
||||||
|
unknown or unsupported properties or values, and fall back
|
||||||
|
on any previous declaration.
|
||||||
|
|
||||||
|
:mod:`tinycss.color3` parses color values, but other values
|
||||||
|
will need specific parsing/validation code.
|
||||||
|
|
||||||
|
.. attribute:: priority
|
||||||
|
|
||||||
|
Either the string ``'important'`` or ``None``.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, name, value, priority, line, column):
|
||||||
|
self.name = name
|
||||||
|
self.value = TokenList(value)
|
||||||
|
self.priority = priority
|
||||||
|
self.line = line
|
||||||
|
self.column = column
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
priority = ' !' + self.priority if self.priority else ''
|
||||||
|
return ('<{0.__class__.__name__} {0.line}:{0.column}'
|
||||||
|
' {0.name}: {1}{2}>'.format(
|
||||||
|
self, self.value.as_css(), priority))
|
||||||
|
|
||||||
|
|
||||||
|
class PageRule(object):
|
||||||
|
"""A parsed CSS 2.1 @page rule.
|
||||||
|
|
||||||
|
.. attribute:: at_keyword
|
||||||
|
|
||||||
|
Always ``'@page'``
|
||||||
|
|
||||||
|
.. attribute:: selector
|
||||||
|
|
||||||
|
The page selector.
|
||||||
|
In CSS 2.1 this is either ``None`` (no selector), or the string
|
||||||
|
``'first'``, ``'left'`` or ``'right'`` for the pseudo class
|
||||||
|
of the same name.
|
||||||
|
|
||||||
|
.. attribute:: specificity
|
||||||
|
|
||||||
|
Specificity of the page selector. This is a tuple of four integers,
|
||||||
|
but these tuples are mostly meant to be compared to each other.
|
||||||
|
|
||||||
|
.. attribute:: declarations
|
||||||
|
|
||||||
|
A list of :class:`Declaration`, in source order.
|
||||||
|
|
||||||
|
.. attribute:: at_rules
|
||||||
|
|
||||||
|
The list of parsed at-rules inside the @page block, in source order.
|
||||||
|
Always empty for CSS 2.1.
|
||||||
|
|
||||||
|
"""
|
||||||
|
at_keyword = '@page'
|
||||||
|
|
||||||
|
def __init__(self, selector, specificity, declarations, at_rules,
|
||||||
|
line, column):
|
||||||
|
self.selector = selector
|
||||||
|
self.specificity = specificity
|
||||||
|
self.declarations = declarations
|
||||||
|
self.at_rules = at_rules
|
||||||
|
self.line = line
|
||||||
|
self.column = column
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return ('<{0.__class__.__name__} {0.line}:{0.column}'
|
||||||
|
' {0.selector}>'.format(self))
|
||||||
|
|
||||||
|
|
||||||
|
class MediaRule(object):
|
||||||
|
"""A parsed @media rule.
|
||||||
|
|
||||||
|
.. attribute:: at_keyword
|
||||||
|
|
||||||
|
Always ``'@media'``
|
||||||
|
|
||||||
|
.. attribute:: media
|
||||||
|
|
||||||
|
For CSS 2.1 without media queries: the media types
|
||||||
|
as a list of strings.
|
||||||
|
|
||||||
|
.. attribute:: rules
|
||||||
|
|
||||||
|
The list :class:`RuleSet` and various at-rules inside the @media
|
||||||
|
block, in source order.
|
||||||
|
|
||||||
|
"""
|
||||||
|
at_keyword = '@media'
|
||||||
|
|
||||||
|
def __init__(self, media, rules, line, column):
|
||||||
|
self.media = media
|
||||||
|
self.rules = rules
|
||||||
|
self.line = line
|
||||||
|
self.column = column
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return ('<{0.__class__.__name__} {0.line}:{0.column}'
|
||||||
|
' {0.media}>'.format(self))
|
||||||
|
|
||||||
|
|
||||||
|
class ImportRule(object):
|
||||||
|
"""A parsed @import rule.
|
||||||
|
|
||||||
|
.. attribute:: at_keyword
|
||||||
|
|
||||||
|
Always ``'@import'``
|
||||||
|
|
||||||
|
.. attribute:: uri
|
||||||
|
|
||||||
|
The URI to be imported, as read from the stylesheet.
|
||||||
|
(URIs are not made absolute.)
|
||||||
|
|
||||||
|
.. attribute:: media
|
||||||
|
|
||||||
|
For CSS 2.1 without media queries: the media types
|
||||||
|
as a list of strings.
|
||||||
|
This attribute is explicitly ``['all']`` if the media was omitted
|
||||||
|
in the source.
|
||||||
|
|
||||||
|
"""
|
||||||
|
at_keyword = '@import'
|
||||||
|
|
||||||
|
def __init__(self, uri, media, line, column):
|
||||||
|
self.uri = uri
|
||||||
|
self.media = media
|
||||||
|
self.line = line
|
||||||
|
self.column = column
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return ('<{0.__class__.__name__} {0.line}:{0.column}'
|
||||||
|
' {0.uri}>'.format(self))
|
||||||
|
|
||||||
|
|
||||||
|
def _remove_at_charset(tokens):
|
||||||
|
"""Remove any valid @charset at the beggining of a token stream.
|
||||||
|
|
||||||
|
:param tokens:
|
||||||
|
An iterable of tokens
|
||||||
|
:returns:
|
||||||
|
A possibly truncated iterable of tokens
|
||||||
|
|
||||||
|
"""
|
||||||
|
tokens = iter(tokens)
|
||||||
|
header = list(islice(tokens, 4))
|
||||||
|
if [t.type for t in header] == ['ATKEYWORD', 'S', 'STRING', ';']:
|
||||||
|
atkw, space, string, semicolon = header
|
||||||
|
if ((atkw.value, space.value) == ('@charset', ' ') and
|
||||||
|
string.as_css()[0] == '"'):
|
||||||
|
# Found a valid @charset rule, only keep what’s after it.
|
||||||
|
return tokens
|
||||||
|
return chain(header, tokens)
|
||||||
|
|
||||||
|
|
||||||
|
class CSS21Parser(object):
|
||||||
|
"""Parser for CSS 2.1
|
||||||
|
|
||||||
|
This parser supports the core CSS syntax as well as @import, @media,
|
||||||
|
@page and !important.
|
||||||
|
|
||||||
|
Note that property values are still not parsed, as UAs using this
|
||||||
|
parser may only support some properties or some values.
|
||||||
|
|
||||||
|
Currently the parser holds no state. It being a class only allows
|
||||||
|
subclassing and overriding its methods.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# User API:
|
||||||
|
|
||||||
|
def parse_stylesheet_file(self, css_file, protocol_encoding=None,
|
||||||
|
linking_encoding=None, document_encoding=None):
|
||||||
|
"""Parse a stylesheet from a file or filename.
|
||||||
|
|
||||||
|
Character encoding-related parameters and behavior are the same
|
||||||
|
as in :meth:`parse_stylesheet_bytes`.
|
||||||
|
|
||||||
|
:param css_file:
|
||||||
|
Either a file (any object with a :meth:`~file.read` method)
|
||||||
|
or a filename.
|
||||||
|
:return:
|
||||||
|
A :class:`Stylesheet`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if hasattr(css_file, 'read'):
|
||||||
|
css_bytes = css_file.read()
|
||||||
|
else:
|
||||||
|
with open(css_file, 'rb') as fd:
|
||||||
|
css_bytes = fd.read()
|
||||||
|
return self.parse_stylesheet_bytes(css_bytes, protocol_encoding,
|
||||||
|
linking_encoding, document_encoding)
|
||||||
|
|
||||||
|
def parse_stylesheet_bytes(self, css_bytes, protocol_encoding=None,
|
||||||
|
linking_encoding=None, document_encoding=None):
|
||||||
|
"""Parse a stylesheet from a byte string.
|
||||||
|
|
||||||
|
The character encoding is determined from the passed metadata and the
|
||||||
|
``@charset`` rule in the stylesheet (if any).
|
||||||
|
If no encoding information is available or decoding fails,
|
||||||
|
decoding defaults to UTF-8 and then fall back on ISO-8859-1.
|
||||||
|
|
||||||
|
:param css_bytes:
|
||||||
|
A CSS stylesheet as a byte string.
|
||||||
|
:param protocol_encoding:
|
||||||
|
The "charset" parameter of a "Content-Type" HTTP header (if any),
|
||||||
|
or similar metadata for other protocols.
|
||||||
|
:param linking_encoding:
|
||||||
|
``<link charset="">`` or other metadata from the linking mechanism
|
||||||
|
(if any)
|
||||||
|
:param document_encoding:
|
||||||
|
Encoding of the referring style sheet or document (if any)
|
||||||
|
:return:
|
||||||
|
A :class:`Stylesheet`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
css_unicode, encoding = decode(css_bytes, protocol_encoding,
|
||||||
|
linking_encoding, document_encoding)
|
||||||
|
return self.parse_stylesheet(css_unicode, encoding=encoding)
|
||||||
|
|
||||||
|
def parse_stylesheet(self, css_unicode, encoding=None):
|
||||||
|
"""Parse a stylesheet from an Unicode string.
|
||||||
|
|
||||||
|
:param css_unicode:
|
||||||
|
A CSS stylesheet as an unicode string.
|
||||||
|
:param encoding:
|
||||||
|
The character encoding used to decode the stylesheet from bytes,
|
||||||
|
if any.
|
||||||
|
:return:
|
||||||
|
A :class:`Stylesheet`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
tokens = tokenize_grouped(css_unicode)
|
||||||
|
if encoding:
|
||||||
|
tokens = _remove_at_charset(tokens)
|
||||||
|
rules, errors = self.parse_rules(tokens, context='stylesheet')
|
||||||
|
return Stylesheet(rules, errors, encoding)
|
||||||
|
|
||||||
|
def parse_style_attr(self, css_source):
|
||||||
|
"""Parse a "style" attribute (eg. of an HTML element).
|
||||||
|
|
||||||
|
This method only accepts Unicode as the source (HTML) document
|
||||||
|
is supposed to handle the character encoding.
|
||||||
|
|
||||||
|
:param css_source:
|
||||||
|
The attribute value, as an unicode string.
|
||||||
|
:return:
|
||||||
|
A tuple of the list of valid :class:`Declaration` and
|
||||||
|
a list of :class:`~.parsing.ParseError`.
|
||||||
|
"""
|
||||||
|
return self.parse_declaration_list(tokenize_grouped(css_source))
|
||||||
|
|
||||||
|
# API for subclasses:
|
||||||
|
|
||||||
|
def parse_rules(self, tokens, context):
|
||||||
|
"""Parse a sequence of rules (rulesets and at-rules).
|
||||||
|
|
||||||
|
:param tokens:
|
||||||
|
An iterable of tokens.
|
||||||
|
:param context:
|
||||||
|
Either ``'stylesheet'`` or an at-keyword such as ``'@media'``.
|
||||||
|
(Most at-rules are only allowed in some contexts.)
|
||||||
|
:return:
|
||||||
|
A tuple of a list of parsed rules and a list of
|
||||||
|
:class:`~.parsing.ParseError`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
rules = []
|
||||||
|
errors = []
|
||||||
|
tokens = iter(tokens)
|
||||||
|
for token in tokens:
|
||||||
|
if token.type not in ('S', 'CDO', 'CDC'):
|
||||||
|
try:
|
||||||
|
if token.type == 'ATKEYWORD':
|
||||||
|
rule = self.read_at_rule(token, tokens)
|
||||||
|
result = self.parse_at_rule(
|
||||||
|
rule, rules, errors, context)
|
||||||
|
rules.append(result)
|
||||||
|
else:
|
||||||
|
rule, rule_errors = self.parse_ruleset(token, tokens)
|
||||||
|
rules.append(rule)
|
||||||
|
errors.extend(rule_errors)
|
||||||
|
except ParseError as exc:
|
||||||
|
errors.append(exc)
|
||||||
|
# Skip the entire rule
|
||||||
|
return rules, errors
|
||||||
|
|
||||||
|
def read_at_rule(self, at_keyword_token, tokens):
|
||||||
|
"""Read an at-rule from a token stream.
|
||||||
|
|
||||||
|
:param at_keyword_token:
|
||||||
|
The ATKEYWORD token that starts this at-rule
|
||||||
|
You may have read it already to distinguish the rule
|
||||||
|
from a ruleset.
|
||||||
|
:param tokens:
|
||||||
|
An iterator of subsequent tokens. Will be consumed just enough
|
||||||
|
for one at-rule.
|
||||||
|
:return:
|
||||||
|
An unparsed :class:`AtRule`.
|
||||||
|
:raises:
|
||||||
|
:class:`~.parsing.ParseError` if the head is invalid for the core
|
||||||
|
grammar. The body is **not** validated. See :class:`AtRule`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# CSS syntax is case-insensitive
|
||||||
|
at_keyword = at_keyword_token.value.lower()
|
||||||
|
head = []
|
||||||
|
# For the ParseError in case `tokens` is empty:
|
||||||
|
token = at_keyword_token
|
||||||
|
for token in tokens:
|
||||||
|
if token.type in '{;':
|
||||||
|
break
|
||||||
|
# Ignore white space just after the at-keyword.
|
||||||
|
else:
|
||||||
|
head.append(token)
|
||||||
|
# On unexpected end of stylesheet, pretend that a ';' was there
|
||||||
|
head = strip_whitespace(head)
|
||||||
|
for head_token in head:
|
||||||
|
validate_any(head_token, 'at-rule head')
|
||||||
|
body = token.content if token.type == '{' else None
|
||||||
|
return AtRule(at_keyword, head, body,
|
||||||
|
at_keyword_token.line, at_keyword_token.column)
|
||||||
|
|
||||||
|
def parse_at_rule(self, rule, previous_rules, errors, context):
|
||||||
|
"""Parse an at-rule.
|
||||||
|
|
||||||
|
Subclasses that override this method must use ``super()`` and
|
||||||
|
pass its return value for at-rules they do not know.
|
||||||
|
|
||||||
|
In CSS 2.1, this method handles @charset, @import, @media and @page
|
||||||
|
rules.
|
||||||
|
|
||||||
|
:param rule:
|
||||||
|
An unparsed :class:`AtRule`.
|
||||||
|
:param previous_rules:
|
||||||
|
The list of at-rules and rulesets that have been parsed so far
|
||||||
|
in this context. This list can be used to decide if the current
|
||||||
|
rule is valid. (For example, @import rules are only allowed
|
||||||
|
before anything but a @charset rule.)
|
||||||
|
:param context:
|
||||||
|
Either ``'stylesheet'`` or an at-keyword such as ``'@media'``.
|
||||||
|
(Most at-rules are only allowed in some contexts.)
|
||||||
|
:raises:
|
||||||
|
:class:`~.parsing.ParseError` if the rule is invalid.
|
||||||
|
:return:
|
||||||
|
A parsed at-rule
|
||||||
|
|
||||||
|
"""
|
||||||
|
if rule.at_keyword == '@page':
|
||||||
|
if context != 'stylesheet':
|
||||||
|
raise ParseError(rule, '@page rule not allowed in ' + context)
|
||||||
|
selector, specificity = self.parse_page_selector(rule.head)
|
||||||
|
if rule.body is None:
|
||||||
|
raise ParseError(
|
||||||
|
rule, 'invalid {0} rule: missing block'.format(
|
||||||
|
rule.at_keyword))
|
||||||
|
declarations, at_rules, rule_errors = \
|
||||||
|
self.parse_declarations_and_at_rules(rule.body, '@page')
|
||||||
|
errors.extend(rule_errors)
|
||||||
|
return PageRule(selector, specificity, declarations, at_rules,
|
||||||
|
rule.line, rule.column)
|
||||||
|
|
||||||
|
elif rule.at_keyword == '@media':
|
||||||
|
if context != 'stylesheet':
|
||||||
|
raise ParseError(rule, '@media rule not allowed in ' + context)
|
||||||
|
if not rule.head:
|
||||||
|
raise ParseError(rule, 'expected media types for @media')
|
||||||
|
media = self.parse_media(rule.head)
|
||||||
|
if rule.body is None:
|
||||||
|
raise ParseError(
|
||||||
|
rule, 'invalid {0} rule: missing block'.format(
|
||||||
|
rule.at_keyword))
|
||||||
|
rules, rule_errors = self.parse_rules(rule.body, '@media')
|
||||||
|
errors.extend(rule_errors)
|
||||||
|
return MediaRule(media, rules, rule.line, rule.column)
|
||||||
|
|
||||||
|
elif rule.at_keyword == '@import':
|
||||||
|
if context != 'stylesheet':
|
||||||
|
raise ParseError(
|
||||||
|
rule, '@import rule not allowed in ' + context)
|
||||||
|
for previous_rule in previous_rules:
|
||||||
|
if previous_rule.at_keyword not in ('@charset', '@import'):
|
||||||
|
if previous_rule.at_keyword:
|
||||||
|
type_ = 'an {0} rule'.format(previous_rule.at_keyword)
|
||||||
|
else:
|
||||||
|
type_ = 'a ruleset'
|
||||||
|
raise ParseError(
|
||||||
|
previous_rule,
|
||||||
|
'@import rule not allowed after ' + type_)
|
||||||
|
head = rule.head
|
||||||
|
if not head:
|
||||||
|
raise ParseError(
|
||||||
|
rule, 'expected URI or STRING for @import rule')
|
||||||
|
if head[0].type not in ('URI', 'STRING'):
|
||||||
|
raise ParseError(
|
||||||
|
rule, 'expected URI or STRING for @import rule, got ' +
|
||||||
|
head[0].type)
|
||||||
|
uri = head[0].value
|
||||||
|
media = self.parse_media(strip_whitespace(head[1:]))
|
||||||
|
if rule.body is not None:
|
||||||
|
# The position of the ';' token would be best, but we don’t
|
||||||
|
# have it anymore here.
|
||||||
|
raise ParseError(head[-1], "expected ';', got a block")
|
||||||
|
return ImportRule(uri, media, rule.line, rule.column)
|
||||||
|
|
||||||
|
elif rule.at_keyword == '@charset':
|
||||||
|
raise ParseError(rule, 'mis-placed or malformed @charset rule')
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ParseError(
|
||||||
|
rule, 'unknown at-rule in {0} context: {1}'.format(
|
||||||
|
context, rule.at_keyword))
|
||||||
|
|
||||||
|
def parse_media(self, tokens):
|
||||||
|
"""For CSS 2.1, parse a list of media types.
|
||||||
|
|
||||||
|
Media Queries are expected to override this.
|
||||||
|
|
||||||
|
:param tokens:
|
||||||
|
A list of tokens
|
||||||
|
:raises:
|
||||||
|
:class:`~.parsing.ParseError` on invalid media types/queries
|
||||||
|
:returns:
|
||||||
|
For CSS 2.1, a list of media types as strings
|
||||||
|
"""
|
||||||
|
if not tokens:
|
||||||
|
return ['all']
|
||||||
|
media_types = []
|
||||||
|
for part in split_on_comma(remove_whitespace(tokens)):
|
||||||
|
types = [token.type for token in part]
|
||||||
|
if types == ['IDENT']:
|
||||||
|
media_types.append(part[0].value)
|
||||||
|
else:
|
||||||
|
raise ParseError(
|
||||||
|
tokens[0], 'expected a media type' +
|
||||||
|
((', got ' + ', '.join(types)) if types else ''))
|
||||||
|
return media_types
|
||||||
|
|
||||||
|
def parse_page_selector(self, tokens):
|
||||||
|
"""Parse an @page selector.
|
||||||
|
|
||||||
|
:param tokens:
|
||||||
|
An iterable of token, typically from the ``head`` attribute of
|
||||||
|
an unparsed :class:`AtRule`.
|
||||||
|
:returns:
|
||||||
|
A page selector. For CSS 2.1, this is ``'first'``, ``'left'``,
|
||||||
|
``'right'`` or ``None``.
|
||||||
|
:raises:
|
||||||
|
:class:`~.parsing.ParseError` on invalid selectors
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not tokens:
|
||||||
|
return None, (0, 0)
|
||||||
|
if (len(tokens) == 2 and tokens[0].type == ':' and
|
||||||
|
tokens[1].type == 'IDENT'):
|
||||||
|
pseudo_class = tokens[1].value
|
||||||
|
specificity = {
|
||||||
|
'first': (1, 0), 'left': (0, 1), 'right': (0, 1),
|
||||||
|
}.get(pseudo_class)
|
||||||
|
if specificity:
|
||||||
|
return pseudo_class, specificity
|
||||||
|
raise ParseError(tokens[0], 'invalid @page selector')
|
||||||
|
|
||||||
|
def parse_declarations_and_at_rules(self, tokens, context):
|
||||||
|
"""Parse a mixed list of declarations and at rules, as found eg.
|
||||||
|
in the body of an @page rule.
|
||||||
|
|
||||||
|
Note that to add supported at-rules inside @page,
|
||||||
|
:class:`~.page3.CSSPage3Parser` extends :meth:`parse_at_rule`,
|
||||||
|
not this method.
|
||||||
|
|
||||||
|
:param tokens:
|
||||||
|
An iterable of token, typically from the ``body`` attribute of
|
||||||
|
an unparsed :class:`AtRule`.
|
||||||
|
:param context:
|
||||||
|
An at-keyword such as ``'@page'``.
|
||||||
|
(Most at-rules are only allowed in some contexts.)
|
||||||
|
:returns:
|
||||||
|
A tuple of:
|
||||||
|
|
||||||
|
* A list of :class:`Declaration`
|
||||||
|
* A list of parsed at-rules (empty for CSS 2.1)
|
||||||
|
* A list of :class:`~.parsing.ParseError`
|
||||||
|
|
||||||
|
"""
|
||||||
|
at_rules = []
|
||||||
|
declarations = []
|
||||||
|
errors = []
|
||||||
|
tokens = iter(tokens)
|
||||||
|
for token in tokens:
|
||||||
|
if token.type == 'ATKEYWORD':
|
||||||
|
try:
|
||||||
|
rule = self.read_at_rule(token, tokens)
|
||||||
|
result = self.parse_at_rule(
|
||||||
|
rule, at_rules, errors, context)
|
||||||
|
at_rules.append(result)
|
||||||
|
except ParseError as err:
|
||||||
|
errors.append(err)
|
||||||
|
elif token.type != 'S':
|
||||||
|
declaration_tokens = []
|
||||||
|
while token and token.type != ';':
|
||||||
|
declaration_tokens.append(token)
|
||||||
|
token = next(tokens, None)
|
||||||
|
if declaration_tokens:
|
||||||
|
try:
|
||||||
|
declarations.append(
|
||||||
|
self.parse_declaration(declaration_tokens))
|
||||||
|
except ParseError as err:
|
||||||
|
errors.append(err)
|
||||||
|
return declarations, at_rules, errors
|
||||||
|
|
||||||
|
def parse_ruleset(self, first_token, tokens):
|
||||||
|
"""Parse a ruleset: a selector followed by declaration block.
|
||||||
|
|
||||||
|
:param first_token:
|
||||||
|
The first token of the ruleset (probably of the selector).
|
||||||
|
You may have read it already to distinguish the rule
|
||||||
|
from an at-rule.
|
||||||
|
:param tokens:
|
||||||
|
an iterator of subsequent tokens. Will be consumed just enough
|
||||||
|
for one ruleset.
|
||||||
|
:return:
|
||||||
|
a tuple of a :class:`RuleSet` and an error list.
|
||||||
|
The errors are recovered :class:`~.parsing.ParseError` in
|
||||||
|
declarations. (Parsing continues from the next declaration on such
|
||||||
|
errors.)
|
||||||
|
:raises:
|
||||||
|
:class:`~.parsing.ParseError` if the selector is invalid for the
|
||||||
|
core grammar.
|
||||||
|
Note a that a selector can be valid for the core grammar but
|
||||||
|
not for CSS 2.1 or another level.
|
||||||
|
|
||||||
|
"""
|
||||||
|
selector = []
|
||||||
|
for token in chain([first_token], tokens):
|
||||||
|
if token.type == '{':
|
||||||
|
# Parse/validate once we’ve read the whole rule
|
||||||
|
selector = strip_whitespace(selector)
|
||||||
|
if not selector:
|
||||||
|
raise ParseError(first_token, 'empty selector')
|
||||||
|
for selector_token in selector:
|
||||||
|
validate_any(selector_token, 'selector')
|
||||||
|
declarations, errors = self.parse_declaration_list(
|
||||||
|
token.content)
|
||||||
|
ruleset = RuleSet(selector, declarations,
|
||||||
|
first_token.line, first_token.column)
|
||||||
|
return ruleset, errors
|
||||||
|
else:
|
||||||
|
selector.append(token)
|
||||||
|
raise ParseError(token, 'no declaration block found for ruleset')
|
||||||
|
|
||||||
|
def parse_declaration_list(self, tokens):
|
||||||
|
"""Parse a ``;`` separated declaration list.
|
||||||
|
|
||||||
|
You may want to use :meth:`parse_declarations_and_at_rules` (or
|
||||||
|
some other method that uses :func:`parse_declaration` directly)
|
||||||
|
instead if you have not just declarations in the same context.
|
||||||
|
|
||||||
|
:param tokens:
|
||||||
|
an iterable of tokens. Should stop at (before) the end
|
||||||
|
of the block, as marked by ``}``.
|
||||||
|
:return:
|
||||||
|
a tuple of the list of valid :class:`Declaration` and a list
|
||||||
|
of :class:`~.parsing.ParseError`
|
||||||
|
|
||||||
|
"""
|
||||||
|
# split at ';'
|
||||||
|
parts = []
|
||||||
|
this_part = []
|
||||||
|
for token in tokens:
|
||||||
|
if token.type == ';':
|
||||||
|
parts.append(this_part)
|
||||||
|
this_part = []
|
||||||
|
else:
|
||||||
|
this_part.append(token)
|
||||||
|
parts.append(this_part)
|
||||||
|
|
||||||
|
declarations = []
|
||||||
|
errors = []
|
||||||
|
for tokens in parts:
|
||||||
|
tokens = strip_whitespace(tokens)
|
||||||
|
if tokens:
|
||||||
|
try:
|
||||||
|
declarations.append(self.parse_declaration(tokens))
|
||||||
|
except ParseError as exc:
|
||||||
|
errors.append(exc)
|
||||||
|
# Skip the entire declaration
|
||||||
|
return declarations, errors
|
||||||
|
|
||||||
|
def parse_declaration(self, tokens):
|
||||||
|
"""Parse a single declaration.
|
||||||
|
|
||||||
|
:param tokens:
|
||||||
|
an iterable of at least one token. Should stop at (before)
|
||||||
|
the end of the declaration, as marked by a ``;`` or ``}``.
|
||||||
|
Empty declarations (ie. consecutive ``;`` with only white space
|
||||||
|
in-between) should be skipped earlier and not passed to
|
||||||
|
this method.
|
||||||
|
:returns:
|
||||||
|
a :class:`Declaration`
|
||||||
|
:raises:
|
||||||
|
:class:`~.parsing.ParseError` if the tokens do not match the
|
||||||
|
'declaration' production of the core grammar.
|
||||||
|
|
||||||
|
"""
|
||||||
|
tokens = iter(tokens)
|
||||||
|
|
||||||
|
name_token = next(tokens) # assume there is at least one
|
||||||
|
if name_token.type == 'IDENT':
|
||||||
|
# CSS syntax is case-insensitive
|
||||||
|
property_name = name_token.value.lower()
|
||||||
|
else:
|
||||||
|
raise ParseError(
|
||||||
|
name_token, 'expected a property name, got {0}'.format(
|
||||||
|
name_token.type))
|
||||||
|
|
||||||
|
token = name_token # In case ``tokens`` is now empty
|
||||||
|
for token in tokens:
|
||||||
|
if token.type == ':':
|
||||||
|
break
|
||||||
|
elif token.type != 'S':
|
||||||
|
raise ParseError(
|
||||||
|
token, "expected ':', got {0}".format(token.type))
|
||||||
|
else:
|
||||||
|
raise ParseError(token, "expected ':'")
|
||||||
|
|
||||||
|
value = strip_whitespace(list(tokens))
|
||||||
|
if not value:
|
||||||
|
raise ParseError(token, 'expected a property value')
|
||||||
|
validate_value(value)
|
||||||
|
value, priority = self.parse_value_priority(value)
|
||||||
|
return Declaration(
|
||||||
|
property_name, value, priority, name_token.line, name_token.column)
|
||||||
|
|
||||||
|
def parse_value_priority(self, tokens):
|
||||||
|
"""Separate any ``!important`` marker at the end of a property value.
|
||||||
|
|
||||||
|
:param tokens:
|
||||||
|
A list of tokens for the property value.
|
||||||
|
:returns:
|
||||||
|
A tuple of the actual property value (a list of tokens)
|
||||||
|
and the :attr:`~Declaration.priority`.
|
||||||
|
"""
|
||||||
|
value = list(tokens)
|
||||||
|
# Walk the token list from the end
|
||||||
|
token = value.pop()
|
||||||
|
if token.type == 'IDENT' and token.value.lower() == 'important':
|
||||||
|
while value:
|
||||||
|
token = value.pop()
|
||||||
|
if token.type == 'DELIM' and token.value == '!':
|
||||||
|
# Skip any white space before the '!'
|
||||||
|
while value and value[-1].type == 'S':
|
||||||
|
value.pop()
|
||||||
|
if not value:
|
||||||
|
raise ParseError(
|
||||||
|
token, 'expected a value before !important')
|
||||||
|
return value, 'important'
|
||||||
|
# Skip white space between '!' and 'important'
|
||||||
|
elif token.type != 'S':
|
||||||
|
break
|
||||||
|
return tokens, None
|
|
@ -0,0 +1,252 @@
|
||||||
|
# coding: utf-8
|
||||||
|
"""
|
||||||
|
tinycss.decoding
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Decoding stylesheets from bytes to Unicode.
|
||||||
|
http://www.w3.org/TR/CSS21/syndata.html#charset
|
||||||
|
|
||||||
|
:copyright: (c) 2012 by Simon Sapin.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import operator
|
||||||
|
import re
|
||||||
|
from binascii import unhexlify
|
||||||
|
|
||||||
|
__all__ = ['decode'] # Everything else is implementation detail
|
||||||
|
|
||||||
|
|
||||||
|
def decode(css_bytes, protocol_encoding=None,
|
||||||
|
linking_encoding=None, document_encoding=None):
|
||||||
|
"""
|
||||||
|
Determine the character encoding from the passed metadata and the
|
||||||
|
``@charset`` rule in the stylesheet (if any); and decode accordingly.
|
||||||
|
If no encoding information is available or decoding fails,
|
||||||
|
decoding defaults to UTF-8 and then fall back on ISO-8859-1.
|
||||||
|
|
||||||
|
:param css_bytes:
|
||||||
|
a CSS stylesheet as a byte string
|
||||||
|
:param protocol_encoding:
|
||||||
|
The "charset" parameter of a "Content-Type" HTTP header (if any),
|
||||||
|
or similar metadata for other protocols.
|
||||||
|
:param linking_encoding:
|
||||||
|
``<link charset="">`` or other metadata from the linking mechanism
|
||||||
|
(if any)
|
||||||
|
:param document_encoding:
|
||||||
|
Encoding of the referring style sheet or document (if any)
|
||||||
|
:return:
|
||||||
|
A tuple of an Unicode string, with any BOM removed, and the
|
||||||
|
encoding that was used.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if protocol_encoding:
|
||||||
|
css_unicode = try_encoding(css_bytes, protocol_encoding)
|
||||||
|
if css_unicode is not None:
|
||||||
|
return css_unicode, protocol_encoding
|
||||||
|
for encoding, pattern in ENCODING_MAGIC_NUMBERS:
|
||||||
|
match = pattern(css_bytes)
|
||||||
|
if match:
|
||||||
|
has_at_charset = isinstance(encoding, tuple)
|
||||||
|
if has_at_charset:
|
||||||
|
extract, endianness = encoding
|
||||||
|
encoding = extract(match.group(1))
|
||||||
|
# Get an ASCII-only unicode value.
|
||||||
|
# This is the only thing that works on both Python 2 and 3
|
||||||
|
# for bytes.decode()
|
||||||
|
# Non-ASCII encoding names are invalid anyway,
|
||||||
|
# but make sure they stay invalid.
|
||||||
|
encoding = encoding.decode('ascii', 'replace')
|
||||||
|
encoding = encoding.replace('\ufffd', '?')
|
||||||
|
if encoding.replace('-', '').replace('_', '').lower() in [
|
||||||
|
'utf16', 'utf32']:
|
||||||
|
encoding += endianness
|
||||||
|
encoding = encoding.encode('ascii', 'replace').decode('ascii')
|
||||||
|
css_unicode = try_encoding(css_bytes, encoding)
|
||||||
|
if css_unicode and not (has_at_charset and not
|
||||||
|
css_unicode.startswith('@charset "')):
|
||||||
|
return css_unicode, encoding
|
||||||
|
break
|
||||||
|
for encoding in [linking_encoding, document_encoding]:
|
||||||
|
if encoding:
|
||||||
|
css_unicode = try_encoding(css_bytes, encoding)
|
||||||
|
if css_unicode is not None:
|
||||||
|
return css_unicode, encoding
|
||||||
|
css_unicode = try_encoding(css_bytes, 'UTF-8')
|
||||||
|
if css_unicode is not None:
|
||||||
|
return css_unicode, 'UTF-8'
|
||||||
|
return try_encoding(css_bytes, 'ISO-8859-1', fallback=False), 'ISO-8859-1'
|
||||||
|
|
||||||
|
|
||||||
|
def try_encoding(css_bytes, encoding, fallback=True):
|
||||||
|
if fallback:
|
||||||
|
try:
|
||||||
|
css_unicode = css_bytes.decode(encoding)
|
||||||
|
# LookupError means unknown encoding
|
||||||
|
except (UnicodeDecodeError, LookupError):
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
css_unicode = css_bytes.decode(encoding)
|
||||||
|
if css_unicode and css_unicode[0] == '\ufeff':
|
||||||
|
# Remove any Byte Order Mark
|
||||||
|
css_unicode = css_unicode[1:]
|
||||||
|
return css_unicode
|
||||||
|
|
||||||
|
|
||||||
|
def hex2re(hex_data):
|
||||||
|
return re.escape(unhexlify(hex_data.replace(' ', '').encode('ascii')))
|
||||||
|
|
||||||
|
|
||||||
|
class Slicer(object):
|
||||||
|
"""Slice()[start:stop:end] == slice(start, stop, end)"""
|
||||||
|
def __getitem__(self, slice_):
|
||||||
|
return operator.itemgetter(slice_)
|
||||||
|
|
||||||
|
Slice = Slicer()
|
||||||
|
|
||||||
|
|
||||||
|
# List of (bom_size, encoding, pattern)
|
||||||
|
# bom_size is in bytes and can be zero
|
||||||
|
# encoding is a string or (slice_, endianness) for "as specified"
|
||||||
|
# slice_ is a slice object.How to extract the specified
|
||||||
|
|
||||||
|
ENCODING_MAGIC_NUMBERS = [
|
||||||
|
((Slice[:], ''), re.compile(
|
||||||
|
hex2re('EF BB BF 40 63 68 61 72 73 65 74 20 22') +
|
||||||
|
b'([^\x22]*?)' +
|
||||||
|
hex2re('22 3B')).match),
|
||||||
|
|
||||||
|
('UTF-8', re.compile(
|
||||||
|
hex2re('EF BB BF')).match),
|
||||||
|
|
||||||
|
((Slice[:], ''), re.compile(
|
||||||
|
hex2re('40 63 68 61 72 73 65 74 20 22') +
|
||||||
|
b'([^\x22]*?)' +
|
||||||
|
hex2re('22 3B')).match),
|
||||||
|
|
||||||
|
((Slice[1::2], '-BE'), re.compile(
|
||||||
|
hex2re('FE FF 00 40 00 63 00 68 00 61 00 72 00 73 00 65 00'
|
||||||
|
'74 00 20 00 22') +
|
||||||
|
b'((\x00[^\x22])*?)' +
|
||||||
|
hex2re('00 22 00 3B')).match),
|
||||||
|
|
||||||
|
((Slice[1::2], '-BE'), re.compile(
|
||||||
|
hex2re('00 40 00 63 00 68 00 61 00 72 00 73 00 65 00 74 00'
|
||||||
|
'20 00 22') +
|
||||||
|
b'((\x00[^\x22])*?)' +
|
||||||
|
hex2re('00 22 00 3B')).match),
|
||||||
|
|
||||||
|
((Slice[::2], '-LE'), re.compile(
|
||||||
|
hex2re('FF FE 40 00 63 00 68 00 61 00 72 00 73 00 65 00 74'
|
||||||
|
'00 20 00 22 00') +
|
||||||
|
b'(([^\x22]\x00)*?)' +
|
||||||
|
hex2re('22 00 3B 00')).match),
|
||||||
|
|
||||||
|
((Slice[::2], '-LE'), re.compile(
|
||||||
|
hex2re('40 00 63 00 68 00 61 00 72 00 73 00 65 00 74 00 20'
|
||||||
|
'00 22 00') +
|
||||||
|
b'(([^\x22]\x00)*?)' +
|
||||||
|
hex2re('22 00 3B 00')).match),
|
||||||
|
|
||||||
|
((Slice[3::4], '-BE'), re.compile(
|
||||||
|
hex2re('00 00 FE FF 00 00 00 40 00 00 00 63 00 00 00 68 00'
|
||||||
|
'00 00 61 00 00 00 72 00 00 00 73 00 00 00 65 00 00'
|
||||||
|
'00 74 00 00 00 20 00 00 00 22') +
|
||||||
|
b'((\x00\x00\x00[^\x22])*?)' +
|
||||||
|
hex2re('00 00 00 22 00 00 00 3B')).match),
|
||||||
|
|
||||||
|
((Slice[3::4], '-BE'), re.compile(
|
||||||
|
hex2re('00 00 00 40 00 00 00 63 00 00 00 68 00 00 00 61 00'
|
||||||
|
'00 00 72 00 00 00 73 00 00 00 65 00 00 00 74 00 00'
|
||||||
|
'00 20 00 00 00 22') +
|
||||||
|
b'((\x00\x00\x00[^\x22])*?)' +
|
||||||
|
hex2re('00 00 00 22 00 00 00 3B')).match),
|
||||||
|
|
||||||
|
|
||||||
|
# Python does not support 2143 or 3412 endianness, AFAIK.
|
||||||
|
# I guess we could fix it up ourselves but meh. Patches welcome.
|
||||||
|
|
||||||
|
# ((Slice[2::4], '-2143'), re.compile(
|
||||||
|
# hex2re('00 00 FF FE 00 00 40 00 00 00 63 00 00 00 68 00 00'
|
||||||
|
# '00 61 00 00 00 72 00 00 00 73 00 00 00 65 00 00 00'
|
||||||
|
# '74 00 00 00 20 00 00 00 22 00') +
|
||||||
|
# b'((\x00\x00[^\x22]\x00)*?)' +
|
||||||
|
# hex2re('00 00 22 00 00 00 3B 00')).match),
|
||||||
|
|
||||||
|
# ((Slice[2::4], '-2143'), re.compile(
|
||||||
|
# hex2re('00 00 40 00 00 00 63 00 00 00 68 00 00 00 61 00 00'
|
||||||
|
# '00 72 00 00 00 73 00 00 00 65 00 00 00 74 00 00 00'
|
||||||
|
# '20 00 00 00 22 00') +
|
||||||
|
# b'((\x00\x00[^\x22]\x00)*?)' +
|
||||||
|
# hex2re('00 00 22 00 00 00 3B 00')).match),
|
||||||
|
|
||||||
|
# ((Slice[1::4], '-3412'), re.compile(
|
||||||
|
# hex2re('FE FF 00 00 00 40 00 00 00 63 00 00 00 68 00 00 00'
|
||||||
|
# '61 00 00 00 72 00 00 00 73 00 00 00 65 00 00 00 74'
|
||||||
|
# '00 00 00 20 00 00 00 22 00 00') +
|
||||||
|
# b'((\x00[^\x22]\x00\x00)*?)' +
|
||||||
|
# hex2re('00 22 00 00 00 3B 00 00')).match),
|
||||||
|
|
||||||
|
# ((Slice[1::4], '-3412'), re.compile(
|
||||||
|
# hex2re('00 40 00 00 00 63 00 00 00 68 00 00 00 61 00 00 00'
|
||||||
|
# '72 00 00 00 73 00 00 00 65 00 00 00 74 00 00 00 20'
|
||||||
|
# '00 00 00 22 00 00') +
|
||||||
|
# b'((\x00[^\x22]\x00\x00)*?)' +
|
||||||
|
# hex2re('00 22 00 00 00 3B 00 00')).match),
|
||||||
|
|
||||||
|
((Slice[::4], '-LE'), re.compile(
|
||||||
|
hex2re('FF FE 00 00 40 00 00 00 63 00 00 00 68 00 00 00 61'
|
||||||
|
'00 00 00 72 00 00 00 73 00 00 00 65 00 00 00 74 00'
|
||||||
|
'00 00 20 00 00 00 22 00 00 00') +
|
||||||
|
b'(([^\x22]\x00\x00\x00)*?)' +
|
||||||
|
hex2re('22 00 00 00 3B 00 00 00')).match),
|
||||||
|
|
||||||
|
((Slice[::4], '-LE'), re.compile(
|
||||||
|
hex2re('40 00 00 00 63 00 00 00 68 00 00 00 61 00 00 00 72'
|
||||||
|
'00 00 00 73 00 00 00 65 00 00 00 74 00 00 00 20 00'
|
||||||
|
'00 00 22 00 00 00') +
|
||||||
|
b'(([^\x22]\x00\x00\x00)*?)' +
|
||||||
|
hex2re('22 00 00 00 3B 00 00 00')).match),
|
||||||
|
|
||||||
|
('UTF-32-BE', re.compile(
|
||||||
|
hex2re('00 00 FE FF')).match),
|
||||||
|
|
||||||
|
('UTF-32-LE', re.compile(
|
||||||
|
hex2re('FF FE 00 00')).match),
|
||||||
|
|
||||||
|
# ('UTF-32-2143', re.compile(
|
||||||
|
# hex2re('00 00 FF FE')).match),
|
||||||
|
|
||||||
|
# ('UTF-32-3412', re.compile(
|
||||||
|
# hex2re('FE FF 00 00')).match),
|
||||||
|
|
||||||
|
('UTF-16-BE', re.compile(
|
||||||
|
hex2re('FE FF')).match),
|
||||||
|
|
||||||
|
('UTF-16-LE', re.compile(
|
||||||
|
hex2re('FF FE')).match),
|
||||||
|
|
||||||
|
|
||||||
|
# Some of there are supported by Python, but I didn’t bother.
|
||||||
|
# You know the story with patches ...
|
||||||
|
|
||||||
|
# # as specified, transcoded from EBCDIC to ASCII
|
||||||
|
# ('as_specified-EBCDIC', re.compile(
|
||||||
|
# hex2re('7C 83 88 81 99 A2 85 A3 40 7F')
|
||||||
|
# + b'([^\x7F]*?)'
|
||||||
|
# + hex2re('7F 5E')).match),
|
||||||
|
|
||||||
|
# # as specified, transcoded from IBM1026 to ASCII
|
||||||
|
# ('as_specified-IBM1026', re.compile(
|
||||||
|
# hex2re('AE 83 88 81 99 A2 85 A3 40 FC')
|
||||||
|
# + b'([^\xFC]*?)'
|
||||||
|
# + hex2re('FC 5E')).match),
|
||||||
|
|
||||||
|
# # as specified, transcoded from GSM 03.38 to ASCII
|
||||||
|
# ('as_specified-GSM_03.38', re.compile(
|
||||||
|
# hex2re('00 63 68 61 72 73 65 74 20 22')
|
||||||
|
# + b'([^\x22]*?)'
|
||||||
|
# + hex2re('22 3B')).match),
|
||||||
|
]
|
|
@ -0,0 +1,162 @@
|
||||||
|
# coding: utf-8
|
||||||
|
"""
|
||||||
|
tinycss.page3
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Support for CSS 3 Paged Media syntax:
|
||||||
|
http://dev.w3.org/csswg/css3-page/
|
||||||
|
|
||||||
|
Adds support for named page selectors and margin rules.
|
||||||
|
|
||||||
|
:copyright: (c) 2012 by Simon Sapin.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import division, unicode_literals
|
||||||
|
|
||||||
|
from .css21 import CSS21Parser, ParseError
|
||||||
|
|
||||||
|
|
||||||
|
class MarginRule(object):
|
||||||
|
"""A parsed at-rule for margin box.
|
||||||
|
|
||||||
|
.. attribute:: at_keyword
|
||||||
|
|
||||||
|
One of the 16 following strings:
|
||||||
|
|
||||||
|
* ``@top-left-corner``
|
||||||
|
* ``@top-left``
|
||||||
|
* ``@top-center``
|
||||||
|
* ``@top-right``
|
||||||
|
* ``@top-right-corner``
|
||||||
|
* ``@bottom-left-corner``
|
||||||
|
* ``@bottom-left``
|
||||||
|
* ``@bottom-center``
|
||||||
|
* ``@bottom-right``
|
||||||
|
* ``@bottom-right-corner``
|
||||||
|
* ``@left-top``
|
||||||
|
* ``@left-middle``
|
||||||
|
* ``@left-bottom``
|
||||||
|
* ``@right-top``
|
||||||
|
* ``@right-middle``
|
||||||
|
* ``@right-bottom``
|
||||||
|
|
||||||
|
.. attribute:: declarations
|
||||||
|
|
||||||
|
A list of :class:`~.css21.Declaration` objects.
|
||||||
|
|
||||||
|
.. attribute:: line
|
||||||
|
|
||||||
|
Source line where this was read.
|
||||||
|
|
||||||
|
.. attribute:: column
|
||||||
|
|
||||||
|
Source column where this was read.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, at_keyword, declarations, line, column):
|
||||||
|
self.at_keyword = at_keyword
|
||||||
|
self.declarations = declarations
|
||||||
|
self.line = line
|
||||||
|
self.column = column
|
||||||
|
|
||||||
|
|
||||||
|
class CSSPage3Parser(CSS21Parser):
|
||||||
|
"""Extend :class:`~.css21.CSS21Parser` for `CSS 3 Paged Media`_ syntax.
|
||||||
|
|
||||||
|
.. _CSS 3 Paged Media: http://dev.w3.org/csswg/css3-page/
|
||||||
|
|
||||||
|
Compared to CSS 2.1, the ``at_rules`` and ``selector`` attributes of
|
||||||
|
:class:`~.css21.PageRule` objects are modified:
|
||||||
|
|
||||||
|
* ``at_rules`` is not always empty, it is a list of :class:`MarginRule`
|
||||||
|
objects.
|
||||||
|
|
||||||
|
* ``selector``, instead of a single string, is a tuple of the page name
|
||||||
|
and the pseudo class. Each of these may be a ``None`` or a string.
|
||||||
|
|
||||||
|
+--------------------------+------------------------+
|
||||||
|
| CSS | Parsed selectors |
|
||||||
|
+==========================+========================+
|
||||||
|
| .. code-block:: css | .. code-block:: python |
|
||||||
|
| | |
|
||||||
|
| @page {} | (None, None) |
|
||||||
|
| @page :first {} | (None, 'first') |
|
||||||
|
| @page chapter {} | ('chapter', None) |
|
||||||
|
| @page table:right {} | ('table', 'right') |
|
||||||
|
+--------------------------+------------------------+
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
PAGE_MARGIN_AT_KEYWORDS = [
|
||||||
|
'@top-left-corner',
|
||||||
|
'@top-left',
|
||||||
|
'@top-center',
|
||||||
|
'@top-right',
|
||||||
|
'@top-right-corner',
|
||||||
|
'@bottom-left-corner',
|
||||||
|
'@bottom-left',
|
||||||
|
'@bottom-center',
|
||||||
|
'@bottom-right',
|
||||||
|
'@bottom-right-corner',
|
||||||
|
'@left-top',
|
||||||
|
'@left-middle',
|
||||||
|
'@left-bottom',
|
||||||
|
'@right-top',
|
||||||
|
'@right-middle',
|
||||||
|
'@right-bottom',
|
||||||
|
]
|
||||||
|
|
||||||
|
def parse_at_rule(self, rule, previous_rules, errors, context):
|
||||||
|
if rule.at_keyword in self.PAGE_MARGIN_AT_KEYWORDS:
|
||||||
|
if context != '@page':
|
||||||
|
raise ParseError(
|
||||||
|
rule, '{0} rule not allowed in {1}'.format(
|
||||||
|
rule.at_keyword, context))
|
||||||
|
if rule.head:
|
||||||
|
raise ParseError(
|
||||||
|
rule.head[0],
|
||||||
|
'unexpected {0} token in {1} rule header'.format(
|
||||||
|
rule.head[0].type, rule.at_keyword))
|
||||||
|
declarations, body_errors = self.parse_declaration_list(rule.body)
|
||||||
|
errors.extend(body_errors)
|
||||||
|
return MarginRule(
|
||||||
|
rule.at_keyword, declarations, rule.line, rule.column)
|
||||||
|
return super(CSSPage3Parser, self).parse_at_rule(
|
||||||
|
rule, previous_rules, errors, context)
|
||||||
|
|
||||||
|
def parse_page_selector(self, head):
|
||||||
|
"""Parse an @page selector.
|
||||||
|
|
||||||
|
:param head:
|
||||||
|
The ``head`` attribute of an unparsed :class:`AtRule`.
|
||||||
|
:returns:
|
||||||
|
A page selector. For CSS 2.1, this is 'first', 'left', 'right'
|
||||||
|
or None. 'blank' is added by GCPM.
|
||||||
|
:raises:
|
||||||
|
:class`~parsing.ParseError` on invalid selectors
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not head:
|
||||||
|
return (None, None), (0, 0, 0)
|
||||||
|
if head[0].type == 'IDENT':
|
||||||
|
name = head.pop(0).value
|
||||||
|
while head and head[0].type == 'S':
|
||||||
|
head.pop(0)
|
||||||
|
if not head:
|
||||||
|
return (name, None), (1, 0, 0)
|
||||||
|
name_specificity = (1,)
|
||||||
|
else:
|
||||||
|
name = None
|
||||||
|
name_specificity = (0,)
|
||||||
|
if (len(head) == 2 and head[0].type == ':' and
|
||||||
|
head[1].type == 'IDENT'):
|
||||||
|
pseudo_class = head[1].value
|
||||||
|
specificity = {
|
||||||
|
'first': (1, 0), 'blank': (1, 0),
|
||||||
|
'left': (0, 1), 'right': (0, 1),
|
||||||
|
}.get(pseudo_class)
|
||||||
|
if specificity:
|
||||||
|
return (name, pseudo_class), (name_specificity + specificity)
|
||||||
|
raise ParseError(head[0], 'invalid @page selector')
|
|
@ -0,0 +1,166 @@
|
||||||
|
# coding: utf-8
|
||||||
|
"""
|
||||||
|
tinycss.parsing
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Utilities for parsing lists of tokens.
|
||||||
|
|
||||||
|
:copyright: (c) 2012 by Simon Sapin.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: unit tests
|
||||||
|
|
||||||
|
def split_on_comma(tokens):
|
||||||
|
"""Split a list of tokens on commas, ie ``,`` DELIM tokens.
|
||||||
|
|
||||||
|
Only "top-level" comma tokens are splitting points, not commas inside a
|
||||||
|
function or other :class:`ContainerToken`.
|
||||||
|
|
||||||
|
:param tokens:
|
||||||
|
An iterable of :class:`~.token_data.Token` or
|
||||||
|
:class:`~.token_data.ContainerToken`.
|
||||||
|
:returns:
|
||||||
|
A list of lists of tokens
|
||||||
|
|
||||||
|
"""
|
||||||
|
parts = []
|
||||||
|
this_part = []
|
||||||
|
for token in tokens:
|
||||||
|
if token.type == 'DELIM' and token.value == ',':
|
||||||
|
parts.append(this_part)
|
||||||
|
this_part = []
|
||||||
|
else:
|
||||||
|
this_part.append(token)
|
||||||
|
parts.append(this_part)
|
||||||
|
return parts
|
||||||
|
|
||||||
|
|
||||||
|
def strip_whitespace(tokens):
|
||||||
|
"""Remove whitespace at the beggining and end of a token list.
|
||||||
|
|
||||||
|
Whitespace tokens in-between other tokens in the list are preserved.
|
||||||
|
|
||||||
|
:param tokens:
|
||||||
|
A list of :class:`~.token_data.Token` or
|
||||||
|
:class:`~.token_data.ContainerToken`.
|
||||||
|
:return:
|
||||||
|
A new sub-sequence of the list.
|
||||||
|
|
||||||
|
"""
|
||||||
|
for i, token in enumerate(tokens):
|
||||||
|
if token.type != 'S':
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return [] # only whitespace
|
||||||
|
tokens = tokens[i:]
|
||||||
|
while tokens and tokens[-1].type == 'S':
|
||||||
|
tokens.pop()
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
|
||||||
|
def remove_whitespace(tokens):
|
||||||
|
"""Remove any top-level whitespace in a token list.
|
||||||
|
|
||||||
|
Whitespace tokens inside recursive :class:`~.token_data.ContainerToken`
|
||||||
|
are preserved.
|
||||||
|
|
||||||
|
:param tokens:
|
||||||
|
A list of :class:`~.token_data.Token` or
|
||||||
|
:class:`~.token_data.ContainerToken`.
|
||||||
|
:return:
|
||||||
|
A new sub-sequence of the list.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return [token for token in tokens if token.type != 'S']
|
||||||
|
|
||||||
|
|
||||||
|
def validate_value(tokens):
|
||||||
|
"""Validate a property value.
|
||||||
|
|
||||||
|
:param tokens:
|
||||||
|
an iterable of tokens
|
||||||
|
:raises:
|
||||||
|
:class:`ParseError` if there is any invalid token for the 'value'
|
||||||
|
production of the core grammar.
|
||||||
|
|
||||||
|
"""
|
||||||
|
for token in tokens:
|
||||||
|
type_ = token.type
|
||||||
|
if type_ == '{':
|
||||||
|
validate_block(token.content, 'property value')
|
||||||
|
else:
|
||||||
|
validate_any(token, 'property value')
|
||||||
|
|
||||||
|
|
||||||
|
def validate_block(tokens, context):
|
||||||
|
"""
|
||||||
|
:raises:
|
||||||
|
:class:`ParseError` if there is any invalid token for the 'block'
|
||||||
|
production of the core grammar.
|
||||||
|
:param tokens: an iterable of tokens
|
||||||
|
:param context: a string for the 'unexpected in ...' message
|
||||||
|
|
||||||
|
"""
|
||||||
|
for token in tokens:
|
||||||
|
type_ = token.type
|
||||||
|
if type_ == '{':
|
||||||
|
validate_block(token.content, context)
|
||||||
|
elif type_ not in (';', 'ATKEYWORD'):
|
||||||
|
validate_any(token, context)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_any(token, context):
|
||||||
|
"""
|
||||||
|
:raises:
|
||||||
|
:class:`ParseError` if this is an invalid token for the
|
||||||
|
'any' production of the core grammar.
|
||||||
|
:param token: a single token
|
||||||
|
:param context: a string for the 'unexpected in ...' message
|
||||||
|
|
||||||
|
"""
|
||||||
|
type_ = token.type
|
||||||
|
if type_ in ('FUNCTION', '(', '['):
|
||||||
|
for token in token.content:
|
||||||
|
validate_any(token, type_)
|
||||||
|
elif type_ not in ('S', 'IDENT', 'DIMENSION', 'PERCENTAGE', 'NUMBER',
|
||||||
|
'INTEGER', 'URI', 'DELIM', 'STRING', 'HASH', ':',
|
||||||
|
'UNICODE-RANGE'):
|
||||||
|
if type_ in ('}', ')', ']'):
|
||||||
|
adjective = 'unmatched'
|
||||||
|
else:
|
||||||
|
adjective = 'unexpected'
|
||||||
|
raise ParseError(
|
||||||
|
token, '{0} {1} token in {2}'.format(adjective, type_, context))
|
||||||
|
|
||||||
|
|
||||||
|
class ParseError(ValueError):
|
||||||
|
"""Details about a CSS syntax error. Usually indicates that something
|
||||||
|
(a rule or a declaration) was ignored and will not appear as a parsed
|
||||||
|
object.
|
||||||
|
|
||||||
|
This exception is typically logged in a list rather than being propagated
|
||||||
|
to the user API.
|
||||||
|
|
||||||
|
.. attribute:: line
|
||||||
|
|
||||||
|
Source line where the error occured.
|
||||||
|
|
||||||
|
.. attribute:: column
|
||||||
|
|
||||||
|
Column in the source line where the error occured.
|
||||||
|
|
||||||
|
.. attribute:: reason
|
||||||
|
|
||||||
|
What happend (a string).
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, subject, reason):
|
||||||
|
self.line = subject.line
|
||||||
|
self.column = subject.column
|
||||||
|
self.reason = reason
|
||||||
|
super(ParseError, self).__init__(
|
||||||
|
'Parse error at {0.line}:{0.column}, {0.reason}'.format(self))
|
|
@ -0,0 +1,189 @@
|
||||||
|
# coding: utf-8
|
||||||
|
"""
|
||||||
|
tinycss.speedups
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Cython module for speeding up inner loops.
|
||||||
|
|
||||||
|
Right now only :func:`tokenize_flat` has a second implementation.
|
||||||
|
|
||||||
|
:copyright: (c) 2010 by Simon Sapin.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .token_data import (
|
||||||
|
COMPILED_TOKEN_REGEXPS, UNICODE_UNESCAPE, NEWLINE_UNESCAPE,
|
||||||
|
SIMPLE_UNESCAPE, FIND_NEWLINES, TOKEN_DISPATCH)
|
||||||
|
|
||||||
|
|
||||||
|
COMPILED_TOKEN_INDEXES = dict(
|
||||||
|
(name, i) for i, (name, regexp) in enumerate(COMPILED_TOKEN_REGEXPS))
|
||||||
|
|
||||||
|
|
||||||
|
cdef class CToken:
|
||||||
|
"""A token built by the Cython speedups. Identical to
|
||||||
|
:class:`~.token_data.Token`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
is_container = False
|
||||||
|
|
||||||
|
cdef public object type, _as_css, value, unit
|
||||||
|
cdef public Py_ssize_t line, column
|
||||||
|
|
||||||
|
def __init__(self, type_, css_value, value, unit, line, column):
|
||||||
|
self.type = type_
|
||||||
|
self._as_css = css_value
|
||||||
|
self.value = value
|
||||||
|
self.unit = unit
|
||||||
|
self.line = line
|
||||||
|
self.column = column
|
||||||
|
|
||||||
|
def as_css(self):
|
||||||
|
"""
|
||||||
|
Return as an Unicode string the CSS representation of the token,
|
||||||
|
as parsed in the source.
|
||||||
|
"""
|
||||||
|
return self._as_css
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return ('<Token {0.type} at {0.line}:{0.column} {0.value!r}{1}>'
|
||||||
|
.format(self, self.unit or ''))
|
||||||
|
|
||||||
|
|
||||||
|
def tokenize_flat(css_source, int ignore_comments=1):
|
||||||
|
"""
|
||||||
|
:param css_source:
|
||||||
|
CSS as an unicode string
|
||||||
|
:param ignore_comments:
|
||||||
|
if true (the default) comments will not be included in the
|
||||||
|
return value
|
||||||
|
:return:
|
||||||
|
An iterator of :class:`Token`
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Make these local variable to avoid global lookups in the loop
|
||||||
|
tokens_dispatch = TOKEN_DISPATCH
|
||||||
|
compiled_token_indexes = COMPILED_TOKEN_INDEXES
|
||||||
|
compiled_tokens = COMPILED_TOKEN_REGEXPS
|
||||||
|
unicode_unescape = UNICODE_UNESCAPE
|
||||||
|
newline_unescape = NEWLINE_UNESCAPE
|
||||||
|
simple_unescape = SIMPLE_UNESCAPE
|
||||||
|
find_newlines = FIND_NEWLINES
|
||||||
|
|
||||||
|
# Use the integer indexes instead of string markers
|
||||||
|
cdef Py_ssize_t BAD_COMMENT = compiled_token_indexes['BAD_COMMENT']
|
||||||
|
cdef Py_ssize_t BAD_STRING = compiled_token_indexes['BAD_STRING']
|
||||||
|
cdef Py_ssize_t PERCENTAGE = compiled_token_indexes['PERCENTAGE']
|
||||||
|
cdef Py_ssize_t DIMENSION = compiled_token_indexes['DIMENSION']
|
||||||
|
cdef Py_ssize_t ATKEYWORD = compiled_token_indexes['ATKEYWORD']
|
||||||
|
cdef Py_ssize_t FUNCTION = compiled_token_indexes['FUNCTION']
|
||||||
|
cdef Py_ssize_t COMMENT = compiled_token_indexes['COMMENT']
|
||||||
|
cdef Py_ssize_t NUMBER = compiled_token_indexes['NUMBER']
|
||||||
|
cdef Py_ssize_t STRING = compiled_token_indexes['STRING']
|
||||||
|
cdef Py_ssize_t IDENT = compiled_token_indexes['IDENT']
|
||||||
|
cdef Py_ssize_t HASH = compiled_token_indexes['HASH']
|
||||||
|
cdef Py_ssize_t URI = compiled_token_indexes['URI']
|
||||||
|
cdef Py_ssize_t DELIM = -1
|
||||||
|
|
||||||
|
cdef Py_ssize_t pos = 0
|
||||||
|
cdef Py_ssize_t line = 1
|
||||||
|
cdef Py_ssize_t column = 1
|
||||||
|
cdef Py_ssize_t source_len = len(css_source)
|
||||||
|
cdef Py_ssize_t n_tokens = len(compiled_tokens)
|
||||||
|
cdef Py_ssize_t length, next_pos, type_
|
||||||
|
cdef CToken token
|
||||||
|
|
||||||
|
tokens = []
|
||||||
|
while pos < source_len:
|
||||||
|
char = css_source[pos]
|
||||||
|
if char in ':;{}()[]':
|
||||||
|
type_ = -1 # not parsed further anyway
|
||||||
|
type_name = char
|
||||||
|
css_value = char
|
||||||
|
else:
|
||||||
|
codepoint = min(ord(char), 160)
|
||||||
|
for type_, type_name, regexp in tokens_dispatch[codepoint]:
|
||||||
|
match = regexp(css_source, pos)
|
||||||
|
if match:
|
||||||
|
# First match is the longest. See comments on TOKENS above.
|
||||||
|
css_value = match.group()
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# No match.
|
||||||
|
# "Any other character not matched by the above rules,
|
||||||
|
# and neither a single nor a double quote."
|
||||||
|
# ... but quotes at the start of a token are always matched
|
||||||
|
# by STRING or BAD_STRING. So DELIM is any single character.
|
||||||
|
type_ = DELIM
|
||||||
|
type_name = 'DELIM'
|
||||||
|
css_value = char
|
||||||
|
length = len(css_value)
|
||||||
|
next_pos = pos + length
|
||||||
|
|
||||||
|
# A BAD_COMMENT is a comment at EOF. Ignore it too.
|
||||||
|
if not (ignore_comments and type_ in (COMMENT, BAD_COMMENT)):
|
||||||
|
# Parse numbers, extract strings and URIs, unescape
|
||||||
|
unit = None
|
||||||
|
if type_ == DIMENSION:
|
||||||
|
value = match.group(1)
|
||||||
|
value = float(value) if '.' in value else int(value)
|
||||||
|
unit = match.group(2)
|
||||||
|
unit = simple_unescape(unit)
|
||||||
|
unit = unicode_unescape(unit)
|
||||||
|
unit = unit.lower() # normalize
|
||||||
|
elif type_ == PERCENTAGE:
|
||||||
|
value = css_value[:-1]
|
||||||
|
value = float(value) if '.' in value else int(value)
|
||||||
|
unit = '%'
|
||||||
|
elif type_ == NUMBER:
|
||||||
|
value = css_value
|
||||||
|
if '.' in value:
|
||||||
|
value = float(value)
|
||||||
|
else:
|
||||||
|
value = int(value)
|
||||||
|
type_name = 'INTEGER'
|
||||||
|
elif type_ in (IDENT, ATKEYWORD, HASH, FUNCTION):
|
||||||
|
value = simple_unescape(css_value)
|
||||||
|
value = unicode_unescape(value)
|
||||||
|
elif type_ == URI:
|
||||||
|
value = match.group(1)
|
||||||
|
if value and value[0] in '"\'':
|
||||||
|
value = value[1:-1] # Remove quotes
|
||||||
|
value = newline_unescape(value)
|
||||||
|
value = simple_unescape(value)
|
||||||
|
value = unicode_unescape(value)
|
||||||
|
elif type_ == STRING:
|
||||||
|
value = css_value[1:-1] # Remove quotes
|
||||||
|
value = newline_unescape(value)
|
||||||
|
value = simple_unescape(value)
|
||||||
|
value = unicode_unescape(value)
|
||||||
|
# BAD_STRING can only be one of:
|
||||||
|
# * Unclosed string at the end of the stylesheet:
|
||||||
|
# Close the string, but this is not an error.
|
||||||
|
# Make it a "good" STRING token.
|
||||||
|
# * Unclosed string at the (unescaped) end of the line:
|
||||||
|
# Close the string, but this is an error.
|
||||||
|
# Leave it as a BAD_STRING, don’t bother parsing it.
|
||||||
|
# See http://www.w3.org/TR/CSS21/syndata.html#parsing-errors
|
||||||
|
elif type_ == BAD_STRING and next_pos == source_len:
|
||||||
|
type_name = 'STRING'
|
||||||
|
value = css_value[1:] # Remove quote
|
||||||
|
value = newline_unescape(value)
|
||||||
|
value = simple_unescape(value)
|
||||||
|
value = unicode_unescape(value)
|
||||||
|
else:
|
||||||
|
value = css_value
|
||||||
|
token = CToken(type_name, css_value, value, unit, line, column)
|
||||||
|
tokens.append(token)
|
||||||
|
|
||||||
|
pos = next_pos
|
||||||
|
newlines = list(find_newlines(css_value))
|
||||||
|
if newlines:
|
||||||
|
line += len(newlines)
|
||||||
|
# Add 1 to have lines start at column 1, not 0
|
||||||
|
column = length - newlines[-1].end() + 1
|
||||||
|
else:
|
||||||
|
column += length
|
||||||
|
return tokens
|
|
@ -0,0 +1,26 @@
|
||||||
|
# coding: utf-8
|
||||||
|
"""
|
||||||
|
Test suite for tinycss
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
:copyright: (c) 2012 by Simon Sapin.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
# Awful workaround to fix isort's "sys.setdefaultencoding('utf-8')".
|
||||||
|
if sys.version_info[0] == 2:
|
||||||
|
reload(sys) # noqa
|
||||||
|
sys.setdefaultencoding('ascii')
|
||||||
|
|
||||||
|
|
||||||
|
def assert_errors(errors, expected_errors):
|
||||||
|
"""Test not complete error messages but only substrings."""
|
||||||
|
assert len(errors) == len(expected_errors)
|
||||||
|
for error, expected in zip(errors, expected_errors):
|
||||||
|
assert expected in str(error)
|
|
@ -0,0 +1,134 @@
|
||||||
|
# coding: utf-8
|
||||||
|
"""
|
||||||
|
Speed tests
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Note: this file is not named test_*.py as it is not part of the
|
||||||
|
test suite ran by pytest.
|
||||||
|
|
||||||
|
:copyright: (c) 2012 by Simon Sapin.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import division, unicode_literals
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import functools
|
||||||
|
import os.path
|
||||||
|
import sys
|
||||||
|
import timeit
|
||||||
|
|
||||||
|
from cssutils import parseString
|
||||||
|
|
||||||
|
from .. import tokenizer
|
||||||
|
from ..css21 import CSS21Parser
|
||||||
|
from ..parsing import remove_whitespace
|
||||||
|
|
||||||
|
CSS_REPEAT = 4
|
||||||
|
TIMEIT_REPEAT = 3
|
||||||
|
TIMEIT_NUMBER = 20
|
||||||
|
|
||||||
|
|
||||||
|
def load_css():
|
||||||
|
filename = os.path.join(os.path.dirname(__file__),
|
||||||
|
'..', '..', 'docs', '_static', 'custom.css')
|
||||||
|
with open(filename, 'rb') as fd:
|
||||||
|
return b'\n'.join([fd.read()] * CSS_REPEAT)
|
||||||
|
|
||||||
|
|
||||||
|
# Pre-load so that I/O is not measured
|
||||||
|
CSS = load_css()
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def install_tokenizer(name):
|
||||||
|
original = tokenizer.tokenize_flat
|
||||||
|
try:
|
||||||
|
tokenizer.tokenize_flat = getattr(tokenizer, name)
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
tokenizer.tokenize_flat = original
|
||||||
|
|
||||||
|
|
||||||
|
def parse(tokenizer_name):
|
||||||
|
with install_tokenizer(tokenizer_name):
|
||||||
|
stylesheet = CSS21Parser().parse_stylesheet_bytes(CSS)
|
||||||
|
result = []
|
||||||
|
for rule in stylesheet.rules:
|
||||||
|
selector = rule.selector.as_css()
|
||||||
|
declarations = [
|
||||||
|
(declaration.name, len(list(remove_whitespace(declaration.value))))
|
||||||
|
for declaration in rule.declarations]
|
||||||
|
result.append((selector, declarations))
|
||||||
|
return result
|
||||||
|
|
||||||
|
parse_cython = functools.partial(parse, 'cython_tokenize_flat')
|
||||||
|
parse_python = functools.partial(parse, 'python_tokenize_flat')
|
||||||
|
|
||||||
|
|
||||||
|
def parse_cssutils():
|
||||||
|
stylesheet = parseString(CSS)
|
||||||
|
result = []
|
||||||
|
for rule in stylesheet.cssRules:
|
||||||
|
selector = rule.selectorText
|
||||||
|
declarations = [
|
||||||
|
(declaration.name, len(list(declaration.propertyValue)))
|
||||||
|
for declaration in rule.style.getProperties(all=True)]
|
||||||
|
result.append((selector, declarations))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def check_consistency():
|
||||||
|
result = parse_python()
|
||||||
|
assert len(result) > 0
|
||||||
|
if tokenizer.cython_tokenize_flat:
|
||||||
|
assert parse_cython() == result
|
||||||
|
assert parse_cssutils() == result
|
||||||
|
version = '.'.join(map(str, sys.version_info[:3]))
|
||||||
|
print('Python {}, consistency OK.'.format(version))
|
||||||
|
|
||||||
|
|
||||||
|
def warm_up():
|
||||||
|
is_pypy = hasattr(sys, 'pypy_translation_info')
|
||||||
|
if is_pypy:
|
||||||
|
print('Warming up for PyPy...')
|
||||||
|
for i in range(80):
|
||||||
|
for i in range(10):
|
||||||
|
parse_python()
|
||||||
|
parse_cssutils()
|
||||||
|
sys.stdout.write('.')
|
||||||
|
sys.stdout.flush()
|
||||||
|
sys.stdout.write('\n')
|
||||||
|
|
||||||
|
|
||||||
|
def time(function):
|
||||||
|
seconds = timeit.Timer(function).repeat(TIMEIT_REPEAT, TIMEIT_NUMBER)
|
||||||
|
miliseconds = int(min(seconds) * 1000)
|
||||||
|
return miliseconds
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
if tokenizer.cython_tokenize_flat:
|
||||||
|
data_set = [
|
||||||
|
('tinycss + speedups ', parse_cython),
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
print('Speedups are NOT available.')
|
||||||
|
data_set = []
|
||||||
|
data_set += [
|
||||||
|
('tinycss WITHOUT speedups', parse_python),
|
||||||
|
('cssutils ', parse_cssutils),
|
||||||
|
]
|
||||||
|
label, function = data_set.pop(0)
|
||||||
|
ref = time(function)
|
||||||
|
print('{} {} ms'.format(label, ref))
|
||||||
|
for label, function in data_set:
|
||||||
|
result = time(function)
|
||||||
|
print('{} {} ms {:.2f}x'.format(label, result, result / ref))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
check_consistency()
|
||||||
|
warm_up()
|
||||||
|
run()
|
|
@ -0,0 +1,45 @@
|
||||||
|
# coding: utf-8
|
||||||
|
"""
|
||||||
|
Tests for the public API
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
:copyright: (c) 2012 by Simon Sapin.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from pytest import raises
|
||||||
|
from tinycss import make_parser
|
||||||
|
from tinycss.page3 import CSSPage3Parser
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_parser():
|
||||||
|
class MyParser(object):
|
||||||
|
def __init__(self, some_config):
|
||||||
|
self.some_config = some_config
|
||||||
|
|
||||||
|
parsers = [
|
||||||
|
make_parser(),
|
||||||
|
make_parser('page3'),
|
||||||
|
make_parser(CSSPage3Parser),
|
||||||
|
make_parser(MyParser, some_config=42),
|
||||||
|
make_parser(CSSPage3Parser, MyParser, some_config=42),
|
||||||
|
make_parser(MyParser, 'page3', some_config=42),
|
||||||
|
]
|
||||||
|
|
||||||
|
for parser, exp in zip(parsers, [False, True, True, False, True, True]):
|
||||||
|
assert isinstance(parser, CSSPage3Parser) == exp
|
||||||
|
|
||||||
|
for parser, exp in zip(parsers, [False, False, False, True, True, True]):
|
||||||
|
assert isinstance(parser, MyParser) == exp
|
||||||
|
|
||||||
|
for parser in parsers[3:]:
|
||||||
|
assert parser.some_config == 42
|
||||||
|
|
||||||
|
# Extra or missing named parameters
|
||||||
|
raises(TypeError, make_parser, some_config=4)
|
||||||
|
raises(TypeError, make_parser, 'page3', some_config=4)
|
||||||
|
raises(TypeError, make_parser, MyParser)
|
||||||
|
raises(TypeError, make_parser, MyParser, some_config=4, other_config=7)
|
|
@ -0,0 +1,202 @@
|
||||||
|
# coding: utf-8
|
||||||
|
"""
|
||||||
|
Tests for the CSS 3 color parser
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
:copyright: (c) 2012 by Simon Sapin.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from tinycss.color3 import hsl_to_rgb, parse_color_string
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(('css_source', 'expected_result'), [
|
||||||
|
('', None),
|
||||||
|
(' /* hey */\n', None),
|
||||||
|
('4', None),
|
||||||
|
('top', None),
|
||||||
|
('/**/transparent', (0, 0, 0, 0)),
|
||||||
|
('transparent', (0, 0, 0, 0)),
|
||||||
|
(' transparent\n', (0, 0, 0, 0)),
|
||||||
|
('TransParent', (0, 0, 0, 0)),
|
||||||
|
('currentColor', 'currentColor'),
|
||||||
|
('CURRENTcolor', 'currentColor'),
|
||||||
|
('current_Color', None),
|
||||||
|
|
||||||
|
('black', (0, 0, 0, 1)),
|
||||||
|
('white', (1, 1, 1, 1)),
|
||||||
|
('fuchsia', (1, 0, 1, 1)),
|
||||||
|
('cyan', (0, 1, 1, 1)),
|
||||||
|
('CyAn', (0, 1, 1, 1)),
|
||||||
|
('darkkhaki', (189 / 255., 183 / 255., 107 / 255., 1)),
|
||||||
|
|
||||||
|
('#', None),
|
||||||
|
('#f', None),
|
||||||
|
('#ff', None),
|
||||||
|
('#fff', (1, 1, 1, 1)),
|
||||||
|
('#ffg', None),
|
||||||
|
('#ffff', None),
|
||||||
|
('#fffff', None),
|
||||||
|
('#ffffff', (1, 1, 1, 1)),
|
||||||
|
('#fffffg', None),
|
||||||
|
('#fffffff', None),
|
||||||
|
('#ffffffff', None),
|
||||||
|
('#fffffffff', None),
|
||||||
|
|
||||||
|
('#cba987', (203 / 255., 169 / 255., 135 / 255., 1)),
|
||||||
|
('#CbA987', (203 / 255., 169 / 255., 135 / 255., 1)),
|
||||||
|
('#1122aA', (17 / 255., 34 / 255., 170 / 255., 1)),
|
||||||
|
('#12a', (17 / 255., 34 / 255., 170 / 255., 1)),
|
||||||
|
|
||||||
|
('rgb(203, 169, 135)', (203 / 255., 169 / 255., 135 / 255., 1)),
|
||||||
|
('RGB(255, 255, 255)', (1, 1, 1, 1)),
|
||||||
|
('rgB(0, 0, 0)', (0, 0, 0, 1)),
|
||||||
|
('rgB(0, 51, 255)', (0, .2, 1, 1)),
|
||||||
|
('rgb(0,51,255)', (0, .2, 1, 1)),
|
||||||
|
('rgb(0\t, 51 ,255)', (0, .2, 1, 1)),
|
||||||
|
('rgb(/* R */0, /* G */51, /* B */255)', (0, .2, 1, 1)),
|
||||||
|
('rgb(-51, 306, 0)', (-.2, 1.2, 0, 1)), # out of 0..1 is allowed
|
||||||
|
|
||||||
|
('rgb(42%, 3%, 50%)', (.42, .03, .5, 1)),
|
||||||
|
('RGB(100%, 100%, 100%)', (1, 1, 1, 1)),
|
||||||
|
('rgB(0%, 0%, 0%)', (0, 0, 0, 1)),
|
||||||
|
('rgB(10%, 20%, 30%)', (.1, .2, .3, 1)),
|
||||||
|
('rgb(10%,20%,30%)', (.1, .2, .3, 1)),
|
||||||
|
('rgb(10%\t, 20% ,30%)', (.1, .2, .3, 1)),
|
||||||
|
('rgb(/* R */10%, /* G */20%, /* B */30%)', (.1, .2, .3, 1)),
|
||||||
|
('rgb(-12%, 110%, 1400%)', (-.12, 1.1, 14, 1)), # out of 0..1 is allowed
|
||||||
|
|
||||||
|
('rgb(10%, 50%, 0)', None),
|
||||||
|
('rgb(255, 50%, 0%)', None),
|
||||||
|
('rgb(0, 0 0)', None),
|
||||||
|
('rgb(0, 0, 0deg)', None),
|
||||||
|
('rgb(0, 0, light)', None),
|
||||||
|
('rgb()', None),
|
||||||
|
('rgb(0)', None),
|
||||||
|
('rgb(0, 0)', None),
|
||||||
|
('rgb(0, 0, 0, 0)', None),
|
||||||
|
('rgb(0%)', None),
|
||||||
|
('rgb(0%, 0%)', None),
|
||||||
|
('rgb(0%, 0%, 0%, 0%)', None),
|
||||||
|
('rgb(0%, 0%, 0%, 0)', None),
|
||||||
|
|
||||||
|
('rgba(0, 0, 0, 0)', (0, 0, 0, 0)),
|
||||||
|
('rgba(203, 169, 135, 0.3)', (203 / 255., 169 / 255., 135 / 255., 0.3)),
|
||||||
|
('RGBA(255, 255, 255, 0)', (1, 1, 1, 0)),
|
||||||
|
('rgBA(0, 51, 255, 1)', (0, 0.2, 1, 1)),
|
||||||
|
('rgba(0, 51, 255, 1.1)', (0, 0.2, 1, 1)),
|
||||||
|
('rgba(0, 51, 255, 37)', (0, 0.2, 1, 1)),
|
||||||
|
('rgba(0, 51, 255, 0.42)', (0, 0.2, 1, 0.42)),
|
||||||
|
('rgba(0, 51, 255, 0)', (0, 0.2, 1, 0)),
|
||||||
|
('rgba(0, 51, 255, -0.1)', (0, 0.2, 1, 0)),
|
||||||
|
('rgba(0, 51, 255, -139)', (0, 0.2, 1, 0)),
|
||||||
|
|
||||||
|
('rgba(42%, 3%, 50%, 0.3)', (.42, .03, .5, 0.3)),
|
||||||
|
('RGBA(100%, 100%, 100%, 0)', (1, 1, 1, 0)),
|
||||||
|
('rgBA(0%, 20%, 100%, 1)', (0, 0.2, 1, 1)),
|
||||||
|
('rgba(0%, 20%, 100%, 1.1)', (0, 0.2, 1, 1)),
|
||||||
|
('rgba(0%, 20%, 100%, 37)', (0, 0.2, 1, 1)),
|
||||||
|
('rgba(0%, 20%, 100%, 0.42)', (0, 0.2, 1, 0.42)),
|
||||||
|
('rgba(0%, 20%, 100%, 0)', (0, 0.2, 1, 0)),
|
||||||
|
('rgba(0%, 20%, 100%, -0.1)', (0, 0.2, 1, 0)),
|
||||||
|
('rgba(0%, 20%, 100%, -139)', (0, 0.2, 1, 0)),
|
||||||
|
|
||||||
|
('rgba(255, 255, 255, 0%)', None),
|
||||||
|
('rgba(10%, 50%, 0, 1)', None),
|
||||||
|
('rgba(255, 50%, 0%, 1)', None),
|
||||||
|
('rgba(0, 0, 0 0)', None),
|
||||||
|
('rgba(0, 0, 0, 0deg)', None),
|
||||||
|
('rgba(0, 0, 0, light)', None),
|
||||||
|
('rgba()', None),
|
||||||
|
('rgba(0)', None),
|
||||||
|
('rgba(0, 0, 0)', None),
|
||||||
|
('rgba(0, 0, 0, 0, 0)', None),
|
||||||
|
('rgba(0%)', None),
|
||||||
|
('rgba(0%, 0%)', None),
|
||||||
|
('rgba(0%, 0%, 0%)', None),
|
||||||
|
('rgba(0%, 0%, 0%, 0%)', None),
|
||||||
|
('rgba(0%, 0%, 0%, 0%, 0%)', None),
|
||||||
|
|
||||||
|
('HSL(0, 0%, 0%)', (0, 0, 0, 1)),
|
||||||
|
('hsL(0, 100%, 50%)', (1, 0, 0, 1)),
|
||||||
|
('hsl(60, 100%, 37.5%)', (0.75, 0.75, 0, 1)),
|
||||||
|
('hsl(780, 100%, 37.5%)', (0.75, 0.75, 0, 1)),
|
||||||
|
('hsl(-300, 100%, 37.5%)', (0.75, 0.75, 0, 1)),
|
||||||
|
('hsl(300, 50%, 50%)', (0.75, 0.25, 0.75, 1)),
|
||||||
|
|
||||||
|
('hsl(10, 50%, 0)', None),
|
||||||
|
('hsl(50%, 50%, 0%)', None),
|
||||||
|
('hsl(0, 0% 0%)', None),
|
||||||
|
('hsl(30deg, 100%, 100%)', None),
|
||||||
|
('hsl(0, 0%, light)', None),
|
||||||
|
('hsl()', None),
|
||||||
|
('hsl(0)', None),
|
||||||
|
('hsl(0, 0%)', None),
|
||||||
|
('hsl(0, 0%, 0%, 0%)', None),
|
||||||
|
|
||||||
|
('HSLA(-300, 100%, 37.5%, 1)', (0.75, 0.75, 0, 1)),
|
||||||
|
('hsLA(-300, 100%, 37.5%, 12)', (0.75, 0.75, 0, 1)),
|
||||||
|
('hsla(-300, 100%, 37.5%, 0.2)', (0.75, 0.75, 0, .2)),
|
||||||
|
('hsla(-300, 100%, 37.5%, 0)', (0.75, 0.75, 0, 0)),
|
||||||
|
('hsla(-300, 100%, 37.5%, -3)', (0.75, 0.75, 0, 0)),
|
||||||
|
|
||||||
|
('hsla(10, 50%, 0, 1)', None),
|
||||||
|
('hsla(50%, 50%, 0%, 1)', None),
|
||||||
|
('hsla(0, 0% 0%, 1)', None),
|
||||||
|
('hsla(30deg, 100%, 100%, 1)', None),
|
||||||
|
('hsla(0, 0%, light, 1)', None),
|
||||||
|
('hsla()', None),
|
||||||
|
('hsla(0)', None),
|
||||||
|
('hsla(0, 0%)', None),
|
||||||
|
('hsla(0, 0%, 0%, 50%)', None),
|
||||||
|
('hsla(0, 0%, 0%, 1, 0%)', None),
|
||||||
|
|
||||||
|
('cmyk(0, 0, 0, 0)', None),
|
||||||
|
])
|
||||||
|
def test_color(css_source, expected_result):
|
||||||
|
result = parse_color_string(css_source)
|
||||||
|
if isinstance(result, tuple):
|
||||||
|
for got, expected in zip(result, expected_result):
|
||||||
|
# Compensate for floating point errors:
|
||||||
|
assert abs(got - expected) < 1e-10
|
||||||
|
for i, attr in enumerate(['red', 'green', 'blue', 'alpha']):
|
||||||
|
assert getattr(result, attr) == result[i]
|
||||||
|
else:
|
||||||
|
assert result == expected_result
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(('hsl', 'expected_rgb'), [
|
||||||
|
# http://en.wikipedia.org/wiki/HSL_and_HSV#Examples
|
||||||
|
((0, 0, 100 ), (1, 1, 1 )), # noqa
|
||||||
|
((127, 0, 100 ), (1, 1, 1 )), # noqa
|
||||||
|
((0, 0, 50 ), (0.5, 0.5, 0.5 )), # noqa
|
||||||
|
((127, 0, 50 ), (0.5, 0.5, 0.5 )), # noqa
|
||||||
|
((0, 0, 0 ), (0, 0, 0 )), # noqa
|
||||||
|
((127, 0, 0 ), (0, 0, 0 )), # noqa
|
||||||
|
((0, 100, 50 ), (1, 0, 0 )), # noqa
|
||||||
|
((60, 100, 37.5), (0.75, 0.75, 0 )), # noqa
|
||||||
|
((780, 100, 37.5), (0.75, 0.75, 0 )), # noqa
|
||||||
|
((-300, 100, 37.5), (0.75, 0.75, 0 )), # noqa
|
||||||
|
((120, 100, 25 ), (0, 0.5, 0 )), # noqa
|
||||||
|
((180, 100, 75 ), (0.5, 1, 1 )), # noqa
|
||||||
|
((240, 100, 75 ), (0.5, 0.5, 1 )), # noqa
|
||||||
|
((300, 50, 50 ), (0.75, 0.25, 0.75 )), # noqa
|
||||||
|
((61.8, 63.8, 39.3), (0.628, 0.643, 0.142)), # noqa
|
||||||
|
((251.1, 83.2, 51.1), (0.255, 0.104, 0.918)), # noqa
|
||||||
|
((134.9, 70.7, 39.6), (0.116, 0.675, 0.255)), # noqa
|
||||||
|
((49.5, 89.3, 49.7), (0.941, 0.785, 0.053)), # noqa
|
||||||
|
((283.7, 77.5, 54.2), (0.704, 0.187, 0.897)), # noqa
|
||||||
|
((14.3, 81.7, 62.4), (0.931, 0.463, 0.316)), # noqa
|
||||||
|
((56.9, 99.1, 76.5), (0.998, 0.974, 0.532)), # noqa
|
||||||
|
((162.4, 77.9, 44.7), (0.099, 0.795, 0.591)), # noqa
|
||||||
|
((248.3, 60.1, 37.3), (0.211, 0.149, 0.597)), # noqa
|
||||||
|
((240.5, 29, 60.7), (0.495, 0.493, 0.721)), # noqa
|
||||||
|
])
|
||||||
|
def test_hsl(hsl, expected_rgb):
|
||||||
|
for got, expected in zip(hsl_to_rgb(*hsl), expected_rgb):
|
||||||
|
# Compensate for floating point errors and Wikipedia’s rounding:
|
||||||
|
assert abs(got - expected) < 0.001
|
|
@ -0,0 +1,353 @@
|
||||||
|
# coding: utf-8
|
||||||
|
"""
|
||||||
|
Tests for the CSS 2.1 parser
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
:copyright: (c) 2012 by Simon Sapin.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from tinycss.css21 import CSS21Parser
|
||||||
|
|
||||||
|
from . import assert_errors
|
||||||
|
from .test_tokenizer import jsonify
|
||||||
|
|
||||||
|
|
||||||
|
def parse_bytes(css_bytes, kwargs):
|
||||||
|
return CSS21Parser().parse_stylesheet_bytes(css_bytes, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_bytesio_file(css_bytes, kwargs):
|
||||||
|
css_file = io.BytesIO(css_bytes)
|
||||||
|
return CSS21Parser().parse_stylesheet_file(css_file, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_filename(css_bytes, kwargs):
|
||||||
|
css_file = tempfile.NamedTemporaryFile(delete=False)
|
||||||
|
try:
|
||||||
|
css_file.write(css_bytes)
|
||||||
|
# Windows can not open the filename a second time while
|
||||||
|
# it is still open for writing.
|
||||||
|
css_file.close()
|
||||||
|
return CSS21Parser().parse_stylesheet_file(css_file.name, **kwargs)
|
||||||
|
finally:
|
||||||
|
os.remove(css_file.name)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(('css_bytes', 'kwargs', 'expected_result', 'parse'), [
|
||||||
|
params + (parse,)
|
||||||
|
for parse in [parse_bytes, parse_bytesio_file, parse_filename]
|
||||||
|
for params in [
|
||||||
|
('@import "é";'.encode('utf8'), {}, 'é'),
|
||||||
|
('@import "é";'.encode('utf16'), {}, 'é'), # with a BOM
|
||||||
|
('@import "é";'.encode('latin1'), {}, 'é'),
|
||||||
|
('@import "£";'.encode('Shift-JIS'), {}, '\x81\x92'), # lat1 mojibake
|
||||||
|
('@charset "Shift-JIS";@import "£";'.encode('Shift-JIS'), {}, '£'),
|
||||||
|
(' @charset "Shift-JIS";@import "£";'.encode('Shift-JIS'), {},
|
||||||
|
'\x81\x92'),
|
||||||
|
('@import "£";'.encode('Shift-JIS'),
|
||||||
|
{'document_encoding': 'Shift-JIS'}, '£'),
|
||||||
|
('@import "£";'.encode('Shift-JIS'),
|
||||||
|
{'document_encoding': 'utf8'}, '\x81\x92'),
|
||||||
|
('@charset "utf8"; @import "£";'.encode('utf8'),
|
||||||
|
{'document_encoding': 'latin1'}, '£'),
|
||||||
|
# Mojibake yay!
|
||||||
|
(' @charset "utf8"; @import "é";'.encode('utf8'),
|
||||||
|
{'document_encoding': 'latin1'}, 'é'),
|
||||||
|
('@import "é";'.encode('utf8'), {'document_encoding': 'latin1'}, 'é'),
|
||||||
|
]
|
||||||
|
])
|
||||||
|
def test_bytes(css_bytes, kwargs, expected_result, parse):
|
||||||
|
stylesheet = parse(css_bytes, kwargs)
|
||||||
|
assert stylesheet.rules[0].at_keyword == '@import'
|
||||||
|
assert stylesheet.rules[0].uri == expected_result
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(('css_source', 'expected_rules', 'expected_errors'), [
|
||||||
|
(' /* hey */\n', 0, []),
|
||||||
|
('foo {}', 1, []),
|
||||||
|
('foo{} @lipsum{} bar{}', 2,
|
||||||
|
['unknown at-rule in stylesheet context: @lipsum']),
|
||||||
|
('@charset "ascii"; foo {}', 1, []),
|
||||||
|
(' @charset "ascii"; foo {}', 1, [
|
||||||
|
'mis-placed or malformed @charset rule']),
|
||||||
|
('@charset ascii; foo {}', 1, ['mis-placed or malformed @charset rule']),
|
||||||
|
('foo {} @charset "ascii";', 1, ['mis-placed or malformed @charset rule']),
|
||||||
|
])
|
||||||
|
def test_at_rules(css_source, expected_rules, expected_errors):
|
||||||
|
# Pass 'encoding' to allow @charset
|
||||||
|
stylesheet = CSS21Parser().parse_stylesheet(css_source, encoding='utf8')
|
||||||
|
assert_errors(stylesheet.errors, expected_errors)
|
||||||
|
result = len(stylesheet.rules)
|
||||||
|
assert result == expected_rules
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(('css_source', 'expected_rules', 'expected_errors'), [
|
||||||
|
(' /* hey */\n', [], []),
|
||||||
|
|
||||||
|
('foo{} /* hey */\n@bar;@baz{}',
|
||||||
|
[('foo', []), ('@bar', [], None), ('@baz', [], [])], []),
|
||||||
|
|
||||||
|
('@import "foo.css"/**/;', [
|
||||||
|
('@import', [('STRING', 'foo.css')], None)], []),
|
||||||
|
|
||||||
|
('@import "foo.css"/**/', [
|
||||||
|
('@import', [('STRING', 'foo.css')], None)], []),
|
||||||
|
|
||||||
|
('@import "foo.css', [
|
||||||
|
('@import', [('STRING', 'foo.css')], None)], []),
|
||||||
|
|
||||||
|
('{}', [], ['empty selector']),
|
||||||
|
|
||||||
|
('a{b:4}', [('a', [('b', [('INTEGER', 4)])])], []),
|
||||||
|
|
||||||
|
('@page {\t b: 4; @margin}', [('@page', [], [
|
||||||
|
('S', '\t '), ('IDENT', 'b'), (':', ':'), ('S', ' '), ('INTEGER', 4),
|
||||||
|
(';', ';'), ('S', ' '), ('ATKEYWORD', '@margin'),
|
||||||
|
])], []),
|
||||||
|
|
||||||
|
('foo', [], ['no declaration block found']),
|
||||||
|
|
||||||
|
('foo @page {} bar {}', [('bar', [])],
|
||||||
|
['unexpected ATKEYWORD token in selector']),
|
||||||
|
|
||||||
|
('foo { content: "unclosed string;\n color:red; ; margin/**/\n: 2cm; }',
|
||||||
|
[('foo', [('margin', [('DIMENSION', 2)])])],
|
||||||
|
['unexpected BAD_STRING token in property value']),
|
||||||
|
|
||||||
|
('foo { 4px; bar: 12% }',
|
||||||
|
[('foo', [('bar', [('PERCENTAGE', 12)])])],
|
||||||
|
['expected a property name, got DIMENSION']),
|
||||||
|
|
||||||
|
('foo { bar! 3cm auto ; baz: 7px }',
|
||||||
|
[('foo', [('baz', [('DIMENSION', 7)])])],
|
||||||
|
["expected ':', got DELIM"]),
|
||||||
|
|
||||||
|
('foo { bar ; baz: {("}"/* comment */) {0@fizz}} }',
|
||||||
|
[('foo', [('baz', [('{', [
|
||||||
|
('(', [('STRING', '}')]), ('S', ' '),
|
||||||
|
('{', [('INTEGER', 0), ('ATKEYWORD', '@fizz')])
|
||||||
|
])])])],
|
||||||
|
["expected ':'"]),
|
||||||
|
|
||||||
|
('foo { bar: ; baz: not(z) }',
|
||||||
|
[('foo', [('baz', [('FUNCTION', 'not', [('IDENT', 'z')])])])],
|
||||||
|
['expected a property value']),
|
||||||
|
|
||||||
|
('foo { bar: (]) ; baz: U+20 }',
|
||||||
|
[('foo', [('baz', [('UNICODE-RANGE', 'U+20')])])],
|
||||||
|
['unmatched ] token in (']),
|
||||||
|
])
|
||||||
|
def test_core_parser(css_source, expected_rules, expected_errors):
|
||||||
|
class CoreParser(CSS21Parser):
|
||||||
|
"""A parser that always accepts unparsed at-rules."""
|
||||||
|
def parse_at_rule(self, rule, stylesheet_rules, errors, context):
|
||||||
|
return rule
|
||||||
|
|
||||||
|
stylesheet = CoreParser().parse_stylesheet(css_source)
|
||||||
|
assert_errors(stylesheet.errors, expected_errors)
|
||||||
|
result = [
|
||||||
|
(rule.at_keyword, list(jsonify(rule.head)),
|
||||||
|
list(jsonify(rule.body))
|
||||||
|
if rule.body is not None else None)
|
||||||
|
if rule.at_keyword else
|
||||||
|
(rule.selector.as_css(), [
|
||||||
|
(decl.name, list(jsonify(decl.value)))
|
||||||
|
for decl in rule.declarations])
|
||||||
|
for rule in stylesheet.rules
|
||||||
|
]
|
||||||
|
assert result == expected_rules
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(('css_source', 'expected_declarations',
|
||||||
|
'expected_errors'), [
|
||||||
|
(' /* hey */\n', [], []),
|
||||||
|
|
||||||
|
('b:4', [('b', [('INTEGER', 4)])], []),
|
||||||
|
|
||||||
|
('{b:4}', [], ['expected a property name, got {']),
|
||||||
|
|
||||||
|
('b:4} c:3', [], ['unmatched } token in property value']),
|
||||||
|
|
||||||
|
(' 4px; bar: 12% ',
|
||||||
|
[('bar', [('PERCENTAGE', 12)])],
|
||||||
|
['expected a property name, got DIMENSION']),
|
||||||
|
|
||||||
|
('bar! 3cm auto ; baz: 7px',
|
||||||
|
[('baz', [('DIMENSION', 7)])],
|
||||||
|
["expected ':', got DELIM"]),
|
||||||
|
|
||||||
|
('foo; bar ; baz: {("}"/* comment */) {0@fizz}}',
|
||||||
|
[('baz', [('{', [
|
||||||
|
('(', [('STRING', '}')]), ('S', ' '),
|
||||||
|
('{', [('INTEGER', 0), ('ATKEYWORD', '@fizz')])
|
||||||
|
])])],
|
||||||
|
["expected ':'", "expected ':'"]),
|
||||||
|
|
||||||
|
('bar: ; baz: not(z)',
|
||||||
|
[('baz', [('FUNCTION', 'not', [('IDENT', 'z')])])],
|
||||||
|
['expected a property value']),
|
||||||
|
|
||||||
|
('bar: (]) ; baz: U+20',
|
||||||
|
[('baz', [('UNICODE-RANGE', 'U+20')])],
|
||||||
|
['unmatched ] token in (']),
|
||||||
|
])
|
||||||
|
def test_parse_style_attr(css_source, expected_declarations, expected_errors):
|
||||||
|
declarations, errors = CSS21Parser().parse_style_attr(css_source)
|
||||||
|
assert_errors(errors, expected_errors)
|
||||||
|
result = [(decl.name, list(jsonify(decl.value)))
|
||||||
|
for decl in declarations]
|
||||||
|
assert result == expected_declarations
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(('css_source', 'expected_declarations',
|
||||||
|
'expected_errors'), [
|
||||||
|
(' /* hey */\n', [], []),
|
||||||
|
|
||||||
|
('a:1; b:2',
|
||||||
|
[('a', [('INTEGER', 1)], None), ('b', [('INTEGER', 2)], None)], []),
|
||||||
|
|
||||||
|
('a:1 important; b: important',
|
||||||
|
[('a', [('INTEGER', 1), ('S', ' '), ('IDENT', 'important')], None),
|
||||||
|
('b', [('IDENT', 'important')], None)],
|
||||||
|
[]),
|
||||||
|
|
||||||
|
('a:1 !important; b:2',
|
||||||
|
[('a', [('INTEGER', 1)], 'important'), ('b', [('INTEGER', 2)], None)],
|
||||||
|
[]),
|
||||||
|
|
||||||
|
('a:1!\t Im\\50 O\\RTant; b:2',
|
||||||
|
[('a', [('INTEGER', 1)], 'important'), ('b', [('INTEGER', 2)], None)],
|
||||||
|
[]),
|
||||||
|
|
||||||
|
('a: !important; b:2',
|
||||||
|
[('b', [('INTEGER', 2)], None)],
|
||||||
|
['expected a value before !important']),
|
||||||
|
|
||||||
|
])
|
||||||
|
def test_important(css_source, expected_declarations, expected_errors):
|
||||||
|
declarations, errors = CSS21Parser().parse_style_attr(css_source)
|
||||||
|
assert_errors(errors, expected_errors)
|
||||||
|
result = [(decl.name, list(jsonify(decl.value)), decl.priority)
|
||||||
|
for decl in declarations]
|
||||||
|
assert result == expected_declarations
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(('css_source', 'expected_rules', 'expected_errors'), [
|
||||||
|
(' /* hey */\n', [], []),
|
||||||
|
('@import "foo.css";', [('foo.css', ['all'])], []),
|
||||||
|
('@import url(foo.css);', [('foo.css', ['all'])], []),
|
||||||
|
('@import "foo.css" screen, print;',
|
||||||
|
[('foo.css', ['screen', 'print'])], []),
|
||||||
|
('@charset "ascii"; @import "foo.css"; @import "bar.css";',
|
||||||
|
[('foo.css', ['all']), ('bar.css', ['all'])], []),
|
||||||
|
('foo {} @import "foo.css";',
|
||||||
|
[], ['@import rule not allowed after a ruleset']),
|
||||||
|
('@page {} @import "foo.css";',
|
||||||
|
[], ['@import rule not allowed after an @page rule']),
|
||||||
|
('@import ;',
|
||||||
|
[], ['expected URI or STRING for @import rule']),
|
||||||
|
('@import foo.css;',
|
||||||
|
[], ['expected URI or STRING for @import rule, got IDENT']),
|
||||||
|
('@import "foo.css" {}',
|
||||||
|
[], ["expected ';', got a block"]),
|
||||||
|
])
|
||||||
|
def test_at_import(css_source, expected_rules, expected_errors):
|
||||||
|
# Pass 'encoding' to allow @charset
|
||||||
|
stylesheet = CSS21Parser().parse_stylesheet(css_source, encoding='utf8')
|
||||||
|
assert_errors(stylesheet.errors, expected_errors)
|
||||||
|
|
||||||
|
result = [
|
||||||
|
(rule.uri, rule.media)
|
||||||
|
for rule in stylesheet.rules
|
||||||
|
if rule.at_keyword == '@import'
|
||||||
|
]
|
||||||
|
assert result == expected_rules
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(('css', 'expected_result', 'expected_errors'), [
|
||||||
|
('@page {}', (None, (0, 0), []), []),
|
||||||
|
('@page:first {}', ('first', (1, 0), []), []),
|
||||||
|
('@page :left{}', ('left', (0, 1), []), []),
|
||||||
|
('@page\t\n:right {}', ('right', (0, 1), []), []),
|
||||||
|
('@page :last {}', None, ['invalid @page selector']),
|
||||||
|
('@page : right {}', None, ['invalid @page selector']),
|
||||||
|
('@page table:left {}', None, ['invalid @page selector']),
|
||||||
|
|
||||||
|
('@page;', None, ['invalid @page rule: missing block']),
|
||||||
|
('@page { a:1; ; b: 2 }',
|
||||||
|
(None, (0, 0), [('a', [('INTEGER', 1)]), ('b', [('INTEGER', 2)])]),
|
||||||
|
[]),
|
||||||
|
('@page { a:1; c: ; b: 2 }',
|
||||||
|
(None, (0, 0), [('a', [('INTEGER', 1)]), ('b', [('INTEGER', 2)])]),
|
||||||
|
['expected a property value']),
|
||||||
|
('@page { a:1; @top-left {} b: 2 }',
|
||||||
|
(None, (0, 0), [('a', [('INTEGER', 1)]), ('b', [('INTEGER', 2)])]),
|
||||||
|
['unknown at-rule in @page context: @top-left']),
|
||||||
|
('@page { a:1; @top-left {}; b: 2 }',
|
||||||
|
(None, (0, 0), [('a', [('INTEGER', 1)]), ('b', [('INTEGER', 2)])]),
|
||||||
|
['unknown at-rule in @page context: @top-left']),
|
||||||
|
])
|
||||||
|
def test_at_page(css, expected_result, expected_errors):
|
||||||
|
stylesheet = CSS21Parser().parse_stylesheet(css)
|
||||||
|
assert_errors(stylesheet.errors, expected_errors)
|
||||||
|
|
||||||
|
if expected_result is None:
|
||||||
|
assert not stylesheet.rules
|
||||||
|
else:
|
||||||
|
assert len(stylesheet.rules) == 1
|
||||||
|
rule = stylesheet.rules[0]
|
||||||
|
assert rule.at_keyword == '@page'
|
||||||
|
assert rule.at_rules == [] # in CSS 2.1
|
||||||
|
result = (
|
||||||
|
rule.selector,
|
||||||
|
rule.specificity,
|
||||||
|
[(decl.name, list(jsonify(decl.value)))
|
||||||
|
for decl in rule.declarations],
|
||||||
|
)
|
||||||
|
assert result == expected_result
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(('css_source', 'expected_rules', 'expected_errors'), [
|
||||||
|
(' /* hey */\n', [], []),
|
||||||
|
('@media all {}', [(['all'], [])], []),
|
||||||
|
('@media screen, print {}', [(['screen', 'print'], [])], []),
|
||||||
|
('@media all;', [], ['invalid @media rule: missing block']),
|
||||||
|
('@media {}', [], ['expected media types for @media']),
|
||||||
|
('@media 4 {}', [], ['expected a media type, got INTEGER']),
|
||||||
|
('@media , screen {}', [], ['expected a media type']),
|
||||||
|
('@media screen, {}', [], ['expected a media type']),
|
||||||
|
('@media screen print {}', [],
|
||||||
|
['expected a media type, got IDENT, IDENT']),
|
||||||
|
|
||||||
|
('@media all { @page { a: 1 } @media; @import; foo { a: 1 } }',
|
||||||
|
[(['all'], [('foo', [('a', [('INTEGER', 1)])])])],
|
||||||
|
['@page rule not allowed in @media',
|
||||||
|
'@media rule not allowed in @media',
|
||||||
|
'@import rule not allowed in @media']),
|
||||||
|
|
||||||
|
])
|
||||||
|
def test_at_media(css_source, expected_rules, expected_errors):
|
||||||
|
stylesheet = CSS21Parser().parse_stylesheet(css_source)
|
||||||
|
assert_errors(stylesheet.errors, expected_errors)
|
||||||
|
|
||||||
|
for rule in stylesheet.rules:
|
||||||
|
assert rule.at_keyword == '@media'
|
||||||
|
result = [
|
||||||
|
(rule.media, [
|
||||||
|
(sub_rule.selector.as_css(), [
|
||||||
|
(decl.name, list(jsonify(decl.value)))
|
||||||
|
for decl in sub_rule.declarations])
|
||||||
|
for sub_rule in rule.rules
|
||||||
|
])
|
||||||
|
for rule in stylesheet.rules
|
||||||
|
]
|
||||||
|
assert result == expected_rules
|
|
@ -0,0 +1,79 @@
|
||||||
|
# coding: utf-8
|
||||||
|
"""
|
||||||
|
Tests for decoding bytes to Unicode
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
:copyright: (c) 2012 by Simon Sapin.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from tinycss.decoding import decode
|
||||||
|
|
||||||
|
|
||||||
|
def params(css, encoding, use_bom=False, expect_error=False, **kwargs):
|
||||||
|
"""Nicer syntax to make a tuple."""
|
||||||
|
return css, encoding, use_bom, expect_error, kwargs
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(('css', 'encoding', 'use_bom', 'expect_error',
|
||||||
|
'kwargs'), [
|
||||||
|
params('', 'utf8'), # default to utf8
|
||||||
|
params('𐂃', 'utf8'),
|
||||||
|
params('é', 'latin1'), # utf8 fails, fall back on ShiftJIS
|
||||||
|
params('£', 'ShiftJIS', expect_error=True),
|
||||||
|
params('£', 'ShiftJIS', protocol_encoding='Shift-JIS'),
|
||||||
|
params('£', 'ShiftJIS', linking_encoding='Shift-JIS'),
|
||||||
|
params('£', 'ShiftJIS', document_encoding='Shift-JIS'),
|
||||||
|
params('£', 'ShiftJIS', protocol_encoding='utf8',
|
||||||
|
document_encoding='ShiftJIS'),
|
||||||
|
params('@charset "utf8"; £', 'ShiftJIS', expect_error=True),
|
||||||
|
params('@charset "utf£8"; £', 'ShiftJIS', expect_error=True),
|
||||||
|
params('@charset "unknown-encoding"; £', 'ShiftJIS', expect_error=True),
|
||||||
|
params('@charset "utf8"; £', 'ShiftJIS', document_encoding='ShiftJIS'),
|
||||||
|
params('£', 'ShiftJIS', linking_encoding='utf8',
|
||||||
|
document_encoding='ShiftJIS'),
|
||||||
|
params('@charset "utf-32"; 𐂃', 'utf-32-be'),
|
||||||
|
params('@charset "Shift-JIS"; £', 'ShiftJIS'),
|
||||||
|
params('@charset "ISO-8859-8"; £', 'ShiftJIS', expect_error=True),
|
||||||
|
params('𐂃', 'utf-16-le', expect_error=True), # no BOM
|
||||||
|
params('𐂃', 'utf-16-le', use_bom=True),
|
||||||
|
params('𐂃', 'utf-32-be', expect_error=True),
|
||||||
|
params('𐂃', 'utf-32-be', use_bom=True),
|
||||||
|
params('𐂃', 'utf-32-be', document_encoding='utf-32-be'),
|
||||||
|
params('𐂃', 'utf-32-be', linking_encoding='utf-32-be'),
|
||||||
|
params('@charset "utf-32-le"; 𐂃', 'utf-32-be',
|
||||||
|
use_bom=True, expect_error=True),
|
||||||
|
# protocol_encoding takes precedence over @charset
|
||||||
|
params('@charset "ISO-8859-8"; £', 'ShiftJIS',
|
||||||
|
protocol_encoding='Shift-JIS'),
|
||||||
|
params('@charset "unknown-encoding"; £', 'ShiftJIS',
|
||||||
|
protocol_encoding='Shift-JIS'),
|
||||||
|
params('@charset "Shift-JIS"; £', 'ShiftJIS',
|
||||||
|
protocol_encoding='utf8'),
|
||||||
|
# @charset takes precedence over document_encoding
|
||||||
|
params('@charset "Shift-JIS"; £', 'ShiftJIS',
|
||||||
|
document_encoding='ISO-8859-8'),
|
||||||
|
# @charset takes precedence over linking_encoding
|
||||||
|
params('@charset "Shift-JIS"; £', 'ShiftJIS',
|
||||||
|
linking_encoding='ISO-8859-8'),
|
||||||
|
# linking_encoding takes precedence over document_encoding
|
||||||
|
params('£', 'ShiftJIS',
|
||||||
|
linking_encoding='Shift-JIS', document_encoding='ISO-8859-8'),
|
||||||
|
])
|
||||||
|
def test_decode(css, encoding, use_bom, expect_error, kwargs):
|
||||||
|
# Workaround PyPy and CPython 3.0 bug: https://bugs.pypy.org/issue1094
|
||||||
|
css = css.encode('utf16').decode('utf16')
|
||||||
|
if use_bom:
|
||||||
|
source = '\ufeff' + css
|
||||||
|
else:
|
||||||
|
source = css
|
||||||
|
css_bytes = source.encode(encoding)
|
||||||
|
result, result_encoding = decode(css_bytes, **kwargs)
|
||||||
|
if expect_error:
|
||||||
|
assert result != css, 'Unexpected unicode success'
|
||||||
|
else:
|
||||||
|
assert result == css, 'Unexpected unicode error'
|
|
@ -0,0 +1,101 @@
|
||||||
|
# coding: utf-8
|
||||||
|
"""
|
||||||
|
Tests for the Paged Media 3 parser
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
:copyright: (c) 2012 by Simon Sapin.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from tinycss.page3 import CSSPage3Parser
|
||||||
|
|
||||||
|
from . import assert_errors
|
||||||
|
from .test_tokenizer import jsonify
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(('css', 'expected_selector',
|
||||||
|
'expected_specificity', 'expected_errors'), [
|
||||||
|
('@page {}', (None, None), (0, 0, 0), []),
|
||||||
|
|
||||||
|
('@page :first {}', (None, 'first'), (0, 1, 0), []),
|
||||||
|
('@page:left{}', (None, 'left'), (0, 0, 1), []),
|
||||||
|
('@page :right {}', (None, 'right'), (0, 0, 1), []),
|
||||||
|
('@page :blank{}', (None, 'blank'), (0, 1, 0), []),
|
||||||
|
('@page :last {}', None, None, ['invalid @page selector']),
|
||||||
|
('@page : first {}', None, None, ['invalid @page selector']),
|
||||||
|
|
||||||
|
('@page foo:first {}', ('foo', 'first'), (1, 1, 0), []),
|
||||||
|
('@page bar :left {}', ('bar', 'left'), (1, 0, 1), []),
|
||||||
|
(r'@page \26:right {}', ('&', 'right'), (1, 0, 1), []),
|
||||||
|
|
||||||
|
('@page foo {}', ('foo', None), (1, 0, 0), []),
|
||||||
|
(r'@page \26 {}', ('&', None), (1, 0, 0), []),
|
||||||
|
|
||||||
|
('@page foo fist {}', None, None, ['invalid @page selector']),
|
||||||
|
('@page foo, bar {}', None, None, ['invalid @page selector']),
|
||||||
|
('@page foo&first {}', None, None, ['invalid @page selector']),
|
||||||
|
])
|
||||||
|
def test_selectors(css, expected_selector, expected_specificity,
|
||||||
|
expected_errors):
|
||||||
|
stylesheet = CSSPage3Parser().parse_stylesheet(css)
|
||||||
|
assert_errors(stylesheet.errors, expected_errors)
|
||||||
|
|
||||||
|
if stylesheet.rules:
|
||||||
|
assert len(stylesheet.rules) == 1
|
||||||
|
rule = stylesheet.rules[0]
|
||||||
|
assert rule.at_keyword == '@page'
|
||||||
|
selector = rule.selector
|
||||||
|
assert rule.specificity == expected_specificity
|
||||||
|
else:
|
||||||
|
selector = None
|
||||||
|
assert selector == expected_selector
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(('css', 'expected_declarations',
|
||||||
|
'expected_rules', 'expected_errors'), [
|
||||||
|
('@page {}', [], [], []),
|
||||||
|
('@page { foo: 4; bar: z }',
|
||||||
|
[('foo', [('INTEGER', 4)]), ('bar', [('IDENT', 'z')])], [], []),
|
||||||
|
('''@page { foo: 4;
|
||||||
|
@top-center { content: "Awesome Title" }
|
||||||
|
@bottom-left { content: counter(page) }
|
||||||
|
bar: z
|
||||||
|
}''',
|
||||||
|
[('foo', [('INTEGER', 4)]), ('bar', [('IDENT', 'z')])],
|
||||||
|
[('@top-center', [('content', [('STRING', 'Awesome Title')])]),
|
||||||
|
('@bottom-left', [('content', [
|
||||||
|
('FUNCTION', 'counter', [('IDENT', 'page')])])])],
|
||||||
|
[]),
|
||||||
|
('''@page { foo: 4;
|
||||||
|
@bottom-top { content: counter(page) }
|
||||||
|
bar: z
|
||||||
|
}''',
|
||||||
|
[('foo', [('INTEGER', 4)]), ('bar', [('IDENT', 'z')])],
|
||||||
|
[],
|
||||||
|
['unknown at-rule in @page context: @bottom-top']),
|
||||||
|
|
||||||
|
('@page{} @top-right{}', [], [], [
|
||||||
|
'@top-right rule not allowed in stylesheet']),
|
||||||
|
('@page{ @top-right 4 {} }', [], [], [
|
||||||
|
'unexpected INTEGER token in @top-right rule header']),
|
||||||
|
# Not much error recovery tests here. This should be covered in test_css21
|
||||||
|
])
|
||||||
|
def test_content(css, expected_declarations, expected_rules, expected_errors):
|
||||||
|
stylesheet = CSSPage3Parser().parse_stylesheet(css)
|
||||||
|
assert_errors(stylesheet.errors, expected_errors)
|
||||||
|
|
||||||
|
def declarations(rule):
|
||||||
|
return [(decl.name, list(jsonify(decl.value)))
|
||||||
|
for decl in rule.declarations]
|
||||||
|
|
||||||
|
assert len(stylesheet.rules) == 1
|
||||||
|
rule = stylesheet.rules[0]
|
||||||
|
assert rule.at_keyword == '@page'
|
||||||
|
assert declarations(rule) == expected_declarations
|
||||||
|
rules = [(margin_rule.at_keyword, declarations(margin_rule))
|
||||||
|
for margin_rule in rule.at_rules]
|
||||||
|
assert rules == expected_rules
|
|
@ -0,0 +1,452 @@
|
||||||
|
# coding: utf-8
|
||||||
|
"""
|
||||||
|
tinycss.token_data
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Shared data for both implementations (Cython and Python) of the tokenizer.
|
||||||
|
|
||||||
|
:copyright: (c) 2012 by Simon Sapin.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import operator
|
||||||
|
import re
|
||||||
|
import string
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# * Raw strings with the r'' notation are used so that \ do not need
|
||||||
|
# to be escaped.
|
||||||
|
# * Names and regexps are separated by a tabulation.
|
||||||
|
# * Macros are re-ordered so that only previous definitions are needed.
|
||||||
|
# * {} are used for macro substitution with ``string.Formatter``,
|
||||||
|
# so other uses of { or } have been doubled.
|
||||||
|
# * The syntax is otherwise compatible with re.compile.
|
||||||
|
# * Some parentheses were added to add capturing groups.
|
||||||
|
# (in unicode, DIMENSION and URI)
|
||||||
|
|
||||||
|
# *** Willful violation: ***
|
||||||
|
# Numbers can take a + or - sign, but the sign is a separate DELIM token.
|
||||||
|
# Since comments are allowed anywhere between tokens, this makes
|
||||||
|
# the following this is valid. It means 10 negative pixels:
|
||||||
|
# margin-top: -/**/10px
|
||||||
|
|
||||||
|
# This makes parsing numbers a pain, so instead we’ll do the same is Firefox
|
||||||
|
# and make the sign part as of the 'num' macro. The above CSS will be invalid.
|
||||||
|
# See discussion:
|
||||||
|
# http://lists.w3.org/Archives/Public/www-style/2011Oct/0028.html
|
||||||
|
MACROS = r'''
|
||||||
|
nl \n|\r\n|\r|\f
|
||||||
|
w [ \t\r\n\f]*
|
||||||
|
nonascii [^\0-\237]
|
||||||
|
unicode \\([0-9a-f]{{1,6}})(\r\n|[ \n\r\t\f])?
|
||||||
|
simple_escape [^\n\r\f0-9a-f]
|
||||||
|
escape {unicode}|\\{simple_escape}
|
||||||
|
nmstart [_a-z]|{nonascii}|{escape}
|
||||||
|
nmchar [_a-z0-9-]|{nonascii}|{escape}
|
||||||
|
name {nmchar}+
|
||||||
|
ident [-]?{nmstart}{nmchar}*
|
||||||
|
num [-+]?(?:[0-9]*\.[0-9]+|[0-9]+)
|
||||||
|
string1 \"([^\n\r\f\\"]|\\{nl}|{escape})*\"
|
||||||
|
string2 \'([^\n\r\f\\']|\\{nl}|{escape})*\'
|
||||||
|
string {string1}|{string2}
|
||||||
|
badstring1 \"([^\n\r\f\\"]|\\{nl}|{escape})*\\?
|
||||||
|
badstring2 \'([^\n\r\f\\']|\\{nl}|{escape})*\\?
|
||||||
|
badstring {badstring1}|{badstring2}
|
||||||
|
badcomment1 \/\*[^*]*\*+([^/*][^*]*\*+)*
|
||||||
|
badcomment2 \/\*[^*]*(\*+[^/*][^*]*)*
|
||||||
|
badcomment {badcomment1}|{badcomment2}
|
||||||
|
baduri1 url\({w}([!#$%&*-~]|{nonascii}|{escape})*{w}
|
||||||
|
baduri2 url\({w}{string}{w}
|
||||||
|
baduri3 url\({w}{badstring}
|
||||||
|
baduri {baduri1}|{baduri2}|{baduri3}
|
||||||
|
'''.replace(r'\0', '\0').replace(r'\237', '\237')
|
||||||
|
|
||||||
|
# Removed these tokens. Instead, they’re tokenized as two DELIM each.
|
||||||
|
# INCLUDES ~=
|
||||||
|
# DASHMATCH |=
|
||||||
|
# They are only used in selectors but selectors3 also have ^=, *= and $=.
|
||||||
|
# We don’t actually parse selectors anyway
|
||||||
|
|
||||||
|
# Re-ordered so that the longest match is always the first.
|
||||||
|
# For example, "url('foo')" matches URI, BAD_URI, FUNCTION and IDENT,
|
||||||
|
# but URI would always be a longer match than the others.
|
||||||
|
TOKENS = r'''
|
||||||
|
S [ \t\r\n\f]+
|
||||||
|
|
||||||
|
URI url\({w}({string}|([!#$%&*-\[\]-~]|{nonascii}|{escape})*){w}\)
|
||||||
|
BAD_URI {baduri}
|
||||||
|
FUNCTION {ident}\(
|
||||||
|
UNICODE-RANGE u\+[0-9a-f?]{{1,6}}(-[0-9a-f]{{1,6}})?
|
||||||
|
IDENT {ident}
|
||||||
|
|
||||||
|
ATKEYWORD @{ident}
|
||||||
|
HASH #{name}
|
||||||
|
|
||||||
|
DIMENSION ({num})({ident})
|
||||||
|
PERCENTAGE {num}%
|
||||||
|
NUMBER {num}
|
||||||
|
|
||||||
|
STRING {string}
|
||||||
|
BAD_STRING {badstring}
|
||||||
|
|
||||||
|
COMMENT \/\*[^*]*\*+([^/*][^*]*\*+)*\/
|
||||||
|
BAD_COMMENT {badcomment}
|
||||||
|
|
||||||
|
: :
|
||||||
|
; ;
|
||||||
|
{ \{{
|
||||||
|
} \}}
|
||||||
|
( \(
|
||||||
|
) \)
|
||||||
|
[ \[
|
||||||
|
] \]
|
||||||
|
CDO <!--
|
||||||
|
CDC -->
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
# Strings with {macro} expanded
|
||||||
|
COMPILED_MACROS = {}
|
||||||
|
|
||||||
|
|
||||||
|
COMPILED_TOKEN_REGEXPS = [] # [(name, regexp.match)] ordered
|
||||||
|
COMPILED_TOKEN_INDEXES = {} # {name: i} helper for the C speedups
|
||||||
|
|
||||||
|
|
||||||
|
# Indexed by codepoint value of the first character of a token.
|
||||||
|
# Codepoints >= 160 (aka nonascii) all use the index 160.
|
||||||
|
# values are (i, name, regexp.match)
|
||||||
|
TOKEN_DISPATCH = []
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
unichr
|
||||||
|
except NameError:
|
||||||
|
# Python 3
|
||||||
|
unichr = chr
|
||||||
|
unicode = str
|
||||||
|
|
||||||
|
|
||||||
|
def _init():
|
||||||
|
"""Import-time initialization."""
|
||||||
|
COMPILED_MACROS.clear()
|
||||||
|
for line in MACROS.splitlines():
|
||||||
|
if line.strip():
|
||||||
|
name, value = line.split('\t')
|
||||||
|
COMPILED_MACROS[name.strip()] = '(?:%s)' \
|
||||||
|
% value.format(**COMPILED_MACROS)
|
||||||
|
|
||||||
|
COMPILED_TOKEN_REGEXPS[:] = (
|
||||||
|
(
|
||||||
|
name.strip(),
|
||||||
|
re.compile(
|
||||||
|
value.format(**COMPILED_MACROS),
|
||||||
|
# Case-insensitive when matching eg. uRL(foo)
|
||||||
|
# but preserve the case in extracted groups
|
||||||
|
re.I
|
||||||
|
).match
|
||||||
|
)
|
||||||
|
for line in TOKENS.splitlines()
|
||||||
|
if line.strip()
|
||||||
|
for name, value in [line.split('\t')]
|
||||||
|
)
|
||||||
|
|
||||||
|
COMPILED_TOKEN_INDEXES.clear()
|
||||||
|
for i, (name, regexp) in enumerate(COMPILED_TOKEN_REGEXPS):
|
||||||
|
COMPILED_TOKEN_INDEXES[name] = i
|
||||||
|
|
||||||
|
dispatch = [[] for i in range(161)]
|
||||||
|
for chars, names in [
|
||||||
|
(' \t\r\n\f', ['S']),
|
||||||
|
('uU', ['URI', 'BAD_URI', 'UNICODE-RANGE']),
|
||||||
|
# \ is an escape outside of another token
|
||||||
|
(string.ascii_letters + '\\_-' + unichr(160), ['FUNCTION', 'IDENT']),
|
||||||
|
(string.digits + '.+-', ['DIMENSION', 'PERCENTAGE', 'NUMBER']),
|
||||||
|
('@', ['ATKEYWORD']),
|
||||||
|
('#', ['HASH']),
|
||||||
|
('\'"', ['STRING', 'BAD_STRING']),
|
||||||
|
('/', ['COMMENT', 'BAD_COMMENT']),
|
||||||
|
('<', ['CDO']),
|
||||||
|
('-', ['CDC']),
|
||||||
|
]:
|
||||||
|
for char in chars:
|
||||||
|
dispatch[ord(char)].extend(names)
|
||||||
|
for char in ':;{}()[]':
|
||||||
|
dispatch[ord(char)] = [char]
|
||||||
|
|
||||||
|
TOKEN_DISPATCH[:] = (
|
||||||
|
[
|
||||||
|
(index,) + COMPILED_TOKEN_REGEXPS[index]
|
||||||
|
for name in names
|
||||||
|
for index in [COMPILED_TOKEN_INDEXES[name]]
|
||||||
|
]
|
||||||
|
for names in dispatch
|
||||||
|
)
|
||||||
|
|
||||||
|
_init()
|
||||||
|
|
||||||
|
|
||||||
|
def _unicode_replace(match, int=int, unichr=unichr, maxunicode=sys.maxunicode):
|
||||||
|
codepoint = int(match.group(1), 16)
|
||||||
|
if codepoint <= maxunicode:
|
||||||
|
return unichr(codepoint)
|
||||||
|
else:
|
||||||
|
return '\N{REPLACEMENT CHARACTER}' # U+FFFD
|
||||||
|
|
||||||
|
UNICODE_UNESCAPE = functools.partial(
|
||||||
|
re.compile(COMPILED_MACROS['unicode'], re.I).sub,
|
||||||
|
_unicode_replace)
|
||||||
|
|
||||||
|
NEWLINE_UNESCAPE = functools.partial(
|
||||||
|
re.compile(r'()\\' + COMPILED_MACROS['nl']).sub,
|
||||||
|
'')
|
||||||
|
|
||||||
|
SIMPLE_UNESCAPE = functools.partial(
|
||||||
|
re.compile(r'\\(%s)' % COMPILED_MACROS['simple_escape'], re.I).sub,
|
||||||
|
# Same as r'\1', but faster on CPython
|
||||||
|
operator.methodcaller('group', 1))
|
||||||
|
|
||||||
|
FIND_NEWLINES = re.compile(COMPILED_MACROS['nl']).finditer
|
||||||
|
|
||||||
|
|
||||||
|
class Token(object):
|
||||||
|
"""A single atomic token.
|
||||||
|
|
||||||
|
.. attribute:: is_container
|
||||||
|
|
||||||
|
Always ``False``.
|
||||||
|
Helps to tell :class:`Token` apart from :class:`ContainerToken`.
|
||||||
|
|
||||||
|
.. attribute:: type
|
||||||
|
|
||||||
|
The type of token as a string:
|
||||||
|
|
||||||
|
``S``
|
||||||
|
A sequence of white space
|
||||||
|
|
||||||
|
``IDENT``
|
||||||
|
An identifier: a name that does not start with a digit.
|
||||||
|
A name is a sequence of letters, digits, ``_``, ``-``, escaped
|
||||||
|
characters and non-ASCII characters. Eg: ``margin-left``
|
||||||
|
|
||||||
|
``HASH``
|
||||||
|
``#`` followed immediately by a name. Eg: ``#ff8800``
|
||||||
|
|
||||||
|
``ATKEYWORD``
|
||||||
|
``@`` followed immediately by an identifier. Eg: ``@page``
|
||||||
|
|
||||||
|
``URI``
|
||||||
|
Eg: ``url(foo)`` The content may or may not be quoted.
|
||||||
|
|
||||||
|
``UNICODE-RANGE``
|
||||||
|
``U+`` followed by one or two hexadecimal
|
||||||
|
Unicode codepoints. Eg: ``U+20-00FF``
|
||||||
|
|
||||||
|
``INTEGER``
|
||||||
|
An integer with an optional ``+`` or ``-`` sign
|
||||||
|
|
||||||
|
``NUMBER``
|
||||||
|
A non-integer number with an optional ``+`` or ``-`` sign
|
||||||
|
|
||||||
|
``DIMENSION``
|
||||||
|
An integer or number followed immediately by an
|
||||||
|
identifier (the unit). Eg: ``12px``
|
||||||
|
|
||||||
|
``PERCENTAGE``
|
||||||
|
An integer or number followed immediately by ``%``
|
||||||
|
|
||||||
|
``STRING``
|
||||||
|
A string, quoted with ``"`` or ``'``
|
||||||
|
|
||||||
|
``:`` or ``;``
|
||||||
|
That character.
|
||||||
|
|
||||||
|
``DELIM``
|
||||||
|
A single character not matched in another token. Eg: ``,``
|
||||||
|
|
||||||
|
See the source of the :mod:`.token_data` module for the precise
|
||||||
|
regular expressions that match various tokens.
|
||||||
|
|
||||||
|
Note that other token types exist in the early tokenization steps,
|
||||||
|
but these are ignored, are syntax errors, or are later transformed
|
||||||
|
into :class:`ContainerToken` or :class:`FunctionToken`.
|
||||||
|
|
||||||
|
.. attribute:: value
|
||||||
|
|
||||||
|
The parsed value:
|
||||||
|
|
||||||
|
* INTEGER, NUMBER, PERCENTAGE or DIMENSION tokens: the numeric value
|
||||||
|
as an int or float.
|
||||||
|
* STRING tokens: the unescaped string without quotes
|
||||||
|
* URI tokens: the unescaped URI without quotes or
|
||||||
|
``url(`` and ``)`` markers.
|
||||||
|
* IDENT, ATKEYWORD or HASH tokens: the unescaped token,
|
||||||
|
with ``@`` or ``#`` markers left as-is
|
||||||
|
* Other tokens: same as :attr:`as_css`
|
||||||
|
|
||||||
|
*Unescaped* refers to the various escaping methods based on the
|
||||||
|
backslash ``\`` character in CSS syntax.
|
||||||
|
|
||||||
|
.. attribute:: unit
|
||||||
|
|
||||||
|
* DIMENSION tokens: the normalized (unescaped, lower-case)
|
||||||
|
unit name as a string. eg. ``'px'``
|
||||||
|
* PERCENTAGE tokens: the string ``'%'``
|
||||||
|
* Other tokens: ``None``
|
||||||
|
|
||||||
|
.. attribute:: line
|
||||||
|
|
||||||
|
The line number in the CSS source of the start of this token.
|
||||||
|
|
||||||
|
.. attribute:: column
|
||||||
|
|
||||||
|
The column number (inside a source line) of the start of this token.
|
||||||
|
|
||||||
|
"""
|
||||||
|
is_container = False
|
||||||
|
__slots__ = 'type', '_as_css', 'value', 'unit', 'line', 'column'
|
||||||
|
|
||||||
|
def __init__(self, type_, css_value, value, unit, line, column):
|
||||||
|
self.type = type_
|
||||||
|
self._as_css = css_value
|
||||||
|
self.value = value
|
||||||
|
self.unit = unit
|
||||||
|
self.line = line
|
||||||
|
self.column = column
|
||||||
|
|
||||||
|
def as_css(self):
|
||||||
|
"""
|
||||||
|
Return as an Unicode string the CSS representation of the token,
|
||||||
|
as parsed in the source.
|
||||||
|
"""
|
||||||
|
return self._as_css
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return ('<Token {0.type} at {0.line}:{0.column} {0.value!r}{1}>'
|
||||||
|
.format(self, self.unit or ''))
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if type(self) != type(other):
|
||||||
|
raise TypeError(
|
||||||
|
'Cannot compare {0} and {1}'.format(type(self), type(other)))
|
||||||
|
else:
|
||||||
|
return all(
|
||||||
|
self.type_ == other.type_,
|
||||||
|
self._as_css == other._as_css,
|
||||||
|
self.value == other.value,
|
||||||
|
self.unit == other.unit,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ContainerToken(object):
|
||||||
|
"""A token that contains other (nested) tokens.
|
||||||
|
|
||||||
|
.. attribute:: is_container
|
||||||
|
|
||||||
|
Always ``True``.
|
||||||
|
Helps to tell :class:`ContainerToken` apart from :class:`Token`.
|
||||||
|
|
||||||
|
.. attribute:: type
|
||||||
|
|
||||||
|
The type of token as a string. One of ``{``, ``(``, ``[`` or
|
||||||
|
``FUNCTION``. For ``FUNCTION``, the object is actually a
|
||||||
|
:class:`FunctionToken`.
|
||||||
|
|
||||||
|
.. attribute:: unit
|
||||||
|
|
||||||
|
Always ``None``. Included to make :class:`ContainerToken` behave
|
||||||
|
more like :class:`Token`.
|
||||||
|
|
||||||
|
.. attribute:: content
|
||||||
|
|
||||||
|
A list of :class:`Token` or nested :class:`ContainerToken`,
|
||||||
|
not including the opening or closing token.
|
||||||
|
|
||||||
|
.. attribute:: line
|
||||||
|
|
||||||
|
The line number in the CSS source of the start of this token.
|
||||||
|
|
||||||
|
.. attribute:: column
|
||||||
|
|
||||||
|
The column number (inside a source line) of the start of this token.
|
||||||
|
|
||||||
|
"""
|
||||||
|
is_container = True
|
||||||
|
unit = None
|
||||||
|
__slots__ = 'type', '_css_start', '_css_end', 'content', 'line', 'column'
|
||||||
|
|
||||||
|
def __init__(self, type_, css_start, css_end, content, line, column):
|
||||||
|
self.type = type_
|
||||||
|
self._css_start = css_start
|
||||||
|
self._css_end = css_end
|
||||||
|
self.content = content
|
||||||
|
self.line = line
|
||||||
|
self.column = column
|
||||||
|
|
||||||
|
def as_css(self):
|
||||||
|
"""
|
||||||
|
Return as an Unicode string the CSS representation of the token,
|
||||||
|
as parsed in the source.
|
||||||
|
"""
|
||||||
|
parts = [self._css_start]
|
||||||
|
parts.extend(token.as_css() for token in self.content)
|
||||||
|
parts.append(self._css_end)
|
||||||
|
return ''.join(parts)
|
||||||
|
|
||||||
|
format_string = '<ContainerToken {0.type} at {0.line}:{0.column}>'
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (self.format_string + ' {0.content}').format(self)
|
||||||
|
|
||||||
|
|
||||||
|
class FunctionToken(ContainerToken):
|
||||||
|
"""A specialized :class:`ContainerToken` for a ``FUNCTION`` group.
|
||||||
|
Has an additional attribute:
|
||||||
|
|
||||||
|
.. attribute:: function_name
|
||||||
|
|
||||||
|
The unescaped name of the function, with the ``(`` marker removed.
|
||||||
|
|
||||||
|
"""
|
||||||
|
__slots__ = 'function_name',
|
||||||
|
|
||||||
|
def __init__(self, type_, css_start, css_end, function_name, content,
|
||||||
|
line, column):
|
||||||
|
super(FunctionToken, self).__init__(
|
||||||
|
type_, css_start, css_end, content, line, column)
|
||||||
|
# Remove the ( marker:
|
||||||
|
self.function_name = function_name[:-1]
|
||||||
|
|
||||||
|
format_string = ('<FunctionToken {0.function_name}() at '
|
||||||
|
'{0.line}:{0.column}>')
|
||||||
|
|
||||||
|
|
||||||
|
class TokenList(list):
|
||||||
|
"""
|
||||||
|
A mixed list of :class:`~.token_data.Token` and
|
||||||
|
:class:`~.token_data.ContainerToken` objects.
|
||||||
|
|
||||||
|
This is a subclass of the builtin :class:`~builtins.list` type.
|
||||||
|
It can be iterated, indexed and sliced as usual, but also has some
|
||||||
|
additional API:
|
||||||
|
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def line(self):
|
||||||
|
"""The line number in the CSS source of the first token."""
|
||||||
|
return self[0].line
|
||||||
|
|
||||||
|
@property
|
||||||
|
def column(self):
|
||||||
|
"""The column number (inside a source line) of the first token."""
|
||||||
|
return self[0].column
|
||||||
|
|
||||||
|
def as_css(self):
|
||||||
|
"""
|
||||||
|
Return as an Unicode string the CSS representation of the tokens,
|
||||||
|
as parsed in the source.
|
||||||
|
"""
|
||||||
|
return ''.join(token.as_css() for token in self)
|
|
@ -0,0 +1,215 @@
|
||||||
|
# coding: utf-8
|
||||||
|
"""
|
||||||
|
tinycss.tokenizer
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Tokenizer for the CSS core syntax:
|
||||||
|
http://www.w3.org/TR/CSS21/syndata.html#tokenization
|
||||||
|
|
||||||
|
This is the pure-python implementation. See also speedups.pyx
|
||||||
|
|
||||||
|
:copyright: (c) 2012 by Simon Sapin.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from . import token_data
|
||||||
|
|
||||||
|
|
||||||
|
def tokenize_flat(
|
||||||
|
css_source, ignore_comments=True,
|
||||||
|
# Make these local variable to avoid global lookups in the loop
|
||||||
|
tokens_dispatch=token_data.TOKEN_DISPATCH,
|
||||||
|
unicode_unescape=token_data.UNICODE_UNESCAPE,
|
||||||
|
newline_unescape=token_data.NEWLINE_UNESCAPE,
|
||||||
|
simple_unescape=token_data.SIMPLE_UNESCAPE,
|
||||||
|
find_newlines=token_data.FIND_NEWLINES,
|
||||||
|
Token=token_data.Token,
|
||||||
|
len=len,
|
||||||
|
int=int,
|
||||||
|
float=float,
|
||||||
|
list=list,
|
||||||
|
_None=None):
|
||||||
|
"""
|
||||||
|
:param css_source:
|
||||||
|
CSS as an unicode string
|
||||||
|
:param ignore_comments:
|
||||||
|
if true (the default) comments will not be included in the
|
||||||
|
return value
|
||||||
|
:return:
|
||||||
|
An iterator of :class:`Token`
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
pos = 0
|
||||||
|
line = 1
|
||||||
|
column = 1
|
||||||
|
source_len = len(css_source)
|
||||||
|
tokens = []
|
||||||
|
while pos < source_len:
|
||||||
|
char = css_source[pos]
|
||||||
|
if char in ':;{}()[]':
|
||||||
|
type_ = char
|
||||||
|
css_value = char
|
||||||
|
else:
|
||||||
|
codepoint = min(ord(char), 160)
|
||||||
|
for _index, type_, regexp in tokens_dispatch[codepoint]:
|
||||||
|
match = regexp(css_source, pos)
|
||||||
|
if match:
|
||||||
|
# First match is the longest. See comments on TOKENS above.
|
||||||
|
css_value = match.group()
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# No match.
|
||||||
|
# "Any other character not matched by the above rules,
|
||||||
|
# and neither a single nor a double quote."
|
||||||
|
# ... but quotes at the start of a token are always matched
|
||||||
|
# by STRING or BAD_STRING. So DELIM is any single character.
|
||||||
|
type_ = 'DELIM'
|
||||||
|
css_value = char
|
||||||
|
length = len(css_value)
|
||||||
|
next_pos = pos + length
|
||||||
|
|
||||||
|
# A BAD_COMMENT is a comment at EOF. Ignore it too.
|
||||||
|
if not (ignore_comments and type_ in ('COMMENT', 'BAD_COMMENT')):
|
||||||
|
# Parse numbers, extract strings and URIs, unescape
|
||||||
|
unit = _None
|
||||||
|
if type_ == 'DIMENSION':
|
||||||
|
value = match.group(1)
|
||||||
|
value = float(value) if '.' in value else int(value)
|
||||||
|
unit = match.group(2)
|
||||||
|
unit = simple_unescape(unit)
|
||||||
|
unit = unicode_unescape(unit)
|
||||||
|
unit = unit.lower() # normalize
|
||||||
|
elif type_ == 'PERCENTAGE':
|
||||||
|
value = css_value[:-1]
|
||||||
|
value = float(value) if '.' in value else int(value)
|
||||||
|
unit = '%'
|
||||||
|
elif type_ == 'NUMBER':
|
||||||
|
value = css_value
|
||||||
|
if '.' in value:
|
||||||
|
value = float(value)
|
||||||
|
else:
|
||||||
|
value = int(value)
|
||||||
|
type_ = 'INTEGER'
|
||||||
|
elif type_ in ('IDENT', 'ATKEYWORD', 'HASH', 'FUNCTION'):
|
||||||
|
value = simple_unescape(css_value)
|
||||||
|
value = unicode_unescape(value)
|
||||||
|
elif type_ == 'URI':
|
||||||
|
value = match.group(1)
|
||||||
|
if value and value[0] in '"\'':
|
||||||
|
value = value[1:-1] # Remove quotes
|
||||||
|
value = newline_unescape(value)
|
||||||
|
value = simple_unescape(value)
|
||||||
|
value = unicode_unescape(value)
|
||||||
|
elif type_ == 'STRING':
|
||||||
|
value = css_value[1:-1] # Remove quotes
|
||||||
|
value = newline_unescape(value)
|
||||||
|
value = simple_unescape(value)
|
||||||
|
value = unicode_unescape(value)
|
||||||
|
# BAD_STRING can only be one of:
|
||||||
|
# * Unclosed string at the end of the stylesheet:
|
||||||
|
# Close the string, but this is not an error.
|
||||||
|
# Make it a "good" STRING token.
|
||||||
|
# * Unclosed string at the (unescaped) end of the line:
|
||||||
|
# Close the string, but this is an error.
|
||||||
|
# Leave it as a BAD_STRING, don’t bother parsing it.
|
||||||
|
# See http://www.w3.org/TR/CSS21/syndata.html#parsing-errors
|
||||||
|
elif type_ == 'BAD_STRING' and next_pos == source_len:
|
||||||
|
type_ = 'STRING'
|
||||||
|
value = css_value[1:] # Remove quote
|
||||||
|
value = newline_unescape(value)
|
||||||
|
value = simple_unescape(value)
|
||||||
|
value = unicode_unescape(value)
|
||||||
|
else:
|
||||||
|
value = css_value
|
||||||
|
tokens.append(Token(type_, css_value, value, unit, line, column))
|
||||||
|
|
||||||
|
pos = next_pos
|
||||||
|
newlines = list(find_newlines(css_value))
|
||||||
|
if newlines:
|
||||||
|
line += len(newlines)
|
||||||
|
# Add 1 to have lines start at column 1, not 0
|
||||||
|
column = length - newlines[-1].end() + 1
|
||||||
|
else:
|
||||||
|
column += length
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
|
||||||
|
def regroup(tokens):
|
||||||
|
"""
|
||||||
|
Match pairs of tokens: () [] {} function()
|
||||||
|
(Strings in "" or '' are taken care of by the tokenizer.)
|
||||||
|
|
||||||
|
Opening tokens are replaced by a :class:`ContainerToken`.
|
||||||
|
Closing tokens are removed. Unmatched closing tokens are invalid
|
||||||
|
but left as-is. All nested structures that are still open at
|
||||||
|
the end of the stylesheet are implicitly closed.
|
||||||
|
|
||||||
|
:param tokens:
|
||||||
|
a *flat* iterable of tokens, as returned by :func:`tokenize_flat`.
|
||||||
|
:return:
|
||||||
|
A tree of tokens.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# "global" objects for the inner recursion
|
||||||
|
pairs = {'FUNCTION': ')', '(': ')', '[': ']', '{': '}'}
|
||||||
|
tokens = iter(tokens)
|
||||||
|
eof = [False]
|
||||||
|
|
||||||
|
def _regroup_inner(stop_at=None, tokens=tokens, pairs=pairs, eof=eof,
|
||||||
|
ContainerToken=token_data.ContainerToken,
|
||||||
|
FunctionToken=token_data.FunctionToken):
|
||||||
|
for token in tokens:
|
||||||
|
type_ = token.type
|
||||||
|
if type_ == stop_at:
|
||||||
|
return
|
||||||
|
|
||||||
|
end = pairs.get(type_)
|
||||||
|
if end is None:
|
||||||
|
yield token # Not a grouping token
|
||||||
|
else:
|
||||||
|
assert not isinstance(token, ContainerToken), (
|
||||||
|
'Token looks already grouped: {0}'.format(token))
|
||||||
|
content = list(_regroup_inner(end))
|
||||||
|
if eof[0]:
|
||||||
|
end = '' # Implicit end of structure at EOF.
|
||||||
|
if type_ == 'FUNCTION':
|
||||||
|
yield FunctionToken(token.type, token.as_css(), end,
|
||||||
|
token.value, content,
|
||||||
|
token.line, token.column)
|
||||||
|
else:
|
||||||
|
yield ContainerToken(token.type, token.as_css(), end,
|
||||||
|
content,
|
||||||
|
token.line, token.column)
|
||||||
|
else:
|
||||||
|
eof[0] = True # end of file/stylesheet
|
||||||
|
return _regroup_inner()
|
||||||
|
|
||||||
|
|
||||||
|
def tokenize_grouped(css_source, ignore_comments=True):
|
||||||
|
"""
|
||||||
|
:param css_source:
|
||||||
|
CSS as an unicode string
|
||||||
|
:param ignore_comments:
|
||||||
|
if true (the default) comments will not be included in the
|
||||||
|
return value
|
||||||
|
:return:
|
||||||
|
An iterator of :class:`Token`
|
||||||
|
|
||||||
|
"""
|
||||||
|
return regroup(tokenize_flat(css_source, ignore_comments))
|
||||||
|
|
||||||
|
|
||||||
|
# Optional Cython version of tokenize_flat
|
||||||
|
# Make both versions available with explicit names for tests.
|
||||||
|
python_tokenize_flat = tokenize_flat
|
||||||
|
try:
|
||||||
|
from . import speedups
|
||||||
|
except ImportError:
|
||||||
|
cython_tokenize_flat = None
|
||||||
|
else:
|
||||||
|
cython_tokenize_flat = speedups.tokenize_flat
|
||||||
|
# Default to the Cython version if available
|
||||||
|
tokenize_flat = cython_tokenize_flat
|
|
@ -6,5 +6,5 @@
|
||||||
"tetra"
|
"tetra"
|
||||||
],
|
],
|
||||||
"title": "Portal Auth",
|
"title": "Portal Auth",
|
||||||
"version": "1.9"
|
"version": "2.0"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue