mirror of
https://github.com/ezyang/htmlpurifier.git
synced 2025-01-03 05:11:52 +00:00
[3.1.1] Added HTMLPurifier_UnitConverter and HTMLPurifier_Length for convenient handling of CSS-style lengths.
- Fixed another de-underscoring in the SimpleTest library git-svn-id: http://htmlpurifier.org/svnroot/htmlpurifier/trunk@1746 48356398-32a2-884e-a903-53898d9a118a
This commit is contained in:
parent
32a6afa27c
commit
16fa73afa0
4
NEWS
4
NEWS
@ -9,7 +9,9 @@ NEWS ( CHANGELOG and HISTORY ) HTMLPurifier
|
||||
. Internal change
|
||||
==========================
|
||||
|
||||
3.2.0, unknown release date
|
||||
3.1.1, unknown release date
|
||||
. Added HTMLPurifier_UnitConverter and HTMLPurifier_Length for convenient
|
||||
handling of CSS-style lengths.
|
||||
|
||||
3.1.0, released 2008-05-18
|
||||
# Unnecessary references to objects (vestiges of PHP4) removed from method
|
||||
|
73
library/HTMLPurifier/Length.php
Normal file
73
library/HTMLPurifier/Length.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Represents a measurable length, with a string numeric magnitude
|
||||
* and a unit.
|
||||
*/
|
||||
class HTMLPurifier_Length
|
||||
{
|
||||
|
||||
/**
|
||||
* String numeric magnitude.
|
||||
*/
|
||||
public $n;
|
||||
|
||||
/**
|
||||
* String unit. False is permitted if $n = 0.
|
||||
*/
|
||||
public $unit;
|
||||
|
||||
/**
|
||||
* Lookup array of units recognized by CSS 2.1
|
||||
*/
|
||||
protected static $allowedUnits = array(
|
||||
'em' => true, 'ex' => true, 'px' => true, 'in' => true,
|
||||
'cm' => true, 'mm' => true, 'pt' => true, 'pc' => true
|
||||
);
|
||||
|
||||
/**
|
||||
* @param number $n Magnitude
|
||||
* @param string $u Unit
|
||||
*/
|
||||
public function __construct($n = '0', $u = false) {
|
||||
$this->n = $n;
|
||||
$this->unit = $u;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $s Unit string, like '2em' or '3.4in'
|
||||
* @warning Does not perform validation.
|
||||
*/
|
||||
static public function make($s) {
|
||||
$n_length = strspn($s, '1234567890.+-');
|
||||
$n = substr($s, 0, $n_length);
|
||||
$unit = substr($s, $n_length);
|
||||
if ($unit === '') $unit = false;
|
||||
return new HTMLPurifier_Length($n, $unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the number and unit.
|
||||
* @param bool $non_negative Whether or not to disable negative values.
|
||||
* @note Maybe should be put in another class.
|
||||
*/
|
||||
public function validate($non_negative = false, $config, $context) {
|
||||
// Special case:
|
||||
if ($this->n === '0' && $this->unit === false) return true;
|
||||
if (!ctype_lower($this->unit)) $this->unit = strtolower($this->unit);
|
||||
if (!isset(HTMLPurifier_Length::$allowedUnits[$this->unit])) return false;
|
||||
$def = new HTMLPurifier_AttrDef_CSS_Number($non_negative);
|
||||
$result = $def->validate($this->n, $config, $context);
|
||||
if ($result === false) return false;
|
||||
$this->n = $result;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns string representation of number.
|
||||
*/
|
||||
public function toString() {
|
||||
return $this->n . $this->unit;
|
||||
}
|
||||
|
||||
}
|
164
library/HTMLPurifier/UnitConverter.php
Normal file
164
library/HTMLPurifier/UnitConverter.php
Normal file
@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Class for converting between different unit-lengths as specified by
|
||||
* CSS.
|
||||
*/
|
||||
class HTMLPurifier_UnitConverter
|
||||
{
|
||||
|
||||
const ENGLISH = 1;
|
||||
const METRIC = 2;
|
||||
|
||||
/**
|
||||
* Units information array. Units are grouped into measuring systems
|
||||
* (English, Metric), and are assigned an integer representing
|
||||
* the conversion factor between that unit and the smallest unit in
|
||||
* the system. Numeric indexes are actually magical constants that
|
||||
* encode conversion data from one system to the next, with a O(n^2)
|
||||
* constraint on memory (this is generally not a problem, since
|
||||
* the number of measuring systems is small.)
|
||||
*/
|
||||
protected static $units = array(
|
||||
self::ENGLISH => array(
|
||||
'pt' => 1,
|
||||
'pc' => 12,
|
||||
'in' => 72,
|
||||
self::METRIC => array('pt', '0.352777778', 'mm'),
|
||||
),
|
||||
self::METRIC => array(
|
||||
'mm' => 1,
|
||||
'cm' => 10,
|
||||
self::ENGLISH => array('mm', '2.83464567', 'pt'),
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Minimum bcmath precision for output.
|
||||
*/
|
||||
protected $outputPrecision;
|
||||
|
||||
/**
|
||||
* Bcmath precision for internal calculations.
|
||||
*/
|
||||
protected $internalPrecision;
|
||||
|
||||
public function __construct($output_precision = 4, $internal_precision = 10) {
|
||||
$this->outputPrecision = $output_precision;
|
||||
$this->internalPrecision = $internal_precision;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a length object of one unit into another unit.
|
||||
* @note
|
||||
* About precision: This conversion function pays very special
|
||||
* attention to the incoming precision of values and attempts
|
||||
* to maintain a number of significant figure. Results are
|
||||
* fairly accurate up to nine digits. Some caveats:
|
||||
* - If a number is zero-padded as a result of this significant
|
||||
* figure tracking, the zeroes will be eliminated.
|
||||
* - If a number contains less than four sigfigs ($outputPrecision)
|
||||
* and this causes some decimals to be excluded, those
|
||||
* decimals will be added on.
|
||||
* - Significant digits will be ignored for quantities greater
|
||||
* than one. This is a limitation of BCMath and I don't
|
||||
* feel like coding around it.
|
||||
*/
|
||||
public function convert($length, $to_unit) {
|
||||
if ($length->n === '0' || $length->unit === false) {
|
||||
return new HTMLPurifier_Length('0', $unit);
|
||||
}
|
||||
|
||||
$state = $dest = false;
|
||||
foreach (self::$units as $k => $x) {
|
||||
if (isset($x[$length->unit])) $state = $k;
|
||||
if (isset($x[$to_unit])) $dest_state = $k;
|
||||
}
|
||||
if (!$state || !$dest_state) return false;
|
||||
|
||||
$n = $length->n;
|
||||
$unit = $length->unit;
|
||||
|
||||
// Some calculations about the initial precision of the number;
|
||||
// this will be useful when we need to do final rounding.
|
||||
$log = (int) floor(log($n, 10));
|
||||
if (strpos($n, '.') === false) {
|
||||
$sigfigs = strlen(trim($n, '0+-'));
|
||||
} else {
|
||||
$sigfigs = strlen(ltrim($n, '0+-')) - 1; // eliminate extra decimal character
|
||||
}
|
||||
if ($sigfigs < $this->outputPrecision) $sigfigs = $this->outputPrecision;
|
||||
|
||||
// BCMath's internal precision deals only with decimals. Use
|
||||
// our default if the initial number has no decimals, or increase
|
||||
// it by how ever many decimals, thus, the number of guard digits
|
||||
// will always be greater than or equal to internalPrecision.
|
||||
$cp = ($log < 0) ? $this->internalPrecision - $log : $this->internalPrecision; // internal precision
|
||||
|
||||
for ($i = 0; $i < 2; $i++) {
|
||||
|
||||
// Determine what unit IN THIS SYSTEM we need to convert to
|
||||
if ($dest_state === $state) {
|
||||
// Simple conversion
|
||||
$dest_unit = $to_unit;
|
||||
} else {
|
||||
// Convert to the smallest unit, pending a system shift
|
||||
$dest_unit = self::$units[$state][$dest_state][0];
|
||||
}
|
||||
|
||||
// Do the conversion if necessary
|
||||
if ($dest_unit !== $unit) {
|
||||
$factor = bcdiv(self::$units[$state][$unit], self::$units[$state][$dest_unit], $cp);
|
||||
$n = bcmul($n, $factor, $cp);
|
||||
$unit = $dest_unit;
|
||||
}
|
||||
|
||||
// Output was zero, so bail out early
|
||||
if ($n === '') {
|
||||
$n = '0';
|
||||
$unit = $to_unit;
|
||||
break;
|
||||
}
|
||||
|
||||
// It was a simple conversion, so bail out
|
||||
if ($dest_state === $state) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ($i !== 0) {
|
||||
// Conversion failed! Apparently, the system we forwarded
|
||||
// to didn't have this unit. This should never happen!
|
||||
return false;
|
||||
}
|
||||
|
||||
// Pre-condition: $i == 0
|
||||
|
||||
// Perform conversion to next system of units
|
||||
$n = bcmul($n, self::$units[$state][$dest_state][1], $cp);
|
||||
$unit = self::$units[$state][$dest_state][2];
|
||||
$state = $dest_state;
|
||||
|
||||
// One more loop around to convert the unit in the new system.
|
||||
|
||||
}
|
||||
|
||||
// Post-condition: $unit == $to_unit
|
||||
if ($unit !== $to_unit) return false;
|
||||
|
||||
// Calculate how many decimals we need ($rp)
|
||||
// Calculations will always be carried to the decimal; this is
|
||||
// a limitation with BC (we can't set the scale to be negative)
|
||||
$new_log = (int) floor(log($n, 10));
|
||||
|
||||
$rp = $sigfigs - $new_log - $log - 1;
|
||||
if ($rp < 0) $rp = 0;
|
||||
|
||||
$n = bcadd($n, '0.' . str_repeat('0', $rp) . '5', $rp + 1);
|
||||
$n = bcdiv($n, '1', $rp);
|
||||
if (strpos($n, '.') !== false) $n = rtrim($n, '0');
|
||||
$n = rtrim($n, '.');
|
||||
|
||||
return new HTMLPurifier_Length($n, $unit);
|
||||
}
|
||||
|
||||
}
|
@ -76,7 +76,7 @@ class HTMLPurifier_Harness extends UnitTestCase
|
||||
// __onlytest makes only one test get triggered
|
||||
foreach (get_class_methods(get_class($this)) as $method) {
|
||||
if (strtolower(substr($method, 0, 10)) == '__onlytest') {
|
||||
$this->_reporter->paintSkip('All test methods in ' . $this->_label . ' besides ' . $method);
|
||||
$this->reporter->paintSkip('All test methods besides ' . $method);
|
||||
return array($method);
|
||||
}
|
||||
}
|
||||
|
49
tests/HTMLPurifier/LengthTest.php
Normal file
49
tests/HTMLPurifier/LengthTest.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
class HTMLPurifier_LengthTest extends HTMLPurifier_Harness
|
||||
{
|
||||
|
||||
function testConstruct() {
|
||||
$l = new HTMLPurifier_Length('23', 'in');
|
||||
$this->assertIdentical($l->n, '23');
|
||||
$this->assertIdentical($l->unit, 'in');
|
||||
}
|
||||
|
||||
function testMake() {
|
||||
$l = HTMLPurifier_Length::make('+23.4in');
|
||||
$this->assertIdentical($l->n, '+23.4');
|
||||
$this->assertIdentical($l->unit, 'in');
|
||||
}
|
||||
|
||||
function testToString() {
|
||||
$l = new HTMLPurifier_Length('23', 'in');
|
||||
$this->assertIdentical($l->toString(), '23in');
|
||||
}
|
||||
|
||||
protected function assertValidate($string, $expect = true, $disable_negative = false) {
|
||||
if ($expect === true) $expect = $string;
|
||||
$l = HTMLPurifier_Length::make($string);
|
||||
$result = $l->validate($disable_negative, $this->config, $this->context);
|
||||
if ($result === false) $this->assertIdentical($expect, false);
|
||||
else $this->assertIdentical($l->toString(), $expect);
|
||||
}
|
||||
|
||||
function testValidate() {
|
||||
$this->assertValidate('0');
|
||||
$this->assertValidate('0px');
|
||||
$this->assertValidate('4.5px');
|
||||
$this->assertValidate('-4.5px');
|
||||
$this->assertValidate('3ex');
|
||||
$this->assertValidate('3em');
|
||||
$this->assertValidate('3in');
|
||||
$this->assertValidate('3cm');
|
||||
$this->assertValidate('3mm');
|
||||
$this->assertValidate('3pt');
|
||||
$this->assertValidate('3pc');
|
||||
$this->assertValidate('3PX', '3px');
|
||||
$this->assertValidate('3', false);
|
||||
$this->assertValidate('3miles', false);
|
||||
$this->assertValidate('-3mm', false, true); // no-negatives
|
||||
}
|
||||
|
||||
}
|
42
tests/HTMLPurifier/UnitConverterTest.php
Normal file
42
tests/HTMLPurifier/UnitConverterTest.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
class HTMLPurifier_UnitConverterTest extends HTMLPurifier_Harness
|
||||
{
|
||||
|
||||
protected function assertConversion($input, $expect) {
|
||||
$input = HTMLPurifier_Length::make($input);
|
||||
$expect = HTMLPurifier_Length::make($expect);
|
||||
$converter = new HTMLPurifier_UnitConverter();
|
||||
$result = $converter->convert($input, $expect->unit);
|
||||
$this->assertIdentical($result, $expect);
|
||||
}
|
||||
|
||||
function testEnglish() {
|
||||
$this->assertConversion('1in', '6pc');
|
||||
$this->assertConversion('6pc', '1in');
|
||||
|
||||
$this->assertConversion('1in', '72pt');
|
||||
$this->assertConversion('72pt', '1in');
|
||||
|
||||
$this->assertConversion('1pc', '12pt');
|
||||
$this->assertConversion('12pt', '1pc');
|
||||
|
||||
$this->assertConversion('1pt', '0.01389in');
|
||||
$this->assertConversion('1.000pt', '0.01389in');
|
||||
$this->assertConversion('100000pt', '1389in');
|
||||
}
|
||||
|
||||
function testMetric() {
|
||||
$this->assertConversion('1cm', '10mm');
|
||||
$this->assertConversion('10mm', '1cm');
|
||||
$this->assertConversion('1mm', '0.1cm');
|
||||
$this->assertConversion('100mm', '10cm');
|
||||
}
|
||||
|
||||
function testEnglishMetric() {
|
||||
$this->assertConversion('2.835pt', '1mm');
|
||||
$this->assertConversion('1mm', '2.835pt');
|
||||
$this->assertConversion('0.3937in', '1cm');
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user