<?php
// CSFileDiff
//
// FIXME Proper copyright for Horde here!
//
include('Text/Diff.php');
include('Text/Diff/Renderer.php');
include('Text/Diff/Renderer/unified.php');
class CSFileDiff
{
private $differences;
public function &getDifferences() { return $this->differences; }
public function __construct(CSConfig &$oldFile, CSConfig &$newFile)
{
//
// Build a new native Text_Diff object, feeding in contents from
// the old configuration and the new configuration record.
//
$differ = new Text_Diff('native', array(
explode("\n", $oldFile->GetData()),
explode("\n", $newFile->GetData())
));
//
// Now we need to render this diff output into an unified diff, so
// that our diff parser can read it!
//
$renderer = new Text_Diff_Renderer_unified();
$unidiff = explode("\n", $renderer->render($differ));
//
// Now parse the unified diff.
//
$this->ParseUnifiedDiff($unidiff);
}
public function __destruct() { }
private function ParseUnifiedDiff(Array $unidiff)
{
$this->differences = array();
$cols = array( array(), array() ); // Hold the left and right columns of lines for change blocks
$state = 'empty';
foreach($unidiff as $line)
{
// Look for a header which indicates the start of a diff chunk
if (preg_match('/^@@ \-([0-9]+).*\+([0-9]+).*@@(.*)/', $line, $regs))
{
// Push any previous header info to the return stack
if (isset($data))
$this->differences[] = $data;
$data = array(
'type' => 'header',
'oldline' => $regs[1],
'newline' => $regs[2],
'contents' > array()
);
$data['function'] = isset($regs[3]) ? $regs[3] : '';
$state = 'dump';
}
elseif ($state != 'empty')
{
// We are in a chunk, so split out the action (+/-) and the line
preg_match('/^([\+\- ])(.*)/', $line, $regs);
if (count($regs) > 2)
{
$action = $regs[1];
$content = $regs[2];
}
else
{
$action = ' ';
$content = '';
}
if ($action == '+')
{
// This is just an additional line
if ($state == 'dump' || $state == 'add')
{
// Start adding to the addition stack
$cols[0][] = $content;
$state = 'add';
}
else
{
// This is inside a change block, so
// start to accumulate lines
$cols[1][] = $content;
$state = 'change';
}
}
elseif ($action == '-')
{
// This is a removal line
$cols[0][] = $content;
$state = 'remove';
}
else
{
// An empty block with no action...
switch ($state)
{
case 'add':
$data['contents'][] = array(
'type' => 'add',
'lines' => $cols[0]
);
break;
case 'remove':
// We have some removal lines pending
// in our stack, so flush them
$data['contents'][] = array(
'type' => 'remove',
'lines' => $cols[0]
);
break;
case 'change':
// We have both remove and addition lines,
// so this is a change block!
$data['contents'][] = array(
'type' => 'change',
'old' => $cols[0],
'new' => $cols[1]
);
break;
}
$cols = array( array(), array() );
$data['contents'][] = array(
'type' => 'empty',
'line' => $content
);
$state = 'dump';
}
}
}
// Just flush any remaining entries in the columns stack...
switch ($state)
{
case 'add':
$data['contents'][] = array(
'type' => 'add',
'lines' => $cols[0]
);
break;
case 'remove':
// We have some removal lines pending
// in our stack, so flush them
$data['contents'][] = array(
'type' => 'remove',
'lines' => $cols[0]
);
break;
case 'change':
// We have both remove and addition lines,
// so this is a change block!
$data['contents'][] = array(
'type' => 'change',
'old' => $cols[0],
'new' => $cols[1]
);
break;
}
if (isset($data))
$this->differences[] = $data;
}
public function FilesEqual()
{
return (count($this->differences) == 0);
}
private function code($s)
{
if (strlen($s) == 0)
return ' ';
return '<pre>'.$s.'</pre>';
}
public function InsertDifference(Array $h)
{
$currContext = '';
$line = '';
foreach ($h['contents'] as $change)
{
if (!empty($currContext) && $change['type'] != 'empty')
{
$line = $currContext;
$currContext = '';
echo '<tr class="Unchg"><td>'.$this->code($line).'</td>';
echo '<td>'.$this->code($line).'</td></tr>';
}
switch ($change['type'])
{
case 'add':
$line = '';
foreach($change['lines'] as $l)
$line .= htmlspecialchars($l).'<br />';
echo '<tr><td class="AddEmpty"> </td><td class="Add">';
echo $this->code($line).'</td></tr>';
break;
case 'remove':
$line = '';
foreach($change['lines'] as $l)
$line .= htmlspecialchars($l).'<br />';
echo '<tr><td class="Rem">'.$this->code($line).'</td>';
echo '<td class="RemEmpty"> </td></tr>';
break;
case 'empty':
$currContext .= htmlspecialchars($change['line']).'<br />';
break;
case 'change':
// Pop the old/new stacks one by one until empty
$oldSize = count($change['old']);
$newSize = count($change['new']);
$rowMax = max($oldSize, $newSize);
$l = $r = '';
for ($row = 0; $row < $rowMax; $row++)
{
$l .= isset($change['old'][$row]) ? htmlspecialchars($change['old'][$row]) : '';
$l .= '<br />';
$r .= isset($change['new'][$row]) ? htmlspecialchars($change['new'][$row]) : '';
$r .= '<br />';
}
if (!empty($l))
echo '<tr><td class="Chg">'.$this->code($l).'</td>';
elseif ($row < $oldSize)
echo '<tr><td class="Chg"> </td>';
else
echo '<tr><td class="Unchg"> </td>';
if (!empty($r))
echo '<td class="Chg">'.$this->code($r).'</td></tr>';
elseif ($row < $newSize)
echo '<td class="Chg"> </td></tr>';
else
echo '<td class="Unchg"> </td></tr>';
break;
}
}
if (!empty($currContext))
{
$line = $currContext;
$currContext = '';
echo '<tr class="Unchg"><td>'.$this->code($line).'</td>';
echo '<td>'.$this->code($line).'</td></tr>';
}
}
}
?>