0
0
mirror of https://github.com/ezyang/htmlpurifier.git synced 2024-09-19 10:45:18 +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:
Edward Z. Yang 2008-05-20 01:19:00 +00:00
parent 32a6afa27c
commit 16fa73afa0
6 changed files with 332 additions and 2 deletions

4
NEWS
View File

@ -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

View 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;
}
}

View 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);
}
}

View File

@ -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);
}
}

View 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
}
}

View 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');
}
}