|
<?php |
|
|
|
namespace Loilo\MediaQuery; |
|
|
|
/** |
|
* Base class for media query combinations |
|
*/ |
|
abstract class MediaQuery |
|
{ |
|
/** |
|
* Combine with another query using AND logic |
|
*/ |
|
public function and(string|MediaQuery $other): MediaQuery |
|
{ |
|
if (is_string($other)) { |
|
$other = new Atom($other); |
|
} |
|
|
|
return new Conjunction($this, $other); |
|
} |
|
|
|
/** |
|
* Combine with another query using AND NOT logic |
|
*/ |
|
public function and_not(string|MediaQuery $other): MediaQuery |
|
{ |
|
if (is_string($other)) { |
|
$other = new Atom($other); |
|
} |
|
|
|
return new Conjunction($this, new Negation($other)); |
|
} |
|
|
|
/** |
|
* Combine with another query using OR logic |
|
*/ |
|
public function or(string|MediaQuery $other): MediaQuery |
|
{ |
|
if (is_string($other)) { |
|
$other = new Atom($other); |
|
} |
|
|
|
return new Disjunction($this, $other); |
|
} |
|
|
|
/** |
|
* Combine with another query using OR NOT logic |
|
*/ |
|
public function or_not(string|MediaQuery $other): MediaQuery |
|
{ |
|
if (is_string($other)) { |
|
$other = new Atom($other); |
|
} |
|
|
|
return new Disjunction($this, new Negation($other)); |
|
} |
|
|
|
/** |
|
* Negate this query |
|
*/ |
|
public function negate(): MediaQuery |
|
{ |
|
return new Negation($this); |
|
} |
|
|
|
/** |
|
* Convert to Negation Normal Form (negations only applied to atoms) |
|
*/ |
|
abstract protected function to_nnf(): MediaQuery; |
|
|
|
/** |
|
* Convert to Disjunctive Normal Form |
|
*/ |
|
abstract protected function to_dnf(): MediaQuery; |
|
|
|
/** |
|
* Serialize to string (normalizes to DNF first) |
|
*/ |
|
public function __toString() |
|
{ |
|
return $this->to_dnf()->serialize(); |
|
} |
|
|
|
/** |
|
* Internal serialization method (called after normalization) |
|
*/ |
|
protected function serialize(): string |
|
{ |
|
return ''; |
|
} |
|
} |
|
|
|
/** |
|
* Represents a single atomic media query (e.g., "min-width: 400px") |
|
*/ |
|
class Atom extends MediaQuery |
|
{ |
|
private readonly string $query; |
|
|
|
public function __construct(string $query) |
|
{ |
|
$this->query = $query; |
|
} |
|
|
|
protected function to_nnf(): MediaQuery |
|
{ |
|
return $this; |
|
} |
|
|
|
protected function to_dnf(): MediaQuery |
|
{ |
|
return $this; |
|
} |
|
|
|
protected function serialize(): string |
|
{ |
|
return '(' . $this->query . ')'; |
|
} |
|
} |
|
|
|
/** |
|
* Represents a negated media query (e.g., "not (min-width: 400px)") |
|
*/ |
|
class Negation extends MediaQuery |
|
{ |
|
private readonly MediaQuery $query; |
|
|
|
public function __construct(MediaQuery $query) |
|
{ |
|
$this->query = $query; |
|
} |
|
|
|
protected function to_nnf(): MediaQuery |
|
{ |
|
if ($this->query instanceof Atom) { |
|
return $this; |
|
} |
|
|
|
if ($this->query instanceof Negation) { |
|
$result = $this->query->query->to_nnf(); |
|
return $result; |
|
} |
|
|
|
if ($this->query instanceof Conjunction) { |
|
$left = new Negation($this->query->left); |
|
$left = $left->to_nnf(); |
|
$right = new Negation($this->query->right); |
|
$right = $right->to_nnf(); |
|
$disj = new Disjunction($left, $right); |
|
return $disj->to_nnf(); |
|
} |
|
|
|
if ($this->query instanceof Disjunction) { |
|
$left = new Negation($this->query->left); |
|
$left = $left->to_nnf(); |
|
$right = new Negation($this->query->right); |
|
$right = $right->to_nnf(); |
|
$conj = new Conjunction($left, $right); |
|
return $conj->to_nnf(); |
|
} |
|
|
|
return $this; |
|
} |
|
|
|
protected function to_dnf(): MediaQuery |
|
{ |
|
$nnf = $this->to_nnf(); |
|
if ($nnf instanceof Negation) { |
|
return $nnf; |
|
} |
|
return $nnf->to_dnf(); |
|
} |
|
|
|
protected function serialize(): string |
|
{ |
|
return "(not {$this->query})"; |
|
} |
|
} |
|
|
|
/** |
|
* Represents a conjunction of media queries (e.g., "A and B and C") |
|
*/ |
|
class Conjunction extends MediaQuery |
|
{ |
|
public readonly MediaQuery $left; |
|
public readonly MediaQuery $right; |
|
|
|
public function __construct(MediaQuery $left, MediaQuery $right) |
|
{ |
|
$this->left = $left; |
|
$this->right = $right; |
|
} |
|
|
|
protected function to_nnf(): MediaQuery |
|
{ |
|
$left = $this->left->to_nnf(); |
|
$right = $this->right->to_nnf(); |
|
return new Conjunction($left, $right); |
|
} |
|
|
|
protected function to_dnf(): MediaQuery |
|
{ |
|
$left = $this->left->to_dnf(); |
|
$right = $this->right->to_dnf(); |
|
|
|
if ($left instanceof Disjunction) { |
|
$c1 = new Conjunction($left->left, $right); |
|
$c2 = new Conjunction($left->right, $right); |
|
$disj = new Disjunction($c1, $c2); |
|
return $disj->to_dnf(); |
|
} |
|
|
|
if ($right instanceof Disjunction) { |
|
$c1 = new Conjunction($left, $right->left); |
|
$c2 = new Conjunction($left, $right->right); |
|
$disj = new Disjunction($c1, $c2); |
|
return $disj->to_dnf(); |
|
} |
|
|
|
return new Conjunction($left, $right); |
|
} |
|
|
|
protected function serialize(): string |
|
{ |
|
return $this->left . ' and ' . $this->right; |
|
} |
|
} |
|
|
|
/** |
|
* Represents a disjunction of media queries (e.g., "A, B, C" - comma-separated OR) |
|
*/ |
|
class Disjunction extends MediaQuery |
|
{ |
|
public readonly MediaQuery $left; |
|
public readonly MediaQuery $right; |
|
|
|
public function __construct(MediaQuery $left, MediaQuery $right) |
|
{ |
|
$this->left = $left; |
|
$this->right = $right; |
|
} |
|
|
|
protected function to_nnf(): MediaQuery |
|
{ |
|
$left = $this->left->to_nnf(); |
|
$right = $this->right->to_nnf(); |
|
return new Disjunction($left, $right); |
|
} |
|
|
|
protected function to_dnf(): MediaQuery |
|
{ |
|
$left = $this->left->to_dnf(); |
|
$right = $this->right->to_dnf(); |
|
return new Disjunction($left, $right); |
|
} |
|
|
|
protected function serialize(): string |
|
{ |
|
return $this->left . ', ' . $this->right; |
|
} |
|
} |
|
|
|
/** |
|
* Factory function to create a media query atom |
|
*/ |
|
function mq(string $query): MediaQuery |
|
{ |
|
return new Atom($query); |
|
} |