diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 13:23:16 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 13:23:16 +0000 |
commit | 3e97c51418e6d27e9a81906f347fcb7c78e27d4f (patch) | |
tree | ee596ce1bc9840661386f96f9b8d1f919a106317 /vendor/gipfl/web/src | |
parent | Initial commit. (diff) | |
download | icingaweb2-module-incubator-3e97c51418e6d27e9a81906f347fcb7c78e27d4f.tar.xz icingaweb2-module-incubator-3e97c51418e6d27e9a81906f347fcb7c78e27d4f.zip |
Adding upstream version 0.20.0.upstream/0.20.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/gipfl/web/src')
24 files changed, 3007 insertions, 0 deletions
diff --git a/vendor/gipfl/web/src/Form.php b/vendor/gipfl/web/src/Form.php new file mode 100644 index 0000000..e5e52f9 --- /dev/null +++ b/vendor/gipfl/web/src/Form.php @@ -0,0 +1,281 @@ +<?php + +namespace gipfl\Web; + +use Exception; +use gipfl\Web\Form\Decorator\DdDtDecorator; +use gipfl\Web\Form\Validator\AlwaysFailValidator; +use gipfl\Web\Form\Validator\PhpSessionBasedCsrfTokenValidator; +use gipfl\Web\Widget\Hint; +use ipl\Html\BaseHtmlElement; +use ipl\Html\Contract\FormElement; +use ipl\Html\Error; +use ipl\Html\Form as iplForm; +use ipl\Html\FormElement\BaseFormElement; +use ipl\Html\FormElement\HiddenElement; +use ipl\Html\Html; +use RuntimeException; +use function array_key_exists; +use function get_class; +use function parse_str; + +class Form extends iplForm +{ + protected $formNameElementName = '__FORM_NAME'; + + protected $useCsrf = true; + + protected $useFormName = true; + + protected $defaultDecoratorClass = DdDtDecorator::class; + + protected $formCssClasses = ['gipfl-form']; + + /** @var boolean|null */ + protected $hasBeenSubmitted; + + public function ensureAssembled() + { + if ($this->hasBeenAssembled === false) { + if ($this->getRequest() === null) { + throw new RuntimeException('Cannot assemble a WebForm without a Request'); + } + $this->registerGipflElementLoader(); + $this->setupStyling(); + parent::ensureAssembled(); + $this->prepareWebForm(); + } + + return $this; + } + + protected function registerGipflElementLoader() + { + $this->addElementLoader(__NAMESPACE__ . '\\Form\\Element'); + } + + public function setSubmitted($submitted = true) + { + $this->hasBeenSubmitted = (bool) $submitted; + + return $this; + } + + public function hasBeenSubmitted() + { + if ($this->hasBeenSubmitted === null) { + return parent::hasBeenSubmitted(); + } else { + return $this->hasBeenSubmitted; + } + } + + public function disableCsrf() + { + $this->useCsrf = false; + + return $this; + } + + public function doNotCheckFormName() + { + $this->useFormName = false; + + return $this; + } + + protected function prepareWebForm() + { + if ($this->hasElement($this->formNameElementName)) { + return; // Called twice + } + if ($this->useFormName) { + $this->addFormNameElement(); + } + if ($this->useCsrf && $this->getMethod() === 'POST') { + $this->addCsrfElement(); + } + } + + protected function getUniqueFormName() + { + return get_class($this); + } + + protected function addFormNameElement() + { + $element = new HiddenElement($this->formNameElementName, [ + 'value' => $this->getUniqueFormName(), + 'ignore' => true, + ]); + $this->prepend($element); + $this->registerElement($element); + } + + public function addHidden($name, $value = null, $attributes = []) + { + if (is_array($value) && empty($attributes)) { + $attributes = $value; + $value = null; + } elseif ($value === null && is_scalar($attributes)) { + $value = $attributes; + $attributes = []; + } + if ($value !== null) { + $attributes['value'] = $value; + } + $element = new HiddenElement($name, $attributes); + $this->prepend($element); + $this->registerElement($element); + } + + public function registerElement(FormElement $element) + { + $idPrefix = ''; + if ($element instanceof BaseHtmlElement) { + if (! $element->getAttributes()->has('id')) { + $element->addAttributes(['id' => $idPrefix . $element->getName()]); + } + } + + return parent::registerElement($element); + } + + public function setElementValue($element, $value) + { + $this->wantFormElement($element)->setValue($value); + } + + public function getElementValue($elementName, $defaultValue = null) + { + $value = $this->getElement($elementName)->getValue(); + if ($value === null) { + return $defaultValue; + } else { + return $value; + } + } + + public function hasElementValue($elementName) + { + if ($this->hasElement($elementName)) { + return $this->getElement($elementName)->hasValue(); + } else { + return false; + } + } + + /** + * @param $element + * @return FormElement + */ + protected function wantFormElement($element) + { + if ($element instanceof BaseFormElement) { + return $element; + } else { + return $this->getElement($element); + } + } + + public function triggerElementError($element, $message, ...$params) + { + if (! empty($params)) { + $message = Html::sprintf($message, $params); + } + + $element = $this->wantFormElement($this->getElement($element)); + $element->addValidators([ + new AlwaysFailValidator(['message' => $message]) + ]); + } + + protected function setupStyling() + { + $this->setSeparator("\n"); + $this->addAttributes(['class' => $this->formCssClasses]); + if ($this->defaultDecoratorClass !== null) { + $this->setDefaultElementDecorator(new $this->defaultDecoratorClass); + } + } + + protected function addCsrfElement() + { + $element = new HiddenElement('__CSRF__', [ + 'ignore' => true, + ]); + $element->setValidators([ + new PhpSessionBasedCsrfTokenValidator() + ]); + // prepend / register -> avoid decorator + $this->prepend($element); + $this->registerElement($element); + if ($this->hasBeenSent()) { + if (! $element->isValid()) { + $element->setValue(PhpSessionBasedCsrfTokenValidator::generateCsrfValue()); + } + } else { + $element->setValue(PhpSessionBasedCsrfTokenValidator::generateCsrfValue()); + } + } + + public function getSentValue($name, $default = null) + { + $params = $this->getSentValues(); + + if (array_key_exists($name, $params)) { + return $params[$name]; + } else { + return $default; + } + } + + public function getSentValues() + { + $request = $this->getRequest(); + if ($request === null) { + throw new RuntimeException( + "It's impossible to access SENT values with no request" + ); + } + + if ($request->getMethod() === 'POST') { + $params = $request->getParsedBody(); + } elseif ($this->getMethod() === 'GET') { + parse_str($request->getUri()->getQuery(), $params); + } else { + $params = []; + } + + return $params; + } + + protected function onError() + { + $messages = $this->getMessages(); + if (empty($messages)) { + return; + } + $errors = []; + foreach ($this->getMessages() as $message) { + if ($message instanceof Exception) { + $this->prepend(Error::show($message)); + } else { + $errors[] = $message; + } + } + if (! empty($errors)) { + $this->prepend(Hint::error(implode(', ', $errors))); + } + } + + public function hasBeenSent() + { + if (parent::hasBeenSent()) { + return !$this->useFormName || $this->getSentValue($this->formNameElementName) + === $this->getUniqueFormName(); + } else { + return false; + } + } +} diff --git a/vendor/gipfl/web/src/Form/Decorator/DdDtDecorator.php b/vendor/gipfl/web/src/Form/Decorator/DdDtDecorator.php new file mode 100644 index 0000000..e5deae4 --- /dev/null +++ b/vendor/gipfl/web/src/Form/Decorator/DdDtDecorator.php @@ -0,0 +1,158 @@ +<?php + +namespace gipfl\Web\Form\Decorator; + +use gipfl\Web\HtmlHelper; +use ipl\Html\BaseHtmlElement; +use ipl\Html\FormDecorator\DecoratorInterface; +use ipl\Html\FormElement\BaseFormElement; +use ipl\Html\Html; +use ipl\Html\HtmlDocument; + +class DdDtDecorator extends BaseHtmlElement implements DecoratorInterface +{ + const CSS_CLASS_ELEMENT_HAS_ERRORS = 'gipfl-form-element-has-errors'; + + const CSS_CLASS_ELEMENT_ERRORS = 'gipfl-form-element-errors'; + + const CSS_CLASS_DESCRIPTION = 'gipfl-element-description'; + + protected $tag = 'dl'; + + protected $dt; + + protected $dd; + + /** @var BaseFormElement */ + protected $element; + + /** @var HtmlDocument */ + protected $elementDoc; + + /** + * @param BaseFormElement $element + * @return static + */ + public function decorate(BaseFormElement $element) + { + $decorator = clone($this); + $decorator->element = $element; + $decorator->elementDoc = new HtmlDocument(); + $decorator->elementDoc->add($element); + // if (! $element instanceof HiddenElement) { + $element->prependWrapper($decorator); + + return $decorator; + } + + protected function prepareLabel() + { + $element = $this->element; + $label = $element->getLabel(); + if ($label === null || \strlen($label) === 0) { + return null; + } + + // Set HTML element.id to element name unless defined + if ($element->getAttributes()->has('id')) { + $attributes = ['for' => $element->getAttributes()->get('id')->getValue()]; + } else { + $attributes = null; + } + + if ($element->isRequired()) { + $label = [$label, Html::tag('span', ['aria-hidden' => 'true'], '*')]; + } + + return Html::tag('label', $attributes, $label); + } + + public function getAttributes() + { + $attributes = parent::getAttributes(); + + // TODO: only when sent?! + if ($this->element->hasBeenValidated() && ! $this->element->isValid()) { + HtmlHelper::addClassOnce($attributes, static::CSS_CLASS_ELEMENT_HAS_ERRORS); + } + + return $attributes; + } + + protected function prepareDescription() + { + if ($this->element) { + $description = $this->element->getDescription(); + if ($description !== null && \strlen($description)) { + return Html::tag('p', ['class' => static::CSS_CLASS_DESCRIPTION], $description); + } + } + + return null; + } + + protected function prepareErrors() + { + $errors = []; + foreach ($this->element->getMessages() as $message) { + $errors[] = Html::tag('li', $message); + } + + if (empty($errors)) { + return null; + } else { + return Html::tag('ul', ['class' => static::CSS_CLASS_ELEMENT_ERRORS], $errors); + } + } + + public function add($content) + { + // Our wrapper implementation automatically adds the wrapped element but + // we already do so in assemble() + if ($content !== $this->element) { + parent::add($content); + } + + return $this; + } + + protected function assemble() + { + $this->add([$this->dt(), $this->dd()]); + } + + public function getElementDocument() + { + return $this->elementDoc; + } + + public function dt() + { + if ($this->dt === null) { + $this->dt = Html::tag('dt', null, $this->prepareLabel()); + } + + return $this->dt; + } + + /** + * @return \ipl\Html\HtmlElement + */ + public function dd() + { + if ($this->dd === null) { + $this->dd = Html::tag('dd', null, [ + $this->getElementDocument(), + $this->prepareErrors(), + $this->prepareDescription() + ]); + } + + return $this->dd; + } + + public function __destruct() + { + $this->wrapper = null; + } +} diff --git a/vendor/gipfl/web/src/Form/Element/Boolean.php b/vendor/gipfl/web/src/Form/Element/Boolean.php new file mode 100644 index 0000000..dc5f85f --- /dev/null +++ b/vendor/gipfl/web/src/Form/Element/Boolean.php @@ -0,0 +1,39 @@ +<?php + +namespace gipfl\Web\Form\Element; + +use gipfl\Translation\TranslationHelper; +use ipl\Html\FormElement\SelectElement; + +class Boolean extends SelectElement +{ + use TranslationHelper; + + public function __construct($name, $attributes = null) + { + parent::__construct($name, $attributes); + $options = [ + 'y' => $this->translate('Yes'), + 'n' => $this->translate('No'), + ]; + if (! $this->isRequired()) { + $options = [ + null => $this->translate('- please choose -'), + ] + $options; + } + + $this->setOptions($options); + } + + public function setValue($value) + { + if ($value === 'y' || $value === true) { + return parent::setValue('y'); + } elseif ($value === 'n' || $value === false) { + return parent::setValue('n'); + } + + // Hint: this will fail + return parent::setValue($value); + } +} diff --git a/vendor/gipfl/web/src/Form/Element/MultiSelect.php b/vendor/gipfl/web/src/Form/Element/MultiSelect.php new file mode 100644 index 0000000..07e2e9e --- /dev/null +++ b/vendor/gipfl/web/src/Form/Element/MultiSelect.php @@ -0,0 +1,119 @@ +<?php + +namespace gipfl\Web\Form\Element; + +use ipl\Html\Attributes; +use ipl\Html\FormElement\SelectElement; + +class MultiSelect extends SelectElement +{ + protected $value = []; + + public function __construct($name, $attributes = null) + { + // Make sure we set value last as it depends on options + if (isset($attributes['value'])) { + $value = $attributes['value']; + unset($attributes['value']); + $attributes['value'] = $value; + } + + parent::__construct($name, $attributes); + + $this->getAttributes()->add('multiple', true); + } + + protected function registerValueCallback(Attributes $attributes) + { + $attributes->registerAttributeCallback( + 'value', + null, + [$this, 'setValue'] + ); + } + + public function getNameAttribute() + { + return $this->getName() . '[]'; + } + + public function setValue($value) + { + if (empty($value)) { // null, '', [] + $values = []; + } else { + $values = (array) $value; + } + $invalid = []; + foreach ($values as $val) { + if ($option = $this->getOption($val)) { + if ($option->getAttributes()->has('disabled')) { + $invalid[] = $val; + } + } else { + $invalid[] = $val; + } + } + if (count($invalid) > 0) { + $this->failForValues($invalid); + return $this; + } + + $this->value = $values; + $this->valid = null; + $this->updateSelection(); + + return $this; + } + + protected function failForValues($values) + { + $this->valid = false; + if (count($values) === 1) { + $value = array_shift($values); + $this->addMessage("'$value' is not allowed here"); + } else { + $valueString = implode("', '", $values); + $this->addMessage("'$valueString' are not allowed here"); + } + } + + public function validate() + { + /** + * @TODO(lippserd): {@link SelectElement::validate()} doesn't work here because isset checks fail with + * illegal offset type errors since our value is an array. It would make sense to decouple the classes to + * avoid having to copy code from the base class. + * Also note that {@see setValue()} already performs most of the validation. + */ + if ($this->isRequired() && empty($this->getValue())) { + $this->valid = false; + } else { + /** + * Copied from {@link \ipl\Html\BaseHtmlElement::validate()}. + */ + $this->valid = $this->getValidators()->isValid($this->getValue()); + $this->addMessages($this->getValidators()->getMessages()); + } + } + + public function updateSelection() + { + foreach ($this->options as $value => $option) { + if (in_array($value, $this->value)) { + $option->getAttributes()->add('selected', true); + } else { + $option->getAttributes()->remove('selected'); + } + } + + return $this; + } + + protected function assemble() + { + foreach ($this->options as $option) { + $this->add($option); + } + } +} diff --git a/vendor/gipfl/web/src/Form/Element/Password.php b/vendor/gipfl/web/src/Form/Element/Password.php new file mode 100644 index 0000000..b6f148e --- /dev/null +++ b/vendor/gipfl/web/src/Form/Element/Password.php @@ -0,0 +1,11 @@ +<?php + +namespace gipfl\Web\Form\Element; + +use ipl\Html\FormElement\TextElement; + +class Password extends TextElement +{ + // TODO + protected $type = 'password'; +} diff --git a/vendor/gipfl/web/src/Form/Element/TextWithActionButton.php b/vendor/gipfl/web/src/Form/Element/TextWithActionButton.php new file mode 100644 index 0000000..13ebfb8 --- /dev/null +++ b/vendor/gipfl/web/src/Form/Element/TextWithActionButton.php @@ -0,0 +1,104 @@ +<?php + +namespace gipfl\Web\Form\Element; + +use gipfl\Web\Form\Decorator\DdDtDecorator; +use ipl\Html\Attributes; +use ipl\Html\Form; +use ipl\Html\FormElement\SubmitElement; +use ipl\Html\FormElement\TextElement; + +class TextWithActionButton +{ + /** @var SubmitElement */ + protected $button; + + /** @var TextElement */ + protected $element; + + protected $buttonSuffix = '_related_button'; + + /** @var string */ + protected $elementName; + + /** @var array|Attributes */ + protected $elementAttributes; + + /** @var array|Attributes */ + protected $buttonAttributes; + + protected $elementClasses = ['input-with-button']; + + protected $buttonClasses = ['input-element-related-button']; + + /** + * TextWithActionButton constructor. + * @param string $elementName + * @param array|Attributes $elementAttributes + * @param array|Attributes $buttonAttributes + */ + public function __construct($elementName, $elementAttributes, $buttonAttributes) + { + $this->elementName = $elementName; + $this->elementAttributes = $elementAttributes; + $this->buttonAttributes = $buttonAttributes; + } + + public function addToForm(Form $form) + { + $button = $this->getButton(); + $form->registerElement($button); + $element = $this->getElement(); + $form->addElement($element); + /** @var DdDtDecorator $deco */ + $deco = $element->getWrapper(); + if ($deco instanceof DdDtDecorator) { + $deco->addAttributes(['position' => 'relative'])->getElementDocument()->add($button); + } + } + + public function getElement() + { + if ($this->element === null) { + $this->element = $this->createTextElement( + $this->elementName, + $this->elementAttributes + ); + } + + return $this->element; + } + + public function getButton() + { + if ($this->button === null) { + $this->button = $this->createSubmitElement( + $this->elementName . $this->buttonSuffix, + $this->buttonAttributes + ); + } + + return $this->button; + } + + protected function createTextElement($name, $attributes = null) + { + $element = new TextElement($name, $attributes); + $element->addAttributes([ + 'class' => $this->elementClasses, + ]); + + return $element; + } + + protected function createSubmitElement($name, $attributes = null) + { + $element = new SubmitElement($name, $attributes); + $element->addAttributes([ + 'formnovalidate' => true, + 'class' => $this->buttonClasses, + ]); + + return $element; + } +} diff --git a/vendor/gipfl/web/src/Form/Feature/NextConfirmCancel.php b/vendor/gipfl/web/src/Form/Feature/NextConfirmCancel.php new file mode 100644 index 0000000..81885d6 --- /dev/null +++ b/vendor/gipfl/web/src/Form/Feature/NextConfirmCancel.php @@ -0,0 +1,153 @@ +<?php + +namespace gipfl\Web\Form\Feature; + +use gipfl\Web\Form; +use ipl\Html\DeferredText; +use ipl\Html\FormElement\BaseFormElement; +use ipl\Html\FormElement\SubmitElement; +use ipl\Html\HtmlDocument; +use ipl\Html\ValidHtml; + +class NextConfirmCancel +{ + /** @var SubmitElement */ + protected $next; + + /** @var SubmitElement */ + protected $confirm; + + /** @var SubmitElement */ + protected $cancel; + + protected $withNext; + + protected $withNextContent; + + protected $withConfirm; + + protected $withConfirmContent; + + protected $confirmFirst = true; + + public function __construct(SubmitElement $next, SubmitElement $confirm, SubmitElement $cancel) + { + $this->next = $next; + $this->confirm = $confirm; + $this->cancel = $cancel; + $this->withNextContent = new HtmlDocument(); + $this->withNext = new DeferredText(function () { + return $this->withNextContent; + }); + $this->withNext->setEscaped(); + + $this->withConfirmContent = new HtmlDocument(); + $this->withConfirm = new DeferredText(function () { + return $this->withConfirmContent; + }); + $this->withConfirm->setEscaped(); + } + + public function showWithNext($content) + { + $this->withNextContent->add($content); + } + + public function showWithConfirm($content) + { + $this->withConfirmContent->add($content); + } + + /** + * @param ValidHtml $html + * @param array $found Internal parameter + * @return BaseFormElement[] + */ + protected function pickFormElements(ValidHtml $html, &$found = []) + { + if ($html instanceof BaseFormElement) { + $found[] = $html; + } elseif ($html instanceof HtmlDocument) { + foreach ($html->getContent() as $content) { + $this->pickFormElements($content, $found); + } + } + + return $found; + } + + /** + * @param string $label + * @param array $attributes + * @return SubmitElement + */ + public static function buttonNext($label, $attributes = []) + { + return new SubmitElement('next', $attributes + [ + 'label' => $label + ]); + } + + /** + * @param string $label + * @param array $attributes + * @return SubmitElement + */ + public static function buttonConfirm($label, $attributes = []) + { + return new SubmitElement('submit', $attributes + [ + 'label' => $label + ]); + } + + /** + * @param string $label + * @param array $attributes + * @return SubmitElement + */ + public static function buttonCancel($label, $attributes = []) + { + return new SubmitElement('cancel', $attributes + [ + 'label' => $label + ]); + } + + public function addToForm(Form $form) + { + $cancel = $this->cancel; + $confirm = $this->confirm; + $next = $this->next; + if ($form->hasBeenSent()) { + $form->add($this->withConfirm); + if ($this->confirmFirst) { + $form->addElement($confirm); + $form->addElement($cancel); + } else { + $form->addElement($cancel); + $form->addElement($confirm); + } + if ($cancel->hasBeenPressed()) { + $this->withConfirmContent = new HtmlDocument(); + // HINT: we might also want to redirect on cancel and stop here, + // but currently we have no Response + $form->setSubmitted(false); + $form->remove($confirm); + $form->remove($cancel); + $form->add($next); + $form->setSubmitButton($next); + } else { + $form->setSubmitButton($confirm); + $form->remove($next); + foreach ($this->pickFormElements($this->withConfirmContent) as $element) { + $form->registerElement($element); + } + } + } else { + $form->add($this->withNext); + foreach ($this->pickFormElements($this->withNextContent) as $element) { + $form->registerElement($element); + } + $form->addElement($next); + } + } +} diff --git a/vendor/gipfl/web/src/Form/Validator/AlwaysFailValidator.php b/vendor/gipfl/web/src/Form/Validator/AlwaysFailValidator.php new file mode 100644 index 0000000..6fee92f --- /dev/null +++ b/vendor/gipfl/web/src/Form/Validator/AlwaysFailValidator.php @@ -0,0 +1,16 @@ +<?php + +namespace gipfl\Web\Form\Validator; + +class AlwaysFailValidator extends SimpleValidator +{ + public function isValid($value) + { + $message = $this->getSetting('message'); + if ($message) { + $this->addMessage($message); + } + + return false; + } +} diff --git a/vendor/gipfl/web/src/Form/Validator/PhpSessionBasedCsrfTokenValidator.php b/vendor/gipfl/web/src/Form/Validator/PhpSessionBasedCsrfTokenValidator.php new file mode 100644 index 0000000..2f08c6c --- /dev/null +++ b/vendor/gipfl/web/src/Form/Validator/PhpSessionBasedCsrfTokenValidator.php @@ -0,0 +1,34 @@ +<?php + +namespace gipfl\Web\Form\Validator; + +class PhpSessionBasedCsrfTokenValidator extends SimpleValidator +{ + public function isValid($value) + { + if (strpos($value, '|') === false) { + return false; + } + + list($seed, $token) = \explode('|', $value, 2); + + if (! \is_numeric($seed)) { + return false; + } + + if ($token === \hash('sha256', \session_id() . $seed)) { + return true; + } else { + $this->addMessage('An invalid CSRF token has been submitted'); + return false; + } + } + + public static function generateCsrfValue() + { + $seed = \mt_rand(); + $token = \hash('sha256', \session_id() . $seed); + + return \sprintf('%s|%s', $seed, $token); + } +} diff --git a/vendor/gipfl/web/src/Form/Validator/SimpleValidator.php b/vendor/gipfl/web/src/Form/Validator/SimpleValidator.php new file mode 100644 index 0000000..e06a10f --- /dev/null +++ b/vendor/gipfl/web/src/Form/Validator/SimpleValidator.php @@ -0,0 +1,27 @@ +<?php + +namespace gipfl\Web\Form\Validator; + +use ipl\Stdlib\Contract\Validator; +use ipl\Stdlib\Messages; + +abstract class SimpleValidator implements Validator +{ + use Messages; + + protected $settings = []; + + public function __construct(array $settings = []) + { + $this->settings = $settings; + } + + public function getSetting($name, $default = null) + { + if (array_key_exists($name, $this->settings)) { + return $this->settings[$name]; + } else { + return $default; + } + } +} diff --git a/vendor/gipfl/web/src/HtmlHelper.php b/vendor/gipfl/web/src/HtmlHelper.php new file mode 100644 index 0000000..19862f8 --- /dev/null +++ b/vendor/gipfl/web/src/HtmlHelper.php @@ -0,0 +1,29 @@ +<?php + +namespace gipfl\Web; + +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; + +abstract class HtmlHelper +{ + public static function elementHasClass(BaseHtmlElement $element, $class) + { + return static::classIsSet($element->getAttributes(), $class); + } + + public static function addClassOnce(Attributes $attributes, $class) + { + if (! HtmlHelper::classIsSet($attributes, $class)) { + $attributes->add('class', $class); + } + } + + public static function classIsSet(Attributes $attributes, $class) + { + $classes = $attributes->get('class'); + + return \is_array($classes) && in_array($class, $classes) + || \is_string($classes) && $classes === $class; + } +} diff --git a/vendor/gipfl/web/src/InlineForm.php b/vendor/gipfl/web/src/InlineForm.php new file mode 100644 index 0000000..fd6b301 --- /dev/null +++ b/vendor/gipfl/web/src/InlineForm.php @@ -0,0 +1,10 @@ +<?php + +namespace gipfl\Web; + +class InlineForm extends Form +{ + protected $defaultDecoratorClass = null; + + protected $formCssClasses = ['gipfl-form', 'gipfl-inline-form']; +} diff --git a/vendor/gipfl/web/src/Table/NameValueTable.php b/vendor/gipfl/web/src/Table/NameValueTable.php new file mode 100644 index 0000000..7227de7 --- /dev/null +++ b/vendor/gipfl/web/src/Table/NameValueTable.php @@ -0,0 +1,47 @@ +<?php + +namespace gipfl\Web\Table; + +use ipl\Html\BaseHtmlElement; +use ipl\Html\Table; + +class NameValueTable extends Table +{ + protected $defaultAttributes = ['class' => 'gipfl-name-value-table']; + + public static function create($pairs = []) + { + $self = new static; + $self->addNameValuePairs($pairs); + + return $self; + } + + public function createNameValueRow($name, $value) + { + return $this::tr([$this::th($name), $this::wantTd($value)]); + } + + public function addNameValueRow($name, $value) + { + return $this->add($this->createNameValueRow($name, $value)); + } + + public function addNameValuePairs($pairs) + { + foreach ($pairs as $name => $value) { + $this->addNameValueRow($name, $value); + } + + return $this; + } + + protected function wantTd($value) + { + if ($value instanceof BaseHtmlElement && $value->getTag() === 'td') { + return $value; + } else { + return $this::td($value); + } + } +} diff --git a/vendor/gipfl/web/src/Widget/CollapsibleList.php b/vendor/gipfl/web/src/Widget/CollapsibleList.php new file mode 100644 index 0000000..0df8234 --- /dev/null +++ b/vendor/gipfl/web/src/Widget/CollapsibleList.php @@ -0,0 +1,74 @@ +<?php + +namespace gipfl\Web\Widget; + +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; +use InvalidArgumentException; +use LogicException; +use function count; + +class CollapsibleList extends BaseHtmlElement +{ + protected $tag = 'ul'; + + protected $defaultAttributes = [ + 'class' => 'gipfl-collapsible' + ]; + + protected $defaultListAttributes; + + protected $defaultSectionAttributes; + + protected $items = []; + + public function __construct($items = [], $listAttributes = null) + { + if ($listAttributes !== null) { + $this->defaultListAttributes = $listAttributes; + } + foreach ($items as $title => $item) { + $this->addItem($title, $item); + } + } + + public function addItem($title, $content) + { + if ($this->hasItem($title)) { + throw new LogicException("Cannot add item with title '$title' twice"); + } + $item = Html::tag('li', [ + Html::tag('a', ['href' => '#', 'class' => 'gipfl-collapsible-control'], $title), + $content + ]); + + if (count($this->items) > 0) { + $item->getAttributes()->add('class', 'collapsed'); + } + $this->items[$title] = $item; + } + + public function hasItem($title) + { + return isset($this->items[$title]); + } + + public function getItem($name) + { + if (isset($this->items[$name])) { + return $this->items[$name]; + } + + throw new InvalidArgumentException("There is no '$name' item in this list"); + } + + protected function assemble() + { + if ($this->defaultListAttributes) { + $this->addAttributes($this->defaultListAttributes); + } + foreach ($this->items as $item) { + $this->add($item); + } + } +} diff --git a/vendor/gipfl/web/src/Widget/ConfigDiff.php b/vendor/gipfl/web/src/Widget/ConfigDiff.php new file mode 100644 index 0000000..8ac366f --- /dev/null +++ b/vendor/gipfl/web/src/Widget/ConfigDiff.php @@ -0,0 +1,106 @@ +<?php + +namespace gipfl\Web\Widget; + +use Diff; +use ipl\Html\ValidHtml; +use InvalidArgumentException; + +/** + * @deprecated - please use gipfl\Diff + */ +class ConfigDiff implements ValidHtml +{ + protected $a; + + protected $b; + + protected $diff; + + protected $htmlRenderer = 'SideBySide'; + + protected $knownHtmlRenderers = [ + 'SideBySide', + 'Inline', + ]; + + protected $knownTextRenderers = [ + 'Context', + 'Unified', + ]; + + protected $vendorDir; + + protected function __construct($a, $b) + { + $this->vendorDir = \dirname(\dirname(__DIR__)) . '/vendor'; + require_once $this->vendorDir . '/php-diff/lib/Diff.php'; + + if (empty($a)) { + $this->a = []; + } else { + $this->a = explode("\n", (string) $a); + } + + if (empty($b)) { + $this->b = []; + } else { + $this->b = explode("\n", (string) $b); + } + + $options = [ + 'context' => 5, + // 'ignoreWhitespace' => true, + // 'ignoreCase' => true, + ]; + $this->diff = new Diff($this->a, $this->b, $options); + } + + public function render() + { + return $this->renderHtml(); + } + + /** + * @return string + */ + public function renderHtml() + { + return $this->diff->Render($this->getHtmlRenderer()); + } + + public function setHtmlRenderer($name) + { + if (in_array($name, $this->knownHtmlRenderers)) { + $this->htmlRenderer = $name; + } else { + throw new InvalidArgumentException("There is no known '$name' renderer"); + } + + return $this; + } + + protected function getHtmlRenderer() + { + $filename = sprintf( + '%s/vendor/php-diff/lib/Diff/Renderer/Html/%s.php', + $this->vendorDir, + $this->htmlRenderer + ); + require_once($filename); + + $class = 'Diff_Renderer_Html_' . $this->htmlRenderer; + + return new $class(); + } + + public function __toString() + { + return $this->renderHtml(); + } + + public static function create($a, $b) + { + return new static($a, $b); + } +} diff --git a/vendor/gipfl/web/src/Widget/Hint.php b/vendor/gipfl/web/src/Widget/Hint.php new file mode 100644 index 0000000..785d9e4 --- /dev/null +++ b/vendor/gipfl/web/src/Widget/Hint.php @@ -0,0 +1,45 @@ +<?php + +namespace gipfl\Web\Widget; + +use ipl\Html\BaseHtmlElement; +use ipl\Html\Html; + +class Hint extends BaseHtmlElement +{ + protected $tag = 'div'; + + protected $defaultAttributes = [ + 'class' => 'gipfl-widget-hint' + ]; + + public function __construct($message, $class = 'ok', ...$params) + { + $this->addAttributes(['class' => $class]); + if (empty($params)) { + $this->setContent($message); + } else { + $this->setContent(Html::sprintf($message, ...$params)); + } + } + + public static function ok($message, ...$params) + { + return new static($message, 'ok', ...$params); + } + + public static function info($message, ...$params) + { + return new static($message, 'info', ...$params); + } + + public static function warning($message, ...$params) + { + return new static($message, 'warning', ...$params); + } + + public static function error($message, ...$params) + { + return new static($message, 'error', ...$params); + } +} diff --git a/vendor/gipfl/web/src/vendor/php-diff/lib/Diff.php b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff.php new file mode 100644 index 0000000..d1eb9da --- /dev/null +++ b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff.php @@ -0,0 +1,179 @@ +<?php +/** + * Diff + * + * A comprehensive library for generating differences between two strings + * in multiple formats (unified, side by side HTML etc) + * + * PHP version 5 + * + * Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com> + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the Chris Boulton nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package Diff + * @author Chris Boulton <chris.boulton@interspire.com> + * @copyright (c) 2009 Chris Boulton + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 1.1 + * @link http://github.com/chrisboulton/php-diff + */ + +class Diff +{ + /** + * @var array The "old" sequence to use as the basis for the comparison. + */ + private $a = null; + + /** + * @var array The "new" sequence to generate the changes for. + */ + private $b = null; + + /** + * @var array Array containing the generated opcodes for the differences between the two items. + */ + private $groupedCodes = null; + + /** + * @var array Associative array of the default options available for the diff class and their default value. + */ + private $defaultOptions = array( + 'context' => 3, + 'ignoreNewLines' => false, + 'ignoreWhitespace' => false, + 'ignoreCase' => false + ); + + /** + * @var array Array of the options that have been applied for generating the diff. + */ + private $options = array(); + + /** + * The constructor. + * + * @param array $a Array containing the lines of the first string to compare. + * @param array $b Array containing the lines for the second string to compare. + */ + public function __construct($a, $b, $options=array()) + { + $this->a = $a; + $this->b = $b; + + if (is_array($options)) + $this->options = array_merge($this->defaultOptions, $options); + else + $this->options = $this->defaultOptions; + } + + /** + * Render a diff using the supplied rendering class and return it. + * + * @param object $renderer An instance of the rendering object to use for generating the diff. + * @return mixed The generated diff. Exact return value depends on the rendered. + */ + public function render(Diff_Renderer_Abstract $renderer) + { + $renderer->diff = $this; + return $renderer->render(); + } + + /** + * Get a range of lines from $start to $end from the first comparison string + * and return them as an array. If no values are supplied, the entire string + * is returned. It's also possible to specify just one line to return only + * that line. + * + * @param int $start The starting number. + * @param int $end The ending number. If not supplied, only the item in $start will be returned. + * @return array Array of all of the lines between the specified range. + */ + public function getA($start=0, $end=null) + { + if($start == 0 && $end === null) { + return $this->a; + } + + if($end === null) { + $length = 1; + } + else { + $length = $end - $start; + } + + return array_slice($this->a, $start, $length); + + } + + /** + * Get a range of lines from $start to $end from the second comparison string + * and return them as an array. If no values are supplied, the entire string + * is returned. It's also possible to specify just one line to return only + * that line. + * + * @param int $start The starting number. + * @param int $end The ending number. If not supplied, only the item in $start will be returned. + * @return array Array of all of the lines between the specified range. + */ + public function getB($start=0, $end=null) + { + if($start == 0 && $end === null) { + return $this->b; + } + + if($end === null) { + $length = 1; + } + else { + $length = $end - $start; + } + + return array_slice($this->b, $start, $length); + } + + /** + * Generate a list of the compiled and grouped opcodes for the differences between the + * two strings. Generally called by the renderer, this class instantiates the sequence + * matcher and performs the actual diff generation and return an array of the opcodes + * for it. Once generated, the results are cached in the diff class instance. + * + * @return array Array of the grouped opcodes for the generated diff. + */ + public function getGroupedOpcodes() + { + if(!is_null($this->groupedCodes)) { + return $this->groupedCodes; + } + + require_once dirname(__FILE__).'/Diff/SequenceMatcher.php'; + $sequenceMatcher = new Diff_SequenceMatcher($this->a, $this->b, null, $this->options); + $this->groupedCodes = $sequenceMatcher->getGroupedOpcodes($this->options['context']); + return $this->groupedCodes; + } +}
\ No newline at end of file diff --git a/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Abstract.php b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Abstract.php new file mode 100644 index 0000000..f63c3e7 --- /dev/null +++ b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Abstract.php @@ -0,0 +1,82 @@ +<?php +/** + * Abstract class for diff renderers in PHP DiffLib. + * + * PHP version 5 + * + * Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com> + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the Chris Boulton nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package DiffLib + * @author Chris Boulton <chris.boulton@interspire.com> + * @copyright (c) 2009 Chris Boulton + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 1.1 + * @link http://github.com/chrisboulton/php-diff + */ + +abstract class Diff_Renderer_Abstract +{ + /** + * @var object Instance of the diff class that this renderer is generating the rendered diff for. + */ + public $diff; + + /** + * @var array Array of the default options that apply to this renderer. + */ + protected $defaultOptions = array(); + + /** + * @var array Array containing the user applied and merged default options for the renderer. + */ + protected $options = array(); + + /** + * The constructor. Instantiates the rendering engine and if options are passed, + * sets the options for the renderer. + * + * @param array $options Optionally, an array of the options for the renderer. + */ + public function __construct(array $options = array()) + { + $this->setOptions($options); + } + + /** + * Set the options of the renderer to those supplied in the passed in array. + * Options are merged with the default to ensure that there aren't any missing + * options. + * + * @param array $options Array of options to set. + */ + public function setOptions(array $options) + { + $this->options = array_merge($this->defaultOptions, $options); + } +}
\ No newline at end of file diff --git a/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Html/Array.php b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Html/Array.php new file mode 100644 index 0000000..2fe9625 --- /dev/null +++ b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Html/Array.php @@ -0,0 +1,230 @@ +<?php +/** + * Base renderer for rendering HTML based diffs for PHP DiffLib. + * + * PHP version 5 + * + * Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com> + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the Chris Boulton nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package DiffLib + * @author Chris Boulton <chris.boulton@interspire.com> + * @copyright (c) 2009 Chris Boulton + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 1.1 + * @link http://github.com/chrisboulton/php-diff + */ + +require_once dirname(__FILE__).'/../Abstract.php'; + +class Diff_Renderer_Html_Array extends Diff_Renderer_Abstract +{ + /** + * @var array Array of the default options that apply to this renderer. + */ + protected $defaultOptions = array( + 'tabSize' => 4 + ); + + /** + * Render and return an array structure suitable for generating HTML + * based differences. Generally called by subclasses that generate a + * HTML based diff and return an array of the changes to show in the diff. + * + * @return array An array of the generated chances, suitable for presentation in HTML. + */ + public function render() + { + // As we'll be modifying a & b to include our change markers, + // we need to get the contents and store them here. That way + // we're not going to destroy the original data + $a = $this->diff->getA(); + $b = $this->diff->getB(); + + $changes = array(); + $opCodes = $this->diff->getGroupedOpcodes(); + foreach($opCodes as $group) { + $blocks = array(); + $lastTag = null; + $lastBlock = 0; + foreach($group as $code) { + list($tag, $i1, $i2, $j1, $j2) = $code; + + if($tag == 'replace' && $i2 - $i1 == $j2 - $j1) { + for($i = 0; $i < ($i2 - $i1); ++$i) { + $fromLine = $a[$i1 + $i]; + $toLine = $b[$j1 + $i]; + + list($start, $end) = $this->getChangeExtent($fromLine, $toLine); + if($start != 0 || $end != 0) { + $last = $end + strlen($fromLine); + $fromLine = substr_replace($fromLine, "\0", $start, 0); + $fromLine = substr_replace($fromLine, "\1", $last + 1, 0); + $last = $end + strlen($toLine); + $toLine = substr_replace($toLine, "\0", $start, 0); + $toLine = substr_replace($toLine, "\1", $last + 1, 0); + $a[$i1 + $i] = $fromLine; + $b[$j1 + $i] = $toLine; + } + } + } + + if($tag != $lastTag) { + $blocks[] = array( + 'tag' => $tag, + 'base' => array( + 'offset' => $i1, + 'lines' => array() + ), + 'changed' => array( + 'offset' => $j1, + 'lines' => array() + ) + ); + $lastBlock = count($blocks)-1; + } + + $lastTag = $tag; + + if($tag == 'equal') { + $lines = array_slice($a, $i1, ($i2 - $i1)); + $blocks[$lastBlock]['base']['lines'] += $this->formatLines($lines); + $lines = array_slice($b, $j1, ($j2 - $j1)); + $blocks[$lastBlock]['changed']['lines'] += $this->formatLines($lines); + } + else { + if($tag == 'replace' || $tag == 'delete') { + $lines = array_slice($a, $i1, ($i2 - $i1)); + $lines = $this->formatLines($lines); + $lines = str_replace(array("\0", "\1"), array('<del>', '</del>'), $lines); + $blocks[$lastBlock]['base']['lines'] += $lines; + } + + if($tag == 'replace' || $tag == 'insert') { + $lines = array_slice($b, $j1, ($j2 - $j1)); + $lines = $this->formatLines($lines); + $lines = str_replace(array("\0", "\1"), array('<ins>', '</ins>'), $lines); + $blocks[$lastBlock]['changed']['lines'] += $lines; + } + } + } + $changes[] = $blocks; + } + return $changes; + } + + /** + * Given two strings, determine where the changes in the two strings + * begin, and where the changes in the two strings end. + * + * @param string $fromLine The first string. + * @param string $toLine The second string. + * @return array Array containing the starting position (0 by default) and the ending position (-1 by default) + */ + private function getChangeExtent($fromLine, $toLine) + { + $start = 0; + $limit = min(strlen($fromLine), strlen($toLine)); + while($start < $limit && $fromLine{$start} == $toLine{$start}) { + ++$start; + } + $end = -1; + $limit = $limit - $start; + while(-$end <= $limit && substr($fromLine, $end, 1) == substr($toLine, $end, 1)) { + --$end; + } + return array( + $start, + $end + 1 + ); + } + + /** + * Format a series of lines suitable for output in a HTML rendered diff. + * This involves replacing tab characters with spaces, making the HTML safe + * for output, ensuring that double spaces are replaced with etc. + * + * @param array $lines Array of lines to format. + * @return array Array of the formatted lines. + */ + protected function formatLines($lines) + { + $lines = array_map(array($this, 'ExpandTabs'), $lines); + $lines = array_map(array($this, 'HtmlSafe'), $lines); + foreach($lines as &$line) { + $line = preg_replace_callback('# ( +)|^ #', array($this, 'fixSpaces'), $line); + } + return $lines; + } + + /** + * Replace a string containing spaces with a HTML representation using . + * + * @param string[] $matches Array with preg matches. + * @return string The HTML representation of the string. + */ + private function fixSpaces(array $matches) + { + $count = 0; + + if (count($matches) > 1) { + $spaces = $matches[1]; + $count = strlen($spaces); + } + + if ($count == 0) { + return ''; + } + + $div = floor($count / 2); + $mod = $count % 2; + return str_repeat(' ', $div).str_repeat(' ', $mod); + } + + /** + * Replace tabs in a single line with a number of spaces as defined by the tabSize option. + * + * @param string $line The containing tabs to convert. + * @return string The line with the tabs converted to spaces. + */ + private function expandTabs($line) + { + return str_replace("\t", str_repeat(' ', $this->options['tabSize']), $line); + } + + /** + * Make a string containing HTML safe for output on a page. + * + * @param string $string The string. + * @return string The string with the HTML characters replaced by entities. + */ + private function htmlSafe($string) + { + return htmlspecialchars($string, ENT_NOQUOTES, 'UTF-8'); + } +} diff --git a/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Html/Inline.php b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Html/Inline.php new file mode 100644 index 0000000..a37fec6 --- /dev/null +++ b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Html/Inline.php @@ -0,0 +1,143 @@ +<?php +/** + * Inline HTML diff generator for PHP DiffLib. + * + * PHP version 5 + * + * Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com> + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the Chris Boulton nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package DiffLib + * @author Chris Boulton <chris.boulton@interspire.com> + * @copyright (c) 2009 Chris Boulton + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 1.1 + * @link http://github.com/chrisboulton/php-diff + */ + +require_once dirname(__FILE__).'/Array.php'; + +class Diff_Renderer_Html_Inline extends Diff_Renderer_Html_Array +{ + /** + * Render a and return diff with changes between the two sequences + * displayed inline (under each other) + * + * @return string The generated inline diff. + */ + public function render() + { + $changes = parent::render(); + $html = ''; + if(empty($changes)) { + return $html; + } + + $html .= '<table class="Differences DifferencesInline">'; + $html .= '<thead>'; + $html .= '<tr>'; + $html .= '<th>Old</th>'; + $html .= '<th>New</th>'; + $html .= '<th>Differences</th>'; + $html .= '</tr>'; + $html .= '</thead>'; + foreach($changes as $i => $blocks) { + // If this is a separate block, we're condensing code so output ..., + // indicating a significant portion of the code has been collapsed as + // it is the same + if($i > 0) { + $html .= '<tbody class="Skipped">'; + $html .= '<th>…</th>'; + $html .= '<th>…</th>'; + $html .= '<td> </td>'; + $html .= '</tbody>'; + } + + foreach($blocks as $change) { + $html .= '<tbody class="Change'.ucfirst($change['tag']).'">'; + // Equal changes should be shown on both sides of the diff + if($change['tag'] == 'equal') { + foreach($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $toLine = $change['changed']['offset'] + $no + 1; + $html .= '<tr>'; + $html .= '<th>'.$fromLine.'</th>'; + $html .= '<th>'.$toLine.'</th>'; + $html .= '<td class="Left">'.$line.'</td>'; + $html .= '</tr>'; + } + } + // Added lines only on the right side + else if($change['tag'] == 'insert') { + foreach($change['changed']['lines'] as $no => $line) { + $toLine = $change['changed']['offset'] + $no + 1; + $html .= '<tr>'; + $html .= '<th> </th>'; + $html .= '<th>'.$toLine.'</th>'; + $html .= '<td class="Right"><ins>'.$line.'</ins> </td>'; + $html .= '</tr>'; + } + } + // Show deleted lines only on the left side + else if($change['tag'] == 'delete') { + foreach($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $html .= '<tr>'; + $html .= '<th>'.$fromLine.'</th>'; + $html .= '<th> </th>'; + $html .= '<td class="Left"><del>'.$line.'</del> </td>'; + $html .= '</tr>'; + } + } + // Show modified lines on both sides + else if($change['tag'] == 'replace') { + foreach($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $html .= '<tr>'; + $html .= '<th>'.$fromLine.'</th>'; + $html .= '<th> </th>'; + $html .= '<td class="Left"><span>'.$line.'</span></td>'; + $html .= '</tr>'; + } + + foreach($change['changed']['lines'] as $no => $line) { + $toLine = $change['changed']['offset'] + $no + 1; + $html .= '<tr>'; + $html .= '<th> </th>'; + $html .= '<th>'.$toLine.'</th>'; + $html .= '<td class="Right"><span>'.$line.'</span></td>'; + $html .= '</tr>'; + } + } + $html .= '</tbody>'; + } + } + $html .= '</table>'; + return $html; + } +} diff --git a/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Html/SideBySide.php b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Html/SideBySide.php new file mode 100644 index 0000000..307af1c --- /dev/null +++ b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Html/SideBySide.php @@ -0,0 +1,163 @@ +<?php +/** + * Side by Side HTML diff generator for PHP DiffLib. + * + * PHP version 5 + * + * Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com> + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the Chris Boulton nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package DiffLib + * @author Chris Boulton <chris.boulton@interspire.com> + * @copyright (c) 2009 Chris Boulton + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 1.1 + * @link http://github.com/chrisboulton/php-diff + */ + +require_once dirname(__FILE__).'/Array.php'; + +class Diff_Renderer_Html_SideBySide extends Diff_Renderer_Html_Array +{ + /** + * Render a and return diff with changes between the two sequences + * displayed side by side. + * + * @return string The generated side by side diff. + */ + public function render() + { + $changes = parent::render(); + + $html = ''; + if(empty($changes)) { + return $html; + } + + $html .= '<table class="Differences DifferencesSideBySide">'; + $html .= '<thead>'; + $html .= '<tr>'; + $html .= '<th colspan="2">Old Version</th>'; + $html .= '<th colspan="2">New Version</th>'; + $html .= '</tr>'; + $html .= '</thead>'; + foreach($changes as $i => $blocks) { + if($i > 0) { + $html .= '<tbody class="Skipped">'; + $html .= '<th>…</th><td> </td>'; + $html .= '<th>…</th><td> </td>'; + $html .= '</tbody>'; + } + + foreach($blocks as $change) { + $html .= '<tbody class="Change'.ucfirst($change['tag']).'">'; + // Equal changes should be shown on both sides of the diff + if($change['tag'] == 'equal') { + foreach($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $toLine = $change['changed']['offset'] + $no + 1; + $html .= '<tr>'; + $html .= '<th>'.$fromLine.'</th>'; + $html .= '<td class="Left"><span>'.$line.'</span> </span></td>'; + $html .= '<th>'.$toLine.'</th>'; + $html .= '<td class="Right"><span>'.$line.'</span> </span></td>'; + $html .= '</tr>'; + } + } + // Added lines only on the right side + else if($change['tag'] == 'insert') { + foreach($change['changed']['lines'] as $no => $line) { + $toLine = $change['changed']['offset'] + $no + 1; + $html .= '<tr>'; + $html .= '<th> </th>'; + $html .= '<td class="Left"> </td>'; + $html .= '<th>'.$toLine.'</th>'; + $html .= '<td class="Right"><ins>'.$line.'</ins> </td>'; + $html .= '</tr>'; + } + } + // Show deleted lines only on the left side + else if($change['tag'] == 'delete') { + foreach($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $html .= '<tr>'; + $html .= '<th>'.$fromLine.'</th>'; + $html .= '<td class="Left"><del>'.$line.'</del> </td>'; + $html .= '<th> </th>'; + $html .= '<td class="Right"> </td>'; + $html .= '</tr>'; + } + } + // Show modified lines on both sides + else if($change['tag'] == 'replace') { + if(count($change['base']['lines']) >= count($change['changed']['lines'])) { + foreach($change['base']['lines'] as $no => $line) { + $fromLine = $change['base']['offset'] + $no + 1; + $html .= '<tr>'; + $html .= '<th>'.$fromLine.'</th>'; + $html .= '<td class="Left"><span>'.$line.'</span> </td>'; + if(!isset($change['changed']['lines'][$no])) { + $toLine = ' '; + $changedLine = ' '; + } + else { + $toLine = $change['base']['offset'] + $no + 1; + $changedLine = '<span>'.$change['changed']['lines'][$no].'</span>'; + } + $html .= '<th>'.$toLine.'</th>'; + $html .= '<td class="Right">'.$changedLine.'</td>'; + $html .= '</tr>'; + } + } + else { + foreach($change['changed']['lines'] as $no => $changedLine) { + if(!isset($change['base']['lines'][$no])) { + $fromLine = ' '; + $line = ' '; + } + else { + $fromLine = $change['base']['offset'] + $no + 1; + $line = '<span>'.$change['base']['lines'][$no].'</span>'; + } + $html .= '<tr>'; + $html .= '<th>'.$fromLine.'</th>'; + $html .= '<td class="Left"><span>'.$line.'</span> </td>'; + $toLine = $change['changed']['offset'] + $no + 1; + $html .= '<th>'.$toLine.'</th>'; + $html .= '<td class="Right">'.$changedLine.'</td>'; + $html .= '</tr>'; + } + } + } + $html .= '</tbody>'; + } + } + $html .= '</table>'; + return $html; + } +}
\ No newline at end of file diff --git a/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Text/Context.php b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Text/Context.php new file mode 100644 index 0000000..1200b01 --- /dev/null +++ b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Text/Context.php @@ -0,0 +1,128 @@ +<?php +/** + * Context diff generator for PHP DiffLib. + * + * PHP version 5 + * + * Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com> + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the Chris Boulton nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package DiffLib + * @author Chris Boulton <chris.boulton@interspire.com> + * @copyright (c) 2009 Chris Boulton + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 1.1 + * @link http://github.com/chrisboulton/php-diff + */ + +require_once dirname(__FILE__).'/../Abstract.php'; + +class Diff_Renderer_Text_Context extends Diff_Renderer_Abstract +{ + /** + * @var array Array of the different opcode tags and how they map to the context diff equivalent. + */ + private $tagMap = array( + 'insert' => '+', + 'delete' => '-', + 'replace' => '!', + 'equal' => ' ' + ); + + /** + * Render and return a context formatted (old school!) diff file. + * + * @return string The generated context diff. + */ + public function render() + { + $diff = ''; + $opCodes = $this->diff->getGroupedOpcodes(); + foreach($opCodes as $group) { + $diff .= "***************\n"; + $lastItem = count($group)-1; + $i1 = $group[0][1]; + $i2 = $group[$lastItem][2]; + $j1 = $group[0][3]; + $j2 = $group[$lastItem][4]; + + if($i2 - $i1 >= 2) { + $diff .= '*** '.($group[0][1] + 1).','.$i2." ****\n"; + } + else { + $diff .= '*** '.$i2." ****\n"; + } + + if($j2 - $j1 >= 2) { + $separator = '--- '.($j1 + 1).','.$j2." ----\n"; + } + else { + $separator = '--- '.$j2." ----\n"; + } + + $hasVisible = false; + foreach($group as $code) { + if($code[0] == 'replace' || $code[0] == 'delete') { + $hasVisible = true; + break; + } + } + + if($hasVisible) { + foreach($group as $code) { + list($tag, $i1, $i2, $j1, $j2) = $code; + if($tag == 'insert') { + continue; + } + $diff .= $this->tagMap[$tag].' '.implode("\n".$this->tagMap[$tag].' ', $this->diff->GetA($i1, $i2))."\n"; + } + } + + $hasVisible = false; + foreach($group as $code) { + if($code[0] == 'replace' || $code[0] == 'insert') { + $hasVisible = true; + break; + } + } + + $diff .= $separator; + + if($hasVisible) { + foreach($group as $code) { + list($tag, $i1, $i2, $j1, $j2) = $code; + if($tag == 'delete') { + continue; + } + $diff .= $this->tagMap[$tag].' '.implode("\n".$this->tagMap[$tag].' ', $this->diff->GetB($j1, $j2))."\n"; + } + } + } + return $diff; + } +}
\ No newline at end of file diff --git a/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Text/Unified.php b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Text/Unified.php new file mode 100644 index 0000000..e94d951 --- /dev/null +++ b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/Renderer/Text/Unified.php @@ -0,0 +1,87 @@ +<?php +/** + * Unified diff generator for PHP DiffLib. + * + * PHP version 5 + * + * Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com> + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the Chris Boulton nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package DiffLib + * @author Chris Boulton <chris.boulton@interspire.com> + * @copyright (c) 2009 Chris Boulton + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 1.1 + * @link http://github.com/chrisboulton/php-diff + */ + +require_once dirname(__FILE__).'/../Abstract.php'; + +class Diff_Renderer_Text_Unified extends Diff_Renderer_Abstract +{ + /** + * Render and return a unified diff. + * + * @return string The unified diff. + */ + public function render() + { + $diff = ''; + $opCodes = $this->diff->getGroupedOpcodes(); + foreach($opCodes as $group) { + $lastItem = count($group)-1; + $i1 = $group[0][1]; + $i2 = $group[$lastItem][2]; + $j1 = $group[0][3]; + $j2 = $group[$lastItem][4]; + + if($i1 == 0 && $i2 == 0) { + $i1 = -1; + $i2 = -1; + } + + $diff .= '@@ -'.($i1 + 1).','.($i2 - $i1).' +'.($j1 + 1).','.($j2 - $j1)." @@\n"; + foreach($group as $code) { + list($tag, $i1, $i2, $j1, $j2) = $code; + if($tag == 'equal') { + $diff .= ' '.implode("\n ", $this->diff->GetA($i1, $i2))."\n"; + } + else { + if($tag == 'replace' || $tag == 'delete') { + $diff .= '-'.implode("\n-", $this->diff->GetA($i1, $i2))."\n"; + } + + if($tag == 'replace' || $tag == 'insert') { + $diff .= '+'.implode("\n+", $this->diff->GetB($j1, $j2))."\n"; + } + } + } + } + return $diff; + } +}
\ No newline at end of file diff --git a/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/SequenceMatcher.php b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/SequenceMatcher.php new file mode 100644 index 0000000..a289e39 --- /dev/null +++ b/vendor/gipfl/web/src/vendor/php-diff/lib/Diff/SequenceMatcher.php @@ -0,0 +1,742 @@ +<?php +/** + * Sequence matcher for Diff + * + * PHP version 5 + * + * Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com> + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the Chris Boulton nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package Diff + * @author Chris Boulton <chris.boulton@interspire.com> + * @copyright (c) 2009 Chris Boulton + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @version 1.1 + * @link http://github.com/chrisboulton/php-diff + */ + +class Diff_SequenceMatcher +{ + /** + * @var string|array Either a string or an array containing a callback function to determine if a line is "junk" or not. + */ + private $junkCallback = null; + + /** + * @var array The first sequence to compare against. + */ + private $a = array(); + + /** + * @var array The second sequence. + */ + private $b = array(); + + /** + * @var array Array of characters that are considered junk from the second sequence. Characters are the array key. + */ + private $junkDict = array(); + + /** + * @var array Array of indices that do not contain junk elements. + */ + private $b2j = array(); + + private $options = array(); + + private $defaultOptions = array( + 'ignoreNewLines' => false, + 'ignoreWhitespace' => false, + 'ignoreCase' => false + ); + + /** + * The constructor. With the sequences being passed, they'll be set for the + * sequence matcher and it will perform a basic cleanup & calculate junk + * elements. + * + * @param string|array $a A string or array containing the lines to compare against. + * @param string|array $b A string or array containing the lines to compare. + * @param string|array $junkCallback Either an array or string that references a callback function (if there is one) to determine 'junk' characters. + */ + public function __construct($a, $b, $junkCallback=null, $options) + { + $this->a = array(); + $this->b = array(); + $this->junkCallback = $junkCallback; + $this->setOptions($options); + $this->setSequences($a, $b); + } + + public function setOptions($options) + { + $this->options = array_merge($this->defaultOptions, $options); + } + + /** + * Set the first and second sequences to use with the sequence matcher. + * + * @param string|array $a A string or array containing the lines to compare against. + * @param string|array $b A string or array containing the lines to compare. + */ + public function setSequences($a, $b) + { + $this->setSeq1($a); + $this->setSeq2($b); + } + + /** + * Set the first sequence ($a) and reset any internal caches to indicate that + * when calling the calculation methods, we need to recalculate them. + * + * @param string|array $a The sequence to set as the first sequence. + */ + public function setSeq1($a) + { + if(!is_array($a)) { + $a = str_split($a); + } + if($a == $this->a) { + return; + } + + $this->a= $a; + $this->matchingBlocks = null; + $this->opCodes = null; + } + + /** + * Set the second sequence ($b) and reset any internal caches to indicate that + * when calling the calculation methods, we need to recalculate them. + * + * @param string|array $b The sequence to set as the second sequence. + */ + public function setSeq2($b) + { + if(!is_array($b)) { + $b = str_split($b); + } + if($b == $this->b) { + return; + } + + $this->b = $b; + $this->matchingBlocks = null; + $this->opCodes = null; + $this->fullBCount = null; + $this->chainB(); + } + + /** + * Generate the internal arrays containing the list of junk and non-junk + * characters for the second ($b) sequence. + */ + private function chainB() + { + $length = count ($this->b); + $this->b2j = array(); + $popularDict = array(); + + for($i = 0; $i < $length; ++$i) { + $char = $this->b[$i]; + if(isset($this->b2j[$char])) { + if($length >= 200 && count($this->b2j[$char]) * 100 > $length) { + $popularDict[$char] = 1; + unset($this->b2j[$char]); + } + else { + $this->b2j[$char][] = $i; + } + } + else { + $this->b2j[$char] = array( + $i + ); + } + } + + // Remove leftovers + foreach(array_keys($popularDict) as $char) { + unset($this->b2j[$char]); + } + + $this->junkDict = array(); + if(is_callable($this->junkCallback)) { + foreach(array_keys($popularDict) as $char) { + if(call_user_func($this->junkCallback, $char)) { + $this->junkDict[$char] = 1; + unset($popularDict[$char]); + } + } + + foreach(array_keys($this->b2j) as $char) { + if(call_user_func($this->junkCallback, $char)) { + $this->junkDict[$char] = 1; + unset($this->b2j[$char]); + } + } + } + } + + /** + * Checks if a particular character is in the junk dictionary + * for the list of junk characters. + * + * @return boolean $b True if the character is considered junk. False if not. + */ + private function isBJunk($b) + { + if(isset($this->juncDict[$b])) { + return true; + } + + return false; + } + + /** + * Find the longest matching block in the two sequences, as defined by the + * lower and upper constraints for each sequence. (for the first sequence, + * $alo - $ahi and for the second sequence, $blo - $bhi) + * + * Essentially, of all of the maximal matching blocks, return the one that + * startest earliest in $a, and all of those maximal matching blocks that + * start earliest in $a, return the one that starts earliest in $b. + * + * If the junk callback is defined, do the above but with the restriction + * that the junk element appears in the block. Extend it as far as possible + * by matching only junk elements in both $a and $b. + * + * @param int $alo The lower constraint for the first sequence. + * @param int $ahi The upper constraint for the first sequence. + * @param int $blo The lower constraint for the second sequence. + * @param int $bhi The upper constraint for the second sequence. + * @return array Array containing the longest match that includes the starting position in $a, start in $b and the length/size. + */ + public function findLongestMatch($alo, $ahi, $blo, $bhi) + { + $a = $this->a; + $b = $this->b; + + $bestI = $alo; + $bestJ = $blo; + $bestSize = 0; + + $j2Len = array(); + $nothing = array(); + + for($i = $alo; $i < $ahi; ++$i) { + $newJ2Len = array(); + $jDict = $this->arrayGetDefault($this->b2j, $a[$i], $nothing); + foreach($jDict as $jKey => $j) { + if($j < $blo) { + continue; + } + else if($j >= $bhi) { + break; + } + + $k = $this->arrayGetDefault($j2Len, $j -1, 0) + 1; + $newJ2Len[$j] = $k; + if($k > $bestSize) { + $bestI = $i - $k + 1; + $bestJ = $j - $k + 1; + $bestSize = $k; + } + } + + $j2Len = $newJ2Len; + } + + while($bestI > $alo && $bestJ > $blo && !$this->isBJunk($b[$bestJ - 1]) && + !$this->linesAreDifferent($bestI - 1, $bestJ - 1)) { + --$bestI; + --$bestJ; + ++$bestSize; + } + + while($bestI + $bestSize < $ahi && ($bestJ + $bestSize) < $bhi && + !$this->isBJunk($b[$bestJ + $bestSize]) && !$this->linesAreDifferent($bestI + $bestSize, $bestJ + $bestSize)) { + ++$bestSize; + } + + while($bestI > $alo && $bestJ > $blo && $this->isBJunk($b[$bestJ - 1]) && + !$this->isLineDifferent($bestI - 1, $bestJ - 1)) { + --$bestI; + --$bestJ; + ++$bestSize; + } + + while($bestI + $bestSize < $ahi && $bestJ + $bestSize < $bhi && + $this->isBJunk($b[$bestJ + $bestSize]) && !$this->linesAreDifferent($bestI + $bestSize, $bestJ + $bestSize)) { + ++$bestSize; + } + + return array( + $bestI, + $bestJ, + $bestSize + ); + } + + /** + * Check if the two lines at the given indexes are different or not. + * + * @param int $aIndex Line number to check against in a. + * @param int $bIndex Line number to check against in b. + * @return boolean True if the lines are different and false if not. + */ + public function linesAreDifferent($aIndex, $bIndex) + { + $lineA = $this->a[$aIndex]; + $lineB = $this->b[$bIndex]; + + if($this->options['ignoreWhitespace']) { + $replace = array("\t", ' '); + $lineA = str_replace($replace, '', $lineA); + $lineB = str_replace($replace, '', $lineB); + } + + if($this->options['ignoreCase']) { + $lineA = strtolower($lineA); + $lineB = strtolower($lineB); + } + + if($lineA != $lineB) { + return true; + } + + return false; + } + + /** + * Return a nested set of arrays for all of the matching sub-sequences + * in the strings $a and $b. + * + * Each block contains the lower constraint of the block in $a, the lower + * constraint of the block in $b and finally the number of lines that the + * block continues for. + * + * @return array Nested array of the matching blocks, as described by the function. + */ + public function getMatchingBlocks() + { + if(!empty($this->matchingBlocks)) { + return $this->matchingBlocks; + } + + $aLength = count($this->a); + $bLength = count($this->b); + + $queue = array( + array( + 0, + $aLength, + 0, + $bLength + ) + ); + + $matchingBlocks = array(); + while(!empty($queue)) { + list($alo, $ahi, $blo, $bhi) = array_pop($queue); + $x = $this->findLongestMatch($alo, $ahi, $blo, $bhi); + list($i, $j, $k) = $x; + if($k) { + $matchingBlocks[] = $x; + if($alo < $i && $blo < $j) { + $queue[] = array( + $alo, + $i, + $blo, + $j + ); + } + + if($i + $k < $ahi && $j + $k < $bhi) { + $queue[] = array( + $i + $k, + $ahi, + $j + $k, + $bhi + ); + } + } + } + + usort($matchingBlocks, array($this, 'tupleSort')); + + $i1 = 0; + $j1 = 0; + $k1 = 0; + $nonAdjacent = array(); + foreach($matchingBlocks as $block) { + list($i2, $j2, $k2) = $block; + if($i1 + $k1 == $i2 && $j1 + $k1 == $j2) { + $k1 += $k2; + } + else { + if($k1) { + $nonAdjacent[] = array( + $i1, + $j1, + $k1 + ); + } + + $i1 = $i2; + $j1 = $j2; + $k1 = $k2; + } + } + + if($k1) { + $nonAdjacent[] = array( + $i1, + $j1, + $k1 + ); + } + + $nonAdjacent[] = array( + $aLength, + $bLength, + 0 + ); + + $this->matchingBlocks = $nonAdjacent; + return $this->matchingBlocks; + } + + /** + * Return a list of all of the opcodes for the differences between the + * two strings. + * + * The nested array returned contains an array describing the opcode + * which includes: + * 0 - The type of tag (as described below) for the opcode. + * 1 - The beginning line in the first sequence. + * 2 - The end line in the first sequence. + * 3 - The beginning line in the second sequence. + * 4 - The end line in the second sequence. + * + * The different types of tags include: + * replace - The string from $i1 to $i2 in $a should be replaced by + * the string in $b from $j1 to $j2. + * delete - The string in $a from $i1 to $j2 should be deleted. + * insert - The string in $b from $j1 to $j2 should be inserted at + * $i1 in $a. + * equal - The two strings with the specified ranges are equal. + * + * @return array Array of the opcodes describing the differences between the strings. + */ + public function getOpCodes() + { + if(!empty($this->opCodes)) { + return $this->opCodes; + } + + $i = 0; + $j = 0; + $this->opCodes = array(); + + $blocks = $this->getMatchingBlocks(); + foreach($blocks as $block) { + list($ai, $bj, $size) = $block; + $tag = ''; + if($i < $ai && $j < $bj) { + $tag = 'replace'; + } + else if($i < $ai) { + $tag = 'delete'; + } + else if($j < $bj) { + $tag = 'insert'; + } + + if($tag) { + $this->opCodes[] = array( + $tag, + $i, + $ai, + $j, + $bj + ); + } + + $i = $ai + $size; + $j = $bj + $size; + + if($size) { + $this->opCodes[] = array( + 'equal', + $ai, + $i, + $bj, + $j + ); + } + } + return $this->opCodes; + } + + /** + * Return a series of nested arrays containing different groups of generated + * opcodes for the differences between the strings with up to $context lines + * of surrounding content. + * + * Essentially what happens here is any big equal blocks of strings are stripped + * out, the smaller subsets of changes are then arranged in to their groups. + * This means that the sequence matcher and diffs do not need to include the full + * content of the different files but can still provide context as to where the + * changes are. + * + * @param int $context The number of lines of context to provide around the groups. + * @return array Nested array of all of the grouped opcodes. + */ + public function getGroupedOpcodes($context=3) + { + $opCodes = $this->getOpCodes(); + if(empty($opCodes)) { + $opCodes = array( + array( + 'equal', + 0, + 1, + 0, + 1 + ) + ); + } + + if($opCodes[0][0] == 'equal') { + $opCodes[0] = array( + $opCodes[0][0], + max($opCodes[0][1], $opCodes[0][2] - $context), + $opCodes[0][2], + max($opCodes[0][3], $opCodes[0][4] - $context), + $opCodes[0][4] + ); + } + + $lastItem = count($opCodes) - 1; + if($opCodes[$lastItem][0] == 'equal') { + list($tag, $i1, $i2, $j1, $j2) = $opCodes[$lastItem]; + $opCodes[$lastItem] = array( + $tag, + $i1, + min($i2, $i1 + $context), + $j1, + min($j2, $j1 + $context) + ); + } + + $maxRange = $context * 2; + $groups = array(); + $group = array(); + foreach($opCodes as $code) { + list($tag, $i1, $i2, $j1, $j2) = $code; + if($tag == 'equal' && $i2 - $i1 > $maxRange) { + $group[] = array( + $tag, + $i1, + min($i2, $i1 + $context), + $j1, + min($j2, $j1 + $context) + ); + $groups[] = $group; + $group = array(); + $i1 = max($i1, $i2 - $context); + $j1 = max($j1, $j2 - $context); + } + $group[] = array( + $tag, + $i1, + $i2, + $j1, + $j2 + ); + } + + if(!empty($group) && !(count($group) == 1 && $group[0][0] == 'equal')) { + $groups[] = $group; + } + + return $groups; + } + + /** + * Return a measure of the similarity between the two sequences. + * This will be a float value between 0 and 1. + * + * Out of all of the ratio calculation functions, this is the most + * expensive to call if getMatchingBlocks or getOpCodes is yet to be + * called. The other calculation methods (quickRatio and realquickRatio) + * can be used to perform quicker calculations but may be less accurate. + * + * The ratio is calculated as (2 * number of matches) / total number of + * elements in both sequences. + * + * @return float The calculated ratio. + */ + public function Ratio() + { + $matches = array_reduce($this->getMatchingBlocks(), array($this, 'ratioReduce'), 0); + return $this->calculateRatio($matches, count ($this->a) + count ($this->b)); + } + + /** + * Helper function to calculate the number of matches for Ratio(). + * + * @param int $sum The running total for the number of matches. + * @param array $triple Array containing the matching block triple to add to the running total. + * @return int The new running total for the number of matches. + */ + private function ratioReduce($sum, $triple) + { + return $sum + ($triple[count($triple) - 1]); + } + + /** + * Quickly return an upper bound ratio for the similarity of the strings. + * This is quicker to compute than Ratio(). + * + * @return float The calculated ratio. + */ + private function quickRatio() + { + if($this->fullBCount === null) { + $this->fullBCount = array(); + $bLength = count ($b); + for($i = 0; $i < $bLength; ++$i) { + $char = $this->b[$i]; + $this->fullBCount[$char] = $this->arrayGetDefault($this->fullBCount, $char, 0) + 1; + } + } + + $avail = array(); + $matches = 0; + $aLength = count ($this->a); + for($i = 0; $i < $aLength; ++$i) { + $char = $this->a[$i]; + if(isset($avail[$char])) { + $numb = $avail[$char]; + } + else { + $numb = $this->arrayGetDefault($this->fullBCount, $char, 0); + } + $avail[$char] = $numb - 1; + if($numb > 0) { + ++$matches; + } + } + + $this->calculateRatio($matches, count ($this->a) + count ($this->b)); + } + + /** + * Return an upper bound ratio really quickly for the similarity of the strings. + * This is quicker to compute than Ratio() and quickRatio(). + * + * @return float The calculated ratio. + */ + private function realquickRatio() + { + $aLength = count ($this->a); + $bLength = count ($this->b); + + return $this->calculateRatio(min($aLength, $bLength), $aLength + $bLength); + } + + /** + * Helper function for calculating the ratio to measure similarity for the strings. + * The ratio is defined as being 2 * (number of matches / total length) + * + * @param int $matches The number of matches in the two strings. + * @param int $length The length of the two strings. + * @return float The calculated ratio. + */ + private function calculateRatio($matches, $length=0) + { + if($length) { + return 2 * ($matches / $length); + } + else { + return 1; + } + } + + /** + * Helper function that provides the ability to return the value for a key + * in an array of it exists, or if it doesn't then return a default value. + * Essentially cleaner than doing a series of if(isset()) {} else {} calls. + * + * @param array $array The array to search. + * @param string $key The key to check that exists. + * @param mixed $default The value to return as the default value if the key doesn't exist. + * @return mixed The value from the array if the key exists or otherwise the default. + */ + private function arrayGetDefault($array, $key, $default) + { + if(isset($array[$key])) { + return $array[$key]; + } + else { + return $default; + } + } + + /** + * Sort an array by the nested arrays it contains. Helper function for getMatchingBlocks + * + * @param array $a First array to compare. + * @param array $b Second array to compare. + * @return int -1, 0 or 1, as expected by the usort function. + */ + private function tupleSort($a, $b) + { + $max = max(count($a), count($b)); + for($i = 0; $i < $max; ++$i) { + if($a[$i] < $b[$i]) { + return -1; + } + else if($a[$i] > $b[$i]) { + return 1; + } + } + + if(count($a) == $count($b)) { + return 0; + } + else if(count($a) < count($b)) { + return -1; + } + else { + return 1; + } + } +} |