From 6f5a7367c80007ec165ebfc775b6bc544d389140 Mon Sep 17 00:00:00 2001 From: npghub <69086331+npghub@users.noreply.github.com> Date: Mon, 6 Oct 2025 17:31:42 +0300 Subject: [PATCH] Catchup with upstream (#6) * [TASK] Extract value parsing functional tests (part 1) (#1084) In the tests, we should test parsing and rendering separately as this makes debugging test failures a lot easier. Part of #1057 * [TASK] Drop special support for vendor prefixes (#1083) In the past, vendor prefixes like `-moz-` or `-webkit-` were used for experimental CSS features in browsers. Nowadays, the browsers use features for this instead. Hence, special support for vendor prefixes is no longer needed. https://developer.mozilla.org/en-US/docs/Glossary/Vendor_Prefix * [CLEANUP] Avoid Hungarian notation for `size` (#1085) Part of #756 * [CLEANUP] Avoid Hungarian notation for `unit` and `sizeUnits` (#1088) Part of #756 * [CLEANUP] Avoid Hungarian notation for `isColorComponent` (#1090) Part of #756 * [CLEANUP] Avoid Hungarian notation in `Size::render()` (#1092) Part of #756 * [CLEANUP] Avoid Hungarian notation for `url` (#1093) Part of #756 * [CLEANUP] Avoid Hungarian notation for `tokenLength` (#1089) Part of #756 * [CLEANUP] Avoid Hungarian notation for `character` (#1094) Part of #756 * [CLEANUP] Avoid Hungarian notation for `sizeUnit` (#1095) Part of #756 * [CLEANUP] Avoid Hungarian notation for `useUrl` (#1096) Part of #756 * [CLEANUP] Avoid Hungarian notation in `OutputFormatter` (#1097) Part of #756 * [CLEANUP] Avoid Hungarian notation in `OutputFormat` (part 1) (#1098) Part of #756 * [CLEANUP] Improve some `OutputFormat` property and getter names (#1099) As suggested in #1098. The setters are not changed as those are part of the public API. * [CLEANUP] Avoid Hungarian notation in the tests (#1101) Part of #756 Co-authored-by: JakeQZ * [CLEANUP] Avoid Hungarian notation in `SpecificityCalculator` (#1102) Part of #756 * [CLEANUP] Avoid Hungarian notation in `CSSBlockList` (#1104) Part of #756 * [CLEANUP] Avoid Hungarian notation in `DeclarationBlock` (#1105) Part of #756 * [CLEANUP] Use the explicit `OutputFormat` setters in the tests (#1106) The `set()` method will be removed soon. Also unify the tests a bit. Part of #1103 * [TASK] Remove `OutputFormat::get()` (#1108) We now have beautiful, cleanly-typed getters to use instead. Part of #1103 * [CLEANUP] Avoid Hungarian notation in comments (#1109) Part of #756 * [TASK ] Remove `OutputFormat::set()` (#1110) Part of #1103 Closes #1103 * [CLEANUP] Avoid Hungarian notation in `OutputFormat` (part 2) (#1100) Part of #756 * [CLEANUP] Avoid Hungarian notation in `Size` (#1111) Part of #756 * [CLEANUP] Avoid Hungarian notation for `numberOfLines` (#1112) Part of #756 * [CLEANUP] Avoid Hungarian notation for `matches` (#1113) Part of #756 * [CLEANUP] Avoid Hungarian notation in `OutputFormat` (part 3) (#1114) Part of #756 * [CLEANUP] Avoid Hungarian notation in `OutputFormat` (part 4) (#1115) Also rename the (internal) getters to match the changed property names. Part of #756 * [CLEANUP] Avoid Hungarian notation for `input` (#1116) Part of #756 * [CLEANUP] Avoid Hungarian notation in `OutputFormat` (part 5) (#1117) Part of #756 * [CLEANUP] Avoid Hungarian notation in `OutputFormat` (part 6) (#1118) Part of #756 * [CLEANUP] Avoid Hungarian notation for `expression` (#1119) Part of #756 * [CLEANUP] Avoid Hungarian notation for `peek` (#1120) Part of #756 * [CLEANUP] Avoid Hungarian notation in `OutputFormat` (part 7) (#1121) Also rename the getters to match the new property names. Part of #756 * [CLEANUP] Avoid Hungarian notation for `maximumLength` (#1122) Part of #756 * [CLEANUP] Avoid Hungarian notation for `comment` (#1123) Part of #756 * [CLEANUP] Avoid Hungarian notation for `stopCharacters` (#1124) Part of #756 * [CLEANUP] Avoid Hungarian notation in `OutputFormat` (part 8) (#1125) Also rename the getters to match the new property names. Part of #756 * [CLEANUP] Avoid Hungarian notation in `OutputFormat` (part 9) (#1126) Also rename the getters to match the new property names. Part of #756 * [CLEANUP] Improve some variable names in `ParserState` (#1129) * [CLEANUP] Fix some type annotations in `ParserState` (#1130) Also make the parameter names non-Hungarian. Part of #756 Co-authored-by: JakeQZ * [BUGFIX] Fix a typo in `indentationLevel` (#1132) * [TASK] Mark `OutputFormat` as not extendable (#1131) Also mark the constructor as `@internal`. Co-authored-by: JakeQZ * [TASK] Make `OutputFormat` `final` (#1128) This class is not intended to be extended, and it's intended to be created using the factory methods anyway. (Also, we most probably won't need to have mocks of this class in the tests as it's mostly a data object.) * [CLEANUP] Drop empty `OutputFormat` constructor (#1127) * [TASK] Use native type declarations for `$lineNumber` (#1134) Part of #811 Co-authored-by: JakeQZ * [TASK] Add some rendering tests for `Document` (#1138) Part of #757 * [TASK] Add some more type declarations (#1139) These are some random changes the Rector would do as a result of the changes of some open PRs. * [TASK] Use native type declarations in `Document` (#1137) Part of #811 * [TASK] Use native type declarations in `ParserState` (#1136) Part of #811 Co-authored-by: JakeQZ * [TASK] Use native type declarations in `Parser` (#1140) Part of #811 * [TASK] Use native type declarations in `Anchor` (#1141) Part of #811 * [CLEANUP] Make a type annotation more specific (#1143) * [CLEANUP] Drop an unused internal constant (#1144) * [TASK] Initialize `KeyFrame` properties (#1146) They fortunately have obvious default values. This change means it can be enforced that they are always non-empty strings. Type declarations have been updated to reflect that. * [TASK] Add native type declarations for `atRuleName()` (#1145) Part of #811 * [CLEANUP] Avoid magic method forwarding in `AtRuleBlockList` (#1148) Part of #1147 * [CLEANUP] Avoid magic method forwarding in `ValueList` (#1149) Part of #1147 * [CLEANUP] Avoid magic method forwarding in `Rule` (#1150) Part of #1147 * [TASK] Drop `atRuleArgs()` from the `AtRule` interface (#1142) The classes implementing this interface use a wide variaty of inconsistent return types when they implement this method. This is a hard blocker for introducing native return type declaration for this method. Co-authored-by: JakeQZ * [CLEANUP] Avoid magic method forwarding in `CSSList` (#1151) Part of #1147 * [CLEANUP] Avoid magic method forwarding in `Color` (#1152) Part of #1147 * [CLEANUP] Avoid magic method forwarding in `AtRuleSet` (#1153) Part of #1147 * [CLEANUP] Avoid magic method forwarding in `Document` (#1155) Part of #1147 * [CLEANUP] Avoid magic method forwarding in `Import` (#1156) Part of #1147 * [CLEANUP] Avoid magic method forwarding in `CalcRuleValueList` (#1157) Part of #1147 * [DOCS] Avoid Hungarian notation in the README (#1154) Co-authored-by: JakeQZ * [CLEANUP] Avoid magic method forwarding in `KeyFrame` (#1158) Part of #1147 * [CLEANUP] Avoid magic method forwarding in `RuleSet` (#1159) Part of #1147 * [CLEANUP] Avoid magic method forwarding in `Charset` (#1160) Part of #1147 * [CLEANUP] Avoid magic method forwarding in `DeclarationBlock` (#1161) Part of #1147 * [TASK] Add native type declarations in `Size` (#1162) Part of #811 * [TASK] Add more unit tests for `Size` (#1165) Part of #757 * [TASK] Add some basic unit tests for `URL` (#1164) Part of #757 Co-authored-by: JakeQZ * [TASK] Add native type declarations in `URL` (#1163) Part of #811 * [TASK] Add native return type for `render*` methods (#1166) Part of #811 * [TASK] Drop magic method forwarding in `OutputFormat` (#898) * [CLEANUP] Fix method name casing in a call (#1167) * [BUGFIX] Include comments for all rules in declaration block (#1169) - `Rule::parse()` will no longer consume anything after the semicolon terminating the rule - it does not belong to that rule; - The whitespace and comments before a rule will be processed by `RuleSet::parseRuleSet()` and passed as a parameter to `Rule::parse()` - - This is only required while 'strict mode' parsing is an option, to avoid having an exception thrown during normal operation (i.e. when `Rule::parse()` encounters normal `}` as opposed to some other junk, which is not distinguished). Fixes #173. See also #672, #741. * [CLEANUP] Clean up `ParserState` a bit (#1173) * [TASK] Add native type declarations for `Import` (#1172) Part of #811 * [BUGFIX] Fix the return type of `Selector::isValid()` (#1174) Fixes #1043 Part of #811 * [CLEANUP] Make `Commentable` type annotations more specific (#1171) * [TASK] Add native type declarations for `Charset` (#1178) Part of #811 * [TASK] Add native type declarations for `CSSString` (#1179) Part of #811 * [TASK] Avoid the deprecated `__toString()` in tests (#1180) Moving some tests to functional tests and splitting them up will come in later changes for #1057. * [TASK] Remove `__toString()` (#1046) Closes #998 * [TASK] Add native type declarations for `CSSList` (#1181) Part of #811 * [CLEANUP] Fix type annotation of `::getSelectors()` (#1184) Also add a native type declaration. This will need more cleanup and refactoring later on. * [TASK] Add native type declarations for `CSSBlockList` (#1183) Also add some more type checks to ensure that the corresponding types are actually returned. Part of #811 * [TASK] Add native type declarations for `RuleSet` (#1186) Part of #811 * [TASK] Add native type declarations for `CSSNamespace` (#1187) Part of #811 * [TASK] Reduce and finetune the scope of `@covers` annotations (#1188) The legacy tests are not very focused. Until we have split them up, try to avoid false positives for code coverage. Also add `@covers` annotations for the parent classes of the tested classes. * [TASK] Add native type declarations for `Rule` (#1190) Part of #811 * [TASK] Add native type declarations for `AtRuleSet` (#1192) Part of #811 * [TASK] Add native type declarations for `DeclarationBlock` (#1193) Part of #811 * [TASK] Add native type declarations for `ValueList` (#1196) Also polish some PHPDoc type annotations. Part of #811. * [TASK] Add native type declarations for `CSSFunction` (#1197) Also improve the related type annotations and declarations in other classes in order to keep things consistent and to keep Rector from changing things. * [CLEANUP] And some more annotations for non-empty strings (#1199) * [CLEANUP] Make annotations for `OutputFormat` more specific (#1200) * [TASK] Drop the unused `ParserState::strpos()` method (#1202) * [TASK] Add native type declarations for `RuleValueList` (#1203) Part of #811 * [CLEANUP] Improve type annotations in `LineName` (#1198) * [TASK] Add native type declarations for `Color` (#1204) Also make some types more specific. Also improve code formatting a bit. Part of #811 * [CLEANUP] Return `null` from `DeclarationBlock::parse()` on failure (#1209) Also add clarification of meaning of return value from `CSSList::parseListItem()`, where `null` and `false` have different meanings. Part of #1176. * [TASK] Add (and use) a `CSSListItem` type (#1212) This allows a single type to be used for the contents of a `CSSList`, instead of a long list of orred types, and helps with static analysis. Various `assertInstanceOf()` tests are added to the test cases to confirm that the list items are of the type expected. Some `implements` and `exetends` lists are now alphabetically sorted. Also don't implement interfaces extended by another that is also implemented PHP<7.4 does not allow this. Instead, for clarity, add a DocBlock comment stating which additional interfaces should be implemented that are not explicitly listed in the `implements` section. When our minimum PHP version becomes 7.4 or above, we can revisit this. * [TASK] Configure the target PHP version for PHPStan (#1216) This will help avoid it suggesting things that are only possile in later PHP versions. Fixes #1214 * [TASK] Add trait providing standard implementation of `Commentable` (#1206) Part of #813. * [TASK] Use `CommentContainer` trait to implement `Commentable` (#1217) Closes #813. * [TASK] Prevent Dependabot updating "rawr/cross-data-providers" (#1219) Version 3.0 of this package is not compatible. * [TASK] Add rebasing guidelines to `CONTRIBUTING.md` (#1220) This section is copied directly from the sister project, Emogrifier. Resolves #1215. * [TASK] Integrate changelog entries from 8.8 release (#1222) (And one that was missed from 8.7.) * [TASK] Add to UML diagram that `Selector` implements `Renderable` (#1224) Followup to #1017 * [TASK] Add `Positionable` interface and implementing trait (#1221) This is for CSS items which have a position in the document. New methods are added: - `getLineNumber` to replace `getLineNo`; - `getColumnNumber` to replace `getColNo`. These return a nullable `int`, instead of using zero to indicate absence. The old methods are now deprecated, but defined in the interface and implemented in the trait. Note that this change only adds the interface and trait. It does not modify any classes to actually implement or use these. Part of #1207. * [TASK] Update class diagram to include `CSSListItem` (#1226) Follow-up to #1212. * [CLEANUP] Autoformat the code and drop unused imports (#1228) * [CLEANUP] Fix typos in test method names (#1229) * [TASK] Migrate to `rawr/phpunit-data-provider` (#1227) The package `rawr/cross-data-providers` that we used has been abandoned and should not be used anymore. * [TASK] Implement `Positionable` (#1225) Closes #1207. * [TASK] Deprecate `getLineNo()` etc. in v8.9.0 (#1233) The deprecation and changes from #1207 can be merged to 8.x. See #1232. So the deprecation can be brought forward. * [TASK] Update CHANGELOG for #1233 (#1235) * [BUGFIX] Correct `AtRuleBlockListTest::implementsAtRule()` (#1238) Use the interface type to be tested for in the assertion, not the type of the object itself. * [TASK] Add and implement `CSSElement` interface (#1231) Also add tests to confirm that the supplanted types in the DocBlock actually implement the new interface. And correct a DocBlock type to also allow `string`, which is currently possible. cf. #1230 * [TASK] Add `assertInstanceOf` tests for `CSSListItem` (#1237) These should probably have been added as part of #1212. They confirm that the various types supplanted by `CSSListItem` in the API all implement the new interface. Resolves #1236. * [CLEANUP] Remove `CSSBlockList::allDeclarationBlocks()` (#1239) Change the one remaining usage instance to use `getAllDeclarationBlocks()`, which was refactored in #990. Part of #994. * [TASK] Move `getAllValues()` to `CSSBlockList` (#1240) Also add unit tests for this method. Part of #994. Relates to #1230. * [TASK] Deconflate `getAllValues()` parameters (#1241) The `$element` parameter was overloaded with a dual purpose. A second separate parameter has been added for rule filtering, which is not actually mutually exclusive with CSS subtree selection. Since `getAllValues()` is part of the public API, the method now supports being called with the old or new signatures, with the old signature being deprecated. Once the deprecation has been included in the 8.x release branch, the messiness of supporting the previous API can be removed. Part of #994. Also relates to #1230. * [TASK] Remove original `getAllValues()` API (#1243) The method still exists with the same (slightly improved) functionality, but the optional arguments have been refactored, and may require changes. Part of #994. Closes #1230. * [TASK] Refactor `getAllValues()` (#1244) Move functionality from `allValues()` directly into to `getAllValues()`, so as to avoid passing array by reference, removing `allValues()`. Avoid making the recursive call for nested items that are not `CSSElement`s. Part of #994. Closes #1230. * [CLEANUP] `allSelectors()` -> `getAllSelectors()` (#1245) The renamed (internal) method now returns the result, instead of having a reference parameter for it. Closes #994. * [CLEANUP] Avoid negated non-Boolean in `RuleSet` (#1246) Use `=== null` instead to be more precise. * [TASK] Deprecate passing `Rule` to `RuleSet::getRules()` (#1248) And also `getRulesAssoc()`. Relates to #1247. * [Dependabot] Update phpunit/phpunit requirement from 8.5.41 to 8.5.42 (#1250) Updates the requirements on [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit) to permit the latest version. - [Release notes](https://github.com/sebastianbergmann/phpunit/releases) - [Changelog](https://github.com/sebastianbergmann/phpunit/blob/8.5.42/ChangeLog-8.5.md) - [Commits](https://github.com/sebastianbergmann/phpunit/compare/8.5.41...8.5.42) --- updated-dependencies: - dependency-name: phpunit/phpunit dependency-version: 8.5.42 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [TASK] Add separate methods for `RuleSet::removeRule()`, with deprecation (#1249) Passing `string` or `null` to `removeRule()` is deprecated. The new method handles that functionality. Relates to #1247. * [CLEANUP] Use `RuleSet::removeMatchingRules()` in tests (#1254) Now this method has been added, using it appropriately in the tests, rather than the deprecated functionality, will eliminate a PHPStan warning. Note that PHPStan seems to erroneously place the warning against the callee rather than the caller. * [TASK] Drop allowing `Rule` to be passed to `RuleSet::getRules()` (#1253) ... and `getRulesAssoc()`. Relates to #1247. * [TASK] Allow only `Rule` to be passed to `RuleSet::removeRule()` (#1255) Relates to #1247. * [CLEANUP] Check for `Positionable` in `lineNumbersParsing()` test (#1257) Now the interface has been added, it is no longer necessary to check the object is one of a long list of types. Also use the new `getLineNumber()` instead of the deprecated `getLineNo()`. * [TASK] Add interface `RuleContainer` for `RuleSet` (#1256) This covers the maniplation of `Rule`s within the container, and may be implemented by other classes in future (e.g. #1194). Note that the naming is consistent with the current codebase, rather than what the CSS entities are now called: - `Rule` represents what is now called a "declaration"; - `RuleSet` represents what is now called a "declaration block"; - `DeclarationBlock` represents what is now called a "style rule"; - `CSSListItem` (closely) represents what is now generically called a "rule". Renaming things is part of a longer-term plan touched on in #1189. * [TASK] Have `setPosition()` implement fluent interface (#1259) This will aid writing tests for `RuleSet`. Note that the PHP type annotation cannot be `self` because an interface is involved and PHP 7 is still supported. Part of #974. * [BUGFIX] Don't return objects from data providers (#1260) The same objects may be provided to multiple tests. If a test manipulates an object, it will no longer be in the initial expected state for other tests. * [TASK] Add tests for `RuleSet::addRule()` without sibling argument (#1261) Some are currently skipped, pending some minor bug fixes. * [BUGFIX] Ensure column number set after `RuleSet::addRule()` (#1263) Note that this bug (or inconsistency) only occurs following the addtion of `getColumnNumber()` returning a nullable `int` (#1221 and #1225). These changes are not yet included in any release. Part of #974. * [BUGFIX] Ensure valid position for first `Rule` added to `RuleSet` (#1262) Part of #974. * [BUGFIX] Set line number for `AddRule()` with only column number (#1265) Continue to preserve the column number. Also tighten the test to confirm the `Rule` is added at the end. Note that the reason for `markTestSkipped()` was incorrect - the line number was not being set at all. Part of #974. * [TASK] Add tests for `addRule()` with a sibling (#1266) Some tests are skipped pending bug fixes. Part of #974. * [BUGFIX] Ensure non-negative column number in `RuleSet` (#1268) When inserting a `Rule` before a sibling, increment the column number of other `Rule`s, instead of assigning a lower column number. Part of #974. * [CLEANUP] Extract method `RuleSet::comparePositionable` (#1272) As well as being used with `usort()`, it may have other uses. The deprecated `getLineNo()` and `getColNo()` are still used for now. Replacing these will be done separately. Relates to #974. * [BUGFIX] `AddRule` before sibling with different property name (#1270) Part of #974. * [TASK] Add unit tests for `RuleSet::removeRule` (#1273) This re-uses some data providers, which have been renamed to reflect their more generic usage. Also, the PHPDoc type has been tightened to `non-empty-list` where applicable. Part of #974. * [CLEANUP] Separate some test methods in `RuleSetTest` (#1274) These were previously testing more than one aspect of a `RuleSet` method. Now each behaviour has a dedicated test method (albeit with some duplication of the set-up). Also added an additional assertion when there are no expected remaining items following removal, so that an assertion is made. * [DOCS] Switch to the new CoC email address (#1275) * [TASK] Add unit tests for `RuleSet::setRules` (#1276) Also rename a data provider to indicate its (now) more generic purpose. * [TASK] Add unit tests for `RuleSet::getRules` (#1277) Part of #974. * [TASK] Add tests for `RuleSet::getRules` with `$searchPattern` (#1278) Part of #974. * [TASK] Add unit tests for `RuleSet::getRulesAssoc` (#1279) Part of #974. * [CLEANUP] Split data provider for search pattern (#1281) A separate data provider now provides patterns which don't match any property names, and a separate test caters for the non-matching situation. * [TASK] Add tests for `RuleSet::getRulesAssoc` with `$searchPattern` (#1280) Part of #974. * [CLEANUP] Streamline tests for `getRules` with matching pattern (#1282) Combine two tests into one, by asserting an exact set match, instead of two-way subset matches. * [TASK] Update `RuleSet::addRule` to use `getLineNumber` (#1284) Part of #974 * [TASK] Update `RuleSet::comparePositionable` to use new methods (#1283) `getLineNo` and `getColNo` are deprecated. When the titled method was extracted, use of the above-mentioned methods was retained to ease backporting and transition to their replacement counterparts: `getLineNumber` and `getColumnNumber`, which differ by returning `null` in the case of 'not set'. This replaces all instances of calls to `getColNo`. Part of #974 * [TASK] Add tests for `getLineNumber` (#1286) These correspond to the existing tests for `getLineNo` for classes that implement `Positionable`. Also correct an existing test method name to refer to `getLineNo`. * [TASK] Use `getLineNumber` in `ParserTest` (#1285) `getLineNo` is deprecated and will be removed. Part of #974 * [TASK] Remove `getColNo()` (#1287) Note that the removed tests are in `UnitDeprecated`. Equivalent tests already exist for the replacement `getColumnNumber()`. Part of #974 * [TASK] Set line number to `null` by default (#1288) No longer allow or support `0` as a default line or column number. Part of #974 * [TASK] Remove `getLineNo()` (#1258) Closes #974. * [BUGFIX] Exclude absent line number from exception message (#1290) The bug was introduced by #1288, so has not been included in any release; thus a changelog entry is not justified. * [TASK] Add `RuleContainerTest` trait, use in `RuleSetTest` (#1291) The trait provides tests for classes implementing `RuleContainer`. The test methods and data providers have been moved verbatim to the trait from `RuleSetTest`. Will be needed for #1194 * [BUGFIX] Allow comma in selectors (#1293) Also add a note that the specificity is incorrectly calculated in such cases. This will be addressed with a separate fix. * [CLEANUP] Tidy up `DeclarationBlock::parse()` (#1294) - Assign the result of `ParserState::peek()` to a local variable, for efficiency; - Use a switch statement to branch on its value, for extensibility (e.g. #1292); - Don't unnecessarily test that a quote character is not escaped when not within a string. * [TASK] Add `assertInstanceOf` tests for `DeclarationBlock` (#1295) * [BUGFIX] Correct an exception message (#1296) This correction is not worthy of a changelog entry or any tests. * [CLEANUP] Avoid spaces before colons in the Mermaid code (#1300) * [CLEANUP] Order class names in the class diagram alphabetically (#1299) * [DOCS] Fix class list in class diagram (#1301) - adapt the sorting to how the diagram generator sorts - add `SpecificityCalculator` which had been missing * [DOCS] Remove cardinalities from the class diagram (#1303) This brings our class diagram closer to what the latest version of the diagram generator creates. * [DOCS] Make the class diagram markers generator-friendly (#1302) This is in preparation for adding a script for generating the class diagram in #1297. * [DOCS] Change some arrow types in the class diagram (#1304) * [DOCS] Label dependencies in the class diagram as such (#1305) This corresponds to what the latest version of our diagram generator does. * [DOCS] Temporarily drop some markers from the class diagram (#1307) As long as we manually edit the source of the class diagram, having markers for a part being autogenerated does not make sense. * [DOCS] Reorder some lines in the class diagram (part 1) (#1306) This is in preparation for #1298. * [DOCS] Reorder some lines in the class diagram (part 2) (#1308) This is in preparation for #1298. * [DOCS] Reorder some lines in the class diagram (part 3) (#1309) This is in preparation for #1298. * [DOCS] Reorder some lines in the class diagram (part 4) (#1310) This is in preparation for #1298. * [DOCS] Reorder some lines in the class diagram (part 5) (#1311) This is in preparation for #1298. * [DOCS] Reorder some lines in the class diagram (part 6) (#1312) This is in preparation for #1298. * [DOCS] Reorder some lines in the class diagram (part 7) (#1313) This is in preparation for #1298. * [DOCS] Reorder some lines in the class diagram (part 8) (#1314) This is in preparation for #1298. * [DOCS] Reorder some lines in the class diagram (part 9) (#1315) This is in preparation for #1298. * [DOCS] Reorder some lines in the class diagram (part 10) (#1316) This is in preparation for #1298. * [DOCS] Reorder some lines in the class diagram (part 11) (#1317) This is in preparation for #1298. * [DOCS] Reorder some lines in the class diagram (part 12) (#1318) This is in preparation for #1298. * [DOCS] Reorder some lines in the class diagram (part 13) (#1319) This is in preparation for #1298. * [CLEANUP] Use static variable for stop characters (#1321) ... in `DeclarationBlock::parse()`. This improves readability in preparation for #1292 which will extend the list. (It may also slightly improve performance.) * [TASK] Add unit tests for selector parsing (#1322) The tests broadly cover what currently works, and will be extended to cover the fixes in #1292. * [BUGFIX] Allow comma in quoted string in selector (#1323) Split by commas during parsing, not after. * [BUGFIX] Allow comma-separated arguments in selectors (#1292) Fixes #138. Fixes #360. Fixes #1289. * [TASK] Update the development tools (#786) * [DOCS] Integrate the 8.9.0 changelog into main changelog (#1329) This way, the upcoming 9.0.0 release won't have changes in the changelog that already are part of the 8.9.0 changelog. * [TASK] Raise PHPStan to level 4 (#1201) Also allow `assertInstanceOf` checks in the tests (as we find those useful). * [TASK] Use strict equality (#1331) One instance is left out, but is covered by #1330. * [TASK] Make Boolean tests explicit (#1332) * [CLEANUP] Remove superfluous Rector rule (#1333) Since #1201, this becomes a duplicate. * [TASK] Add tests for `removeDeclarationBlockBySelector()` (#1335) * [CLEANUP] Avoid use of short-ternary operator (#1336) (I share a birthday with Elvis, but needs must.) * [CLEANUP] Remove impossible conditional (#1337) The parameter `$parserState` is specified to be a `ParserState`, so it can never be a string. * [BUGFIX] Correct DocBlock for `ParserState::consumeUntil()` (#1338) The special `EOF` constant is actually defined as `null`, so the stop characters may be strings or `null`. * [BUGFIX] Provide the authentication token for PHIVE on CI (#1340) This will hopefully avoid the 403 errors when installing the PHIVE packages. Fixes #1339 * [TASK] Make `RuleSet` concrete (#1341) ... adding internal `render` method. Precursor to #1194. * [CLEANUP] Update `RuleSet` DocBlock (#1343) ... to be consistent with the class now being concrete. Missed in #1341. * [CLEANUP] Reorder `use`s in `DeclarationBlockTest` (#1344) Order alphabetically. * [BUGFIX] Remove trailing semicolon with compact format (#1345) Fixes #1342. * [CLEANUP] Use `getAllDeclarationBlocks` in `colorParsing` test (#1346) ... instead of `getAllRuleSets`. This avoids testing if `RuleSet`s are `DeclarationBlock`s, and will be needed for #1194. * [TASK] Add tests for `RuleSet` constructor (#1348) * [BUGFIX] Correct an `assert` added in #1348 (#1349) * [TASK] Add tests for `DeclarationBlock` constructor (#1350) The class extends `RuleSet`, but the constructor behaviour needs to be tested for each class. * [TASK] Test `RuleSet` constructor with `null` explicitly passed (#1351) * [BUGFIX] Update class diagram to show `RuleSet` as concrete (#1352) Missed in #1341. * [TASK] Use delegation for `DeclarationBlock` -> `RuleSet` (#1194) ... rather than inheritance. This will allow `DeclarationBlock` to instead extend `CSSBlockList` in order to support [CSS nesting](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting). This is a slightly-breaking change, since now `CSSBlockList::getAllRuleSets()` will include the `RuleSet` property of the `DeclarationBlock` instead of the `DeclarationBlock` itself. Part of #1170. * [TASK] Update the development tools (#1334) * [TASK] Prepare release of version 9.0.0 (#1328) Closes #1326 * [DOCS] Correct `}` to `)` in changelog (#1354) * [TASK] Raise PHPStan to level 5 (#1356) * [CLEANUP] Ignore warnings for explicitly invalid values in tests (#1358) * [Dependabot] Update phpunit/phpunit requirement from 8.5.42 to 8.5.43 (#1360) Updates the requirements on [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit) to permit the latest version. - [Release notes](https://github.com/sebastianbergmann/phpunit/releases) - [Changelog](https://github.com/sebastianbergmann/phpunit/blob/8.5.43/ChangeLog-8.5.md) - [Commits](https://github.com/sebastianbergmann/phpunit/compare/8.5.42...8.5.43) --- updated-dependencies: - dependency-name: phpunit/phpunit dependency-version: 8.5.43 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [Dependabot] Bump actions/checkout from 4 to 5 (#1361) Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [Dependabot] Update phpunit/phpunit requirement from 8.5.43 to 8.5.44 (#1362) Updates the requirements on [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit) to permit the latest version. - [Release notes](https://github.com/sebastianbergmann/phpunit/releases) - [Changelog](https://github.com/sebastianbergmann/phpunit/blob/8.5.44/ChangeLog-8.5.md) - [Commits](https://github.com/sebastianbergmann/phpunit/compare/8.5.43...8.5.44) --- updated-dependencies: - dependency-name: phpunit/phpunit dependency-version: 8.5.44 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [TASK] Raise PHPStan to level 6 (#1364) * [TASK] Add `thecodingmachine/safe` (#1453) (#1366) Safe-PHP https://github.com/thecodingmachine/safe provides rewrites of PHP functions to throw an exception instead of returning `false` when an error is encountered. This will allow us to drop out custom `preg_*` wrapper class and to increase type safety in our codebase. Also drop the PHP-CS-Fixer rule that adds a trailing backslash to calls to native PHP functions (as this would change the Safe-PHP calls back to their unsafe versions). The actual code changes will come in subsequent commits. Part of #1168 * [BUGFIX] Use the safe `file_get_contents` in `quickdump.php` (#1367) * [BUGFIX] Use the safe regexp functions in `CSSList` (#1368) * [CLEANUP] Avoid Hungarian notation in `quickdump.php` (#1369) * [BUGFIX] Use the safe regexp functions in `ParserState` (#1370) Part of #1168 * [BUGFIX] Improve selector validation performance (#1372) Avoid [catastrophic backtracking](https://www.regular-expressions.info/catastrophic.html) in selector validation regular expression by using possessive quantifier with mutually exclusive alternations. Also remove outdated description from DocBlock, but add description for extended class summarizing differences. * [BUGFIX] Use the safe regexp functions in `Selector` (#1371) Part of #1168 * [BUGFIX] Use safe `file_get_contents` in `LenientParsingTest` (#1373) Part of #1168 * [Dependabot] Update phpunit/phpunit requirement from 8.5.44 to 8.5.45 (#1375) Updates the requirements on [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit) to permit the latest version. - [Release notes](https://github.com/sebastianbergmann/phpunit/releases) - [Changelog](https://github.com/sebastianbergmann/phpunit/blob/8.5.45/ChangeLog-8.5.md) - [Commits](https://github.com/sebastianbergmann/phpunit/compare/8.5.44...8.5.45) --- updated-dependencies: - dependency-name: phpunit/phpunit dependency-version: 8.5.45 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [TASK] Update the development tools (#1353) * [FEATURE] Add support for PHP 8.5 (#1355) * [TASK] Prepare release of version 9.1.0 (#1376) * [Dependabot] Update phpunit/phpunit requirement from 8.5.45 to 8.5.46 (#1377) Updates the requirements on [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit) to permit the latest version. - [Release notes](https://github.com/sebastianbergmann/phpunit/releases) - [Changelog](https://github.com/sebastianbergmann/phpunit/blob/8.5.46/ChangeLog-8.5.md) - [Commits](https://github.com/sebastianbergmann/phpunit/compare/8.5.45...8.5.46) --- updated-dependencies: - dependency-name: phpunit/phpunit dependency-version: 8.5.46 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [BUGFIX] Use safe file functions in `ParserTest` (#1378) Part of #1168 * [BUGFIX] Use safe preg functions in `Value` (#1379) Also use typesafe comparisons in the affected line. Part of #1168 * [BUGFIX] Use safe preg functions in `Size` (#1380) Also use typesafe comparisons in the affected line. Part of #1168 * [BUGFIX] Use safe preg functions in `CSSString` (#1382) Part of #1168 * [BUGFIX] Use safe preg functions in `Rule` (#1383) Part of #1168 * [BUGFIX] Use safe preg functions in `SpecificityCalculator` (#1384) Part of #1168 * [TASK] Add PHPStan rules for Safe-PHP (#1385) This will prevent unsafe function usage from getting added. Closes #1168 * [Dependabot] Update phpunit/phpunit requirement from 8.5.46 to 8.5.47 (#1386) Updates the requirements on [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit) to permit the latest version. - [Release notes](https://github.com/sebastianbergmann/phpunit/releases) - [Changelog](https://github.com/sebastianbergmann/phpunit/blob/8.5.47/ChangeLog-8.5.md) - [Commits](https://github.com/sebastianbergmann/phpunit/compare/8.5.46...8.5.47) --- updated-dependencies: - dependency-name: phpunit/phpunit dependency-version: 8.5.47 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [Dependabot] Update phpunit/phpunit requirement from 8.5.47 to 8.5.48 (#1387) Updates the requirements on [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit) to permit the latest version. - [Release notes](https://github.com/sebastianbergmann/phpunit/releases) - [Changelog](https://github.com/sebastianbergmann/phpunit/blob/8.5.48/ChangeLog-8.5.md) - [Commits](https://github.com/sebastianbergmann/phpunit/compare/8.5.47...8.5.48) --- updated-dependencies: - dependency-name: phpunit/phpunit dependency-version: 8.5.48 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] Co-authored-by: Oliver Klee Co-authored-by: JakeQZ Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .gitattributes | 2 + .github/CONTRIBUTING.md | 12 - .github/dependabot.yml | 13 + .github/workflows/ci.yml | 23 +- .github/workflows/codecoverage.yml | 23 +- .gitignore | 1 + .phive/phars.xml | 3 +- CHANGELOG.md | 361 ++++- CODE_OF_CONDUCT.md | 141 +- CONTRIBUTING.md | 196 +++ README.md | 473 ++++--- bin/quickdump.php | 18 +- composer.json | 50 +- config/php-cs-fixer.php | 10 +- config/phpstan-baseline.neon | 111 +- config/phpstan.neon | 20 +- config/rector.php | 30 +- docs/API-and-deprecation-policy.md | 52 + docs/release-checklist.md | 15 + phpunit.xml | 5 +- src/CSSElement.php | 17 + src/CSSList/AtRuleBlockList.php | 65 +- src/CSSList/CSSBlockList.php | 191 +-- src/CSSList/CSSList.php | 439 +++--- src/CSSList/CSSListItem.php | 18 + src/CSSList/Document.php | 120 +- src/CSSList/KeyFrame.php | 65 +- src/Comment/Comment.php | 54 +- src/Comment/CommentContainer.php | 44 + src/Comment/Commentable.php | 21 +- src/OutputFormat.php | 684 ++++++++-- src/OutputFormatter.php | 200 +-- src/Parser.php | 38 +- src/Parsing/Anchor.php | 24 +- src/Parsing/OutputException.php | 14 +- src/Parsing/ParserState.php | 452 +++---- src/Parsing/SourceException.php | 33 +- src/Parsing/UnexpectedEOFException.php | 4 +- src/Parsing/UnexpectedTokenException.php | 50 +- src/Position/Position.php | 55 + src/Position/Positionable.php | 31 + src/Property/AtRule.php | 33 +- src/Property/CSSNamespace.php | 121 +- src/Property/Charset.php | 104 +- src/Property/Import.php | 115 +- src/Property/KeyframeSelector.php | 48 +- src/Property/Selector.php | 145 +- .../Selector/SpecificityCalculator.php | 87 ++ src/Renderable.php | 17 +- src/Rule/Rule.php | 290 ++-- src/RuleSet/AtRuleSet.php | 61 +- src/RuleSet/DeclarationBlock.php | 827 +++--------- src/RuleSet/RuleContainer.php | 36 + src/RuleSet/RuleSet.php | 396 +++--- src/Settings.php | 74 +- src/Value/CSSFunction.php | 88 +- src/Value/CSSString.php | 91 +- src/Value/CalcFunction.php | 94 +- src/Value/CalcRuleValueList.php | 15 +- src/Value/Color.php | 432 ++++-- src/Value/LineName.php | 46 +- src/Value/PrimitiveValue.php | 13 +- src/Value/RuleValueList.php | 10 +- src/Value/Size.php | 176 +-- src/Value/URL.php | 71 +- src/Value/Value.php | 222 ++- src/Value/ValueList.php | 80 +- tests/CSSList/AtRuleBlockListTest.php | 66 +- tests/CSSList/DocumentTest.php | 143 -- tests/CSSList/KeyFrameTest.php | 49 - tests/Comment/CommentTest.php | 120 +- tests/Functional/CSSList/DocumentTest.php | 137 ++ tests/Functional/Comment/CommentTest.php | 67 + tests/Functional/ParserTest.php | 39 + tests/Functional/Property/SelectorTest.php | 59 + .../RuleSet/DeclarationBlockTest.php | 69 + tests/Functional/RuleSet/RuleSetTest.php | 119 ++ tests/Functional/Value/ValueTest.php | 45 + tests/FunctionalDeprecated/.gitkeep | 0 tests/OutputFormatTest.php | 123 +- tests/ParserTest.php | 932 +++++++------ tests/RuleSet/DeclarationBlockTest.php | 446 ++---- tests/RuleSet/LenientParsingTest.php | 85 +- tests/Unit/CSSList/AtRuleBlockListTest.php | 147 ++ tests/Unit/CSSList/CSSBlockListTest.php | 474 +++++++ tests/Unit/CSSList/CSSListTest.php | 329 +++++ tests/Unit/CSSList/DocumentTest.php | 66 + .../CSSList/Fixtures/ConcreteCSSBlockList.php | 21 + .../Unit/CSSList/Fixtures/ConcreteCSSList.php | 21 + tests/Unit/CSSList/KeyFrameTest.php | 111 ++ tests/Unit/Comment/CommentContainerTest.php | 236 ++++ tests/Unit/Comment/CommentTest.php | 80 ++ .../Fixtures/ConcreteCommentContainer.php | 13 + tests/Unit/OutputFormatTest.php | 1185 ++++++++++++++++ tests/Unit/OutputFormatterTest.php | 622 +++++++++ tests/Unit/Parsing/OutputExceptionTest.php | 76 ++ tests/Unit/Parsing/SourceExceptionTest.php | 78 ++ .../Parsing/UnexpectedEOFExceptionTest.php | 178 +++ .../Parsing/UnexpectedTokenExceptionTest.php | 178 +++ .../Position/Fixtures/ConcretePosition.php | 13 + tests/Unit/Position/PositionTest.php | 194 +++ tests/Unit/Property/CSSNamespaceTest.php | 34 + tests/Unit/Property/CharsetTest.php | 34 + tests/Unit/Property/ImportTest.php | 35 + .../Selector/SpecificityCalculatorTest.php | 94 ++ tests/Unit/Property/SelectorTest.php | 144 ++ tests/Unit/Rule/RuleTest.php | 73 + tests/Unit/RuleSet/AtRuleSetTest.php | 33 + tests/Unit/RuleSet/DeclarationBlockTest.php | 281 ++++ tests/Unit/RuleSet/RuleContainerTest.php | 1200 +++++++++++++++++ tests/Unit/RuleSet/RuleSetTest.php | 82 ++ tests/{ => Unit}/SettingsTest.php | 16 +- tests/Unit/Value/CSSStringTest.php | 83 ++ .../Value/CalcRuleValueListTest.php | 16 +- tests/Unit/Value/ColorTest.php | 474 +++++++ tests/Unit/Value/Fixtures/ConcreteValue.php | 19 + tests/Unit/Value/SizeTest.php | 101 ++ tests/Unit/Value/URLTest.php | 83 ++ tests/{ => Unit}/Value/ValueTest.php | 54 +- tests/UnitDeprecated/.gitkeep | 0 tests/Value/SizeTest.php | 51 - tests/fixtures/-fault-tolerance.css | 12 +- tests/fixtures/1readme.css | 4 +- tests/fixtures/2readme.css | 6 +- tests/fixtures/atrules.css | 22 +- tests/fixtures/calc.css | 4 +- tests/fixtures/case-insensitivity.css | 14 +- tests/fixtures/colortest.css | 34 +- tests/fixtures/create-shorthands.css | 6 - tests/fixtures/escaped-tokens.css | 6 +- tests/fixtures/expand-shorthands.css | 7 - tests/fixtures/ie-hacks.css | 9 - tests/fixtures/ie.css | 10 +- tests/fixtures/inner-color.css | 4 +- tests/fixtures/missing-property-value.css | 4 +- tests/fixtures/namespaces.css | 6 +- tests/fixtures/nested.css | 12 +- tests/fixtures/slashed.css | 4 +- tests/fixtures/specificity.css | 2 +- tests/fixtures/url.css | 4 +- tests/fixtures/values.css | 18 +- tests/fixtures/whitespace.css | 2 +- 142 files changed, 12478 insertions(+), 5010 deletions(-) delete mode 100644 .github/CONTRIBUTING.md create mode 100644 CONTRIBUTING.md create mode 100644 docs/API-and-deprecation-policy.md create mode 100644 docs/release-checklist.md create mode 100644 src/CSSElement.php create mode 100644 src/CSSList/CSSListItem.php create mode 100644 src/Comment/CommentContainer.php create mode 100644 src/Position/Position.php create mode 100644 src/Position/Positionable.php create mode 100644 src/Property/Selector/SpecificityCalculator.php create mode 100644 src/RuleSet/RuleContainer.php delete mode 100644 tests/CSSList/DocumentTest.php delete mode 100644 tests/CSSList/KeyFrameTest.php create mode 100644 tests/Functional/CSSList/DocumentTest.php create mode 100644 tests/Functional/Comment/CommentTest.php create mode 100644 tests/Functional/ParserTest.php create mode 100644 tests/Functional/Property/SelectorTest.php create mode 100644 tests/Functional/RuleSet/DeclarationBlockTest.php create mode 100644 tests/Functional/RuleSet/RuleSetTest.php create mode 100644 tests/Functional/Value/ValueTest.php create mode 100644 tests/FunctionalDeprecated/.gitkeep create mode 100644 tests/Unit/CSSList/AtRuleBlockListTest.php create mode 100644 tests/Unit/CSSList/CSSBlockListTest.php create mode 100644 tests/Unit/CSSList/CSSListTest.php create mode 100644 tests/Unit/CSSList/DocumentTest.php create mode 100644 tests/Unit/CSSList/Fixtures/ConcreteCSSBlockList.php create mode 100644 tests/Unit/CSSList/Fixtures/ConcreteCSSList.php create mode 100644 tests/Unit/CSSList/KeyFrameTest.php create mode 100644 tests/Unit/Comment/CommentContainerTest.php create mode 100644 tests/Unit/Comment/CommentTest.php create mode 100644 tests/Unit/Comment/Fixtures/ConcreteCommentContainer.php create mode 100644 tests/Unit/OutputFormatTest.php create mode 100644 tests/Unit/OutputFormatterTest.php create mode 100644 tests/Unit/Parsing/OutputExceptionTest.php create mode 100644 tests/Unit/Parsing/SourceExceptionTest.php create mode 100644 tests/Unit/Parsing/UnexpectedEOFExceptionTest.php create mode 100644 tests/Unit/Parsing/UnexpectedTokenExceptionTest.php create mode 100644 tests/Unit/Position/Fixtures/ConcretePosition.php create mode 100644 tests/Unit/Position/PositionTest.php create mode 100644 tests/Unit/Property/CSSNamespaceTest.php create mode 100644 tests/Unit/Property/CharsetTest.php create mode 100644 tests/Unit/Property/ImportTest.php create mode 100644 tests/Unit/Property/Selector/SpecificityCalculatorTest.php create mode 100644 tests/Unit/Property/SelectorTest.php create mode 100644 tests/Unit/Rule/RuleTest.php create mode 100644 tests/Unit/RuleSet/AtRuleSetTest.php create mode 100644 tests/Unit/RuleSet/DeclarationBlockTest.php create mode 100644 tests/Unit/RuleSet/RuleContainerTest.php create mode 100644 tests/Unit/RuleSet/RuleSetTest.php rename tests/{ => Unit}/SettingsTest.php (85%) create mode 100644 tests/Unit/Value/CSSStringTest.php rename tests/{ => Unit}/Value/CalcRuleValueListTest.php (64%) create mode 100644 tests/Unit/Value/ColorTest.php create mode 100644 tests/Unit/Value/Fixtures/ConcreteValue.php create mode 100644 tests/Unit/Value/SizeTest.php create mode 100644 tests/Unit/Value/URLTest.php rename tests/{ => Unit}/Value/ValueTest.php (70%) create mode 100644 tests/UnitDeprecated/.gitkeep delete mode 100644 tests/Value/SizeTest.php delete mode 100644 tests/fixtures/create-shorthands.css delete mode 100644 tests/fixtures/expand-shorthands.css delete mode 100644 tests/fixtures/ie-hacks.css diff --git a/.gitattributes b/.gitattributes index 4bf8ac39d..82a4f0b95 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,7 +4,9 @@ /.gitignore export-ignore /.phive/ export-ignore /CODE_OF_CONDUCT.md export-ignore +/CONTRIBUTING.md export-ignore /bin/ export-ignore /config/ export-ignore +/docs/ export-ignore /phpunit.xml export-ignore /tests/ export-ignore diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index 19269ae90..000000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,12 +0,0 @@ -# Contributing to PHP-CSS-Parser - -Those that wish to contribute bug fixes, new features, refactorings and -clean-up to PHP-CSS-Parser are more than welcome. - -When you contribute, please take the following things into account: - -## Contributor Code of Conduct - -Please note that this project is released with a -[Contributor Code of Conduct](../CODE_OF_CONDUCT.md). By participating in this -project, you agree to abide by its terms. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 919acb304..76a72c265 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,6 +6,9 @@ updates: directory: "/" schedule: interval: "daily" + commit-message: + prefix: "[Dependabot] " + milestone: 10 - package-ecosystem: "composer" directory: "/" @@ -13,4 +16,14 @@ updates: interval: "daily" allow: - dependency-type: "development" + ignore: + - dependency-name: "phpstan/*" + - dependency-name: "phpunit/phpunit" + versions: [ ">= 9.0.0" ] + - dependency-name: "rector/rector" + - dependency-name: "thecodingmachine/safe" + - dependency-name: "thecodingmachine/phpstan-safe-rule" versioning-strategy: "increase" + commit-message: + prefix: "[Dependabot] " + milestone: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76a2d3559..251e8fc4d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,11 +16,11 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - php-version: [ '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3' ] + php-version: [ '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5' ] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Install PHP uses: shivammathur/setup-php@v2 @@ -59,11 +59,11 @@ jobs: strategy: fail-fast: false matrix: - php-version: [ '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3' ] + php-version: [ '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5' ] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Install PHP uses: shivammathur/setup-php@v2 @@ -103,15 +103,16 @@ jobs: fail-fast: false matrix: command: - - fixer - - stan - - rector + - composer:normalize + - php:fixer + - php:stan + - php:rector php-version: - '8.3' steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Install PHP uses: shivammathur/setup-php@v2 @@ -138,8 +139,10 @@ jobs: composer show; - name: Install development tools + env: + GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - phive --no-progress install --trust-gpg-keys BBAB5DF0A0D6672989CF1869E82B2FB314E9906E + phive --no-progress install --trust-gpg-keys 0FDE18AE1D09E19F60F6B1CBC00543248C87FB13,BBAB5DF0A0D6672989CF1869E82B2FB314E9906E - name: Run Command - run: composer ci:php:${{ matrix.command }} + run: composer ci:${{ matrix.command }} diff --git a/.github/workflows/codecoverage.yml b/.github/workflows/codecoverage.yml index 7a094fdc8..29c37ba50 100644 --- a/.github/workflows/codecoverage.yml +++ b/.github/workflows/codecoverage.yml @@ -15,12 +15,14 @@ jobs: runs-on: ubuntu-22.04 strategy: + fail-fast: false matrix: - php-version: [ '7.4' ] + php-version: + - '7.4' steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Install PHP uses: shivammathur/setup-php@v2 @@ -30,6 +32,9 @@ jobs: tools: composer:v2 coverage: xdebug + - name: Show the Composer version + run: composer --version + - name: Show the Composer configuration run: composer config --global --list @@ -47,11 +52,13 @@ jobs: composer show; - name: Run Tests - run: ./vendor/bin/phpunit --coverage-clover build/coverage/xml + run: composer ci:tests:coverage + + - name: Show generated coverage files + run: ls -lah - - name: Upload coverage results to Codacy + - name: Upload coverage results to Coveralls + uses: coverallsapp/github-action@v2 env: - CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} - if: "${{ env.CODACY_PROJECT_TOKEN != '' }}" - run: | - ./vendor/bin/codacycoverage clover build/coverage/xml + github-token: ${{ secrets.GITHUB_TOKEN }} + file: coverage.xml diff --git a/.gitignore b/.gitignore index acf0d9d86..8bdbea99c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ /.php_cs.cache /.phpunit.result.cache /composer.lock +/coverage.xml /phpstan.neon /vendor/ !/.phive/phars.xml diff --git a/.phive/phars.xml b/.phive/phars.xml index a19b8834e..d9ab49f38 100644 --- a/.phive/phars.xml +++ b/.phive/phars.xml @@ -1,4 +1,5 @@ - + + diff --git a/CHANGELOG.md b/CHANGELOG.md index ed390901c..8978fbb71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,40 +3,96 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/). +Please also have a look at our +[API and deprecation policy](docs/API-and-deprecation-policy.md). + ## x.y.z ### Added -- Support arithmetic operators in CSS function arguments (#607) -- Add support for inserting an item in a CSS list (#545) + +### Changed + +### Deprecated + +### Removed + +### Fixed + +- Use typesafe versions of PHP functions (#1379, #1380, #1382, #1383, #1384) + +### Documentation + +## 9.1.0: Add support for PHP 8.5 + +### Added + +- Add support for PHP 8.5 (#1355) + +### Fixed + +- Improve performance of selector validation + (avoiding silent PCRE catastrophic failure) (#1372) +- Use typesafe versions of PHP functions (#1368, #1370) + +## 9.0.0: New features, deprecation removals and bug fixes + +### Added + +- Interface `RuleContainer` for `RuleSet` `Rule` manipulation methods (#1256) +- Partial support for CSS Color Module Level 4: + - `rgb` and `rgba`, and `hsl` and `hsla` are now aliases (#797) + - Parse color functions that use the "modern" syntax (#800) + - Render RGB functions with "modern" syntax when required (#840) + - Support `none` as color function component value (#859) - Add a class diagram to the README (#482) -- Add support for the `dvh`, `lvh` and `svh` length units (#415) - Add more tests (#449) ### Changed -- Improve performance of Value::parseValue with many delimiters by refactoring to remove array_search() (#413) +- `DeclarationBlock` no longer extends `RuleSet` and instead has a `RuleSet` as + a property; use `getRuleSet()` to access it directly (#1194) +- The default line (and column) number is now `null` (not zero) (#1288) +- `setPosition()` (in `Rule` and other classes) now has fluent interface, + returning itself (#1259) +- `RuleSet::removeRule()` now only allows `Rule` as the parameter + (implementing classes are `AtRuleSet` and `DeclarationBlock`); + use `removeMatchingRules()` or `removeAllRules()` for other functions (#1255) +- `RuleSet::getRules()` and `getRulesAssoc()` now only allow `string` or `null` + as the parameter (implementing classes are `AtRuleSet` and `DeclarationBlock`) + (#1253) +- Initialize `KeyFrame` properties to sensible defaults (#1146) +- Make `OutputFormat` `final` (#1128) +- Make `Selector` a `Renderable` (#1017) +- Only allow `string` for some `OutputFormat` properties (#885) +- Use more native type declarations and strict mode + (#641, #772, #774, #778, #804, #841, #873, #875, #891, #922, #923, #933, #958, + #964, #967, #1000, #1044, #1134, #1136, #1137, #1139, #1140, #1141, #1145, + #1162, #1163, #1166, #1172, #1174, #1178, #1179, #1181, #1183, #1184, #1186, + #1187, #1190, #1192, #1193, #1203) - Add visibility to all class/interface constants (#469) -### Deprecated - -- Deprecate `DeclarationBlock::createBorderShorthand()` (#578) -- Deprecate `DeclarationBlock::createFontShorthand()` (#580) -- Deprecate `DeclarationBlock::createDimensionsShorthand()` (#579) -- Deprecate `DeclarationBlock::createListStyleShorthand()` (#577) -- Deprecate `DeclarationBlock::createBackgroundShorthand()` (#576) -- Deprecate `DeclarationBlock::createShorthandProperties()` (#575) -- Deprecate `DeclarationBlock::expandListStyleShorthand()` (#574) -- Deprecate `DeclarationBlock::expandBackgroundShorthand()` (#573) -- Deprecate `DeclarationBlock::expandFontShorthand()` (#572) -- Deprecate `DeclarationBlock::expandDimensionsShorthand()` (#571) -- Deprecate `DeclarationBlock::expandBorderShorthand()` (#570) -- Deprecate `DeclarationBlock::createShorthands()` (#569) -- Deprecate `Document::expandShorthands()` (#566) -- Deprecate `Document::createShorthands()` (#567) -- Deprecate `DeclarationBlock::expandShorthands()` (#558) - ### Removed +- Remove `getLineNo()` from these classes (use `getLineNumber()` instead): + `Comment`, `CSSList`, `SourceException`, `Charset`, `CSSNamespace`, `Import`, + `Rule`, `DeclarationBlock`, `RuleSet`, `CSSFunction`, `Value` (#1258) +- Remove `Rule::getColNo()` (use `getColumnNumber()` instead) (#1287) +- Passing a string as the first argument to `getAllValues()` is no longer + supported and will not work; + the search pattern should now be passed as the second argument (#1243) +- Passing a Boolean as the second argument to `getAllValues()` is no longer + supported and will not work; the flag for searching in function arguments + should now be passed as the third argument (#1243) +- Remove `__toString()` (#1046) +- Drop magic method forwarding in `OutputFormat` (#898) +- Drop `atRuleArgs()` from the `AtRule` interface (#1141) +- Remove `OutputFormat::get()` and `::set()` (#1108, #1110) +- Drop special support for vendor prefixes (#1083) +- Remove the IE hack in `Rule` (#995) +- Drop `getLineNo()` from the `Renderable` interface (#1038) +- Remove `OutputFormat::level()` (#874) +- Remove expansion of shorthand properties (#838) +- Remove `Parser::setCharset/getCharset` (#808) - Remove `Rule::getValues()` (#582) - Remove `Rule::setValues()` (#562) - Remove `Document::getAllSelectors()` (#561) @@ -46,13 +102,166 @@ This project adheres to [Semantic Versioning](https://semver.org/). ### Fixed -- Fix undefined local variable in `CalcFunction::parse()` (#593) -- Fix PHP notice caused by parsing invalid color values having less than 6 characters (#485) -- Fix (regression) failure to parse at-rules with strict parsing (#456) +- Remove trailing semicolon from declaration blocks with 'compact' + `OutputFormat` (#1345) +- Parse selector functions (like `:not`) with comma-separated arguments (#1292) +- Parse quoted attribute selector value containing comma (#1323) +- Allow comma in selectors (e.g. `:not(html, body)`) (#1293) +- Insert `Rule` before sibling even with different property name + (in `RuleSet::addRule()`) (#1270) +- Ensure `RuleSet::addRule()` sets non-negative column number when sibling + provided (#1268) +- Don't render `rgb` colors with percentage values using hex notation (#803) + +### Documentation + +- Add an API and deprecation policy (#720) @ziegenberg is a new contributor to this release and did a lot of the heavy lifting. Thanks! :heart: +## 8.9.0: New features, bug fixes and deprecations + +### Added + +- `RuleSet::removeMatchingRules()` method + (for the implementing classes `AtRuleSet` and `DeclarationBlock`) (#1249) +- `RuleSet::removeAllRules()` method + (for the implementing classes `AtRuleSet` and `DeclarationBlock`) (#1249) +- Add Interface `CSSElement` (#1231) +- Methods `getLineNumber` and `getColumnNumber` which return a nullable `int` + for the following classes: + `Comment`, `CSSList`, `SourceException`, `Charset`, `CSSNamespace`, `Import`, + `Rule`, `DeclarationBlock`, `RuleSet`, `CSSFunction`, `Value` (#1225, #1263) +- `Positionable` interface for CSS items that may have a position + (line and perhaps column number) in the parsed CSS (#1221) + +### Changed + +- Parameters for `getAllValues()` are deconflated, so it now takes three (all + optional), allowing `$element` and `$ruleSearchPattern` to be specified + separately (#1241) +- Implement `Positionable` in the following CSS item classes: + `Comment`, `CSSList`, `SourceException`, `Charset`, `CSSNamespace`, `Import`, + `Rule`, `DeclarationBlock`, `RuleSet`, `CSSFunction`, `Value` (#1225) + +### Deprecated + +- Support for PHP < 7.2 is deprecated; version 9.0 will require PHP 7.2 or later + (#1264) +- Passing a `string` or `null` to `RuleSet::removeRule()` is deprecated + (implementing classes are `AtRuleSet` and `DeclarationBlock`); + use `removeMatchingRules()` or `removeAllRules()` instead (#1249) +- Passing a `Rule` to `RuleSet::getRules()` or `getRulesAssoc()` is deprecated, + affecting the implementing classes `AtRuleSet` and `DeclarationBlock` + (call e.g. `getRules($rule->getRule())` instead) (#1248) +- Passing a string as the first argument to `getAllValues()` is deprecated; + the search pattern should now be passed as the second argument (#1241) +- Passing a Boolean as the second argument to `getAllValues()` is deprecated; + the flag for searching in function arguments should now be passed as the third + argument (#1241) +- `getLineNo()` is deprecated in these classes (use `getLineNumber()` instead): + `Comment`, `CSSList`, `SourceException`, `Charset`, `CSSNamespace`, `Import`, + `Rule`, `DeclarationBlock`, `RuleSet`, `CSSFunction`, `Value` (#1225, #1233) +- `Rule::getColNo()` is deprecated (use `getColumnNumber()` instead) + (#1225, #1233) +- Providing zero as the line number argument to `Rule::setPosition()` is + deprecated (pass `null` instead if there is no line number) (#1225, #1233) + +### Fixed + +- Set line number when `RuleSet::addRule()` called with only column number set + (#1265) +- Ensure first rule added with `RuleSet::addRule()` has valid position (#1262) + +## 8.8.0: Bug fixes and deprecations + +### Added + +- `OutputFormat` properties for space around specific list separators (#880) + +### Changed + +- Mark the `OutputFormat` constructor as `@internal` (#1131) +- Mark `OutputFormatter` as `@internal` (#896) +- Mark `Selector::isValid()` as `@internal` (#1037) +- Mark parsing-related methods of most CSS elements as `@internal` (#908) +- Mark `OutputFormat::nextLevel()` as `@internal` (#901) +- Make all non-private properties `@internal` (#886) + +### Deprecated + +- Deprecate extending `OutputFormat` (#1131) +- Deprecate `OutputFormat::get()` and `::set()` (#1107) +- Deprecate support for `-webkit-calc` and `-moz-calc` (#1086) +- Deprecate magic method forwarding from `OutputFormat` to `OutputFormatter` + (#894) +- Deprecate `__toString()` (#1006) +- Deprecate greedy calculation of selector specificity (#1018) +- Deprecate the IE hack in `Rule` (#993, #1003) +- `OutputFormat` properties for space around list separators as an array (#880) +- Deprecate `OutputFormat::level()` (#870) + +### Fixed + +- Include comments for all rules in declaration block (#1169) +- Render rules in line and column number order (#1059) +- Create `Size` with correct types in `expandBackgroundShorthand` (#814) +- Parse `@font-face` `src` property as comma-delimited list (#794) + +## 8.7.0: Add support for PHP 8.4 + +### Added + +- Add support for PHP 8.4 (#643, #657) + +### Changed + +- Mark parsing-internal classes and methods as `@internal` (#674) +- Block installations on unsupported higher PHP versions (#691) + +### Deprecated + +- Deprecate the expansion of shorthand properties + (#578, #580, #579, #577, #576, #575, #574, #573, #572, #571, #570, #569, #566, + #567, #558, #714) +- Deprecate `Parser::setCharset()` and `Parser::getCharset()` (#688) + +### Fixed + +- Fix type errors in PHP strict mode (#664) + +## 8.6.0 + +### Added + +- Support arithmetic operators in CSS function arguments (#607) +- Add support for inserting an item in a CSS list (#545) +- Add support for the `dvh`, `lvh` and `svh` length units (#415) + +### Changed + +- Improve performance of `Value::parseValue` with many delimiters by refactoring + to remove `array_search()` (#413) + +## 8.5.2 + +### Changed + +- Mark all class constants as `@internal` (#472) + +### Fixed + +- Fix undefined local variable in `CalcFunction::parse()` (#593) + +## 8.5.1 + +### Fixed + +- Fix PHP notice caused by parsing invalid color values having less than + 6 characters (#485) +- Fix (regression) failure to parse at-rules with strict parsing (#456) + ## 8.5.0 ### Added @@ -75,7 +284,8 @@ lifting. Thanks! :heart: * Support for PHP 8.x * PHPDoc annotations -* Allow usage of CSS variables inside color functions (by parsing them as regular functions) +* Allow usage of CSS variables inside color functions (by parsing them as + regular functions) * Use PSR-12 code style * *No deprecations* @@ -90,7 +300,10 @@ lifting. Thanks! :heart: * Allow a file to end after an `@import` * Preserve case of CSS variables as specced * Allow identifiers to use escapes the same way as strings -* No longer use `eval` for the comparison in `getSelectorsBySpecificity`, in case it gets passed untrusted input (CVE-2020-13756). Also fixed in 8.3.1, 8.2.1, 8.1.1, 8.0.1, 7.0.4, 6.0.2, 5.2.1, 5.1.3, 5.0.9, 4.0.1, 3.0.1, 2.0.1, 1.0.1. +* No longer use `eval` for the comparison in `getSelectorsBySpecificity`, in + case it gets passed untrusted input (CVE-2020-13756). Also fixed in 8.3.1, + 8.2.1, 8.1.1, 8.0.1, 7.0.4, 6.0.2, 5.2.1, 5.1.3, 5.0.9, 4.0.1, 3.0.1, 2.0.1, + 1.0.1. * Prevent an infinite loop when parsing invalid grid line names * Remove invalid unit `vm` * Retain rule order after expanding shorthands @@ -102,11 +315,16 @@ lifting. Thanks! :heart: ## 8.3.0 (2019-02-22) -* Refactor parsing logic to mostly reside in the class files whose data structure is to be parsed (this should eventually allow us to unit-test specific parts of the parsing logic individually). -* Fix error in parsing `calc` expessions when the first operand is a negative number, thanks to @raxbg. -* Support parsing CSS4 colors in hex notation with alpha values, thanks to @raxbg. +* Refactor parsing logic to mostly reside in the class files whose data + structure is to be parsed (this should eventually allow us to unit-test + specific parts of the parsing logic individually). +* Fix error in parsing `calc` expessions when the first operand is a negative + number, thanks to @raxbg. +* Support parsing CSS4 colors in hex notation with alpha values, thanks to + @raxbg. * Swallow more errors in lenient mode, thanks to @raxbg. -* Allow specifying arbitrary strings to output before and after declaration blocks, thanks to @westonruter. +* Allow specifying arbitrary strings to output before and after declaration + blocks, thanks to @westonruter. * *No backwards-incompatible changes* * *No deprecations* @@ -114,16 +332,20 @@ lifting. Thanks! :heart: * Support parsing `calc()`, thanks to @raxbg. * Support parsing grid-lines, again thanks to @raxbg. -* Support parsing legacy IE filters (`progid:`) in lenient mode, thanks to @FMCorz +* Support parsing legacy IE filters (`progid:`) in lenient mode, thanks to + @FMCorz * Performance improvements parsing large files, again thanks to @FMCorz * *No backwards-incompatible changes* * *No deprecations* ## 8.1.0 (2016-07-19) -* Comments are no longer silently ignored but stored with the object with which they appear (no render support, though). Thanks to @FMCorz. -* The IE hacks using `\0` and `\9` can now be parsed (and rendered) in lenient mode. Thanks (again) to @FMCorz. -* Media queries with or without spaces before the query are parsed. Still no *real* parsing support, though. Sorry… +* Comments are no longer silently ignored but stored with the object with which + they appear (no render support, though). Thanks to @FMCorz. +* The IE hacks using `\0` and `\9` can now be parsed (and rendered) in lenient + mode. Thanks (again) to @FMCorz. +* Media queries with or without spaces before the query are parsed. Still no + *real* parsing support, though. Sorry… * PHPUnit is now listed as a dev-dependency in composer.json. * *No backwards-incompatible changes* * *No deprecations* @@ -135,7 +357,8 @@ lifting. Thanks! :heart: ### Backwards-incompatible changes -* Unrecoverable parser errors throw an exception of type `Sabberworm\CSS\Parsing\SourceException` instead of `\Exception`. +* Unrecoverable parser errors throw an exception of type + `Sabberworm\CSS\Parsing\SourceException` instead of `\Exception`. ## 7.0.3 (2016-04-27) @@ -145,7 +368,8 @@ lifting. Thanks! :heart: ## 7.0.2 (2016-02-11) -* 150 time performance boost thanks to @[ossinkine](https://github.com/ossinkine) +* 150 time performance boost thanks + to @[ossinkine](https://github.com/ossinkine) * *No backwards-incompatible changes* * *No deprecations* @@ -162,7 +386,8 @@ lifting. Thanks! :heart: ### Backwards-incompatible changes -* The `Sabberworm\CSS\Value\String` class has been renamed to `Sabberworm\CSS\Value\CSSString`. +* The `Sabberworm\CSS\Value\String` class has been renamed to + `Sabberworm\CSS\Value\CSSString`. ## 6.0.1 (2015-08-24) @@ -176,22 +401,27 @@ lifting. Thanks! :heart: ### Deprecations -* The parse() method replaces __toString with an optional argument (instance of the OutputFormat class) +* The parse() method replaces __toString with an optional argument (instance of + the OutputFormat class) ## 5.2.0 (2014-06-30) -* Support removing a selector from a declaration block using `$oBlock->removeSelector($mSelector)` -* Introduce a specialized exception (Sabberworm\CSS\Parsing\OuputException) for exceptions during output rendering +* Support removing a selector from a declaration block using + `$oBlock->removeSelector($mSelector)` +* Introduce a specialized exception (Sabberworm\CSS\Parsing\OuputException) for + exceptions during output rendering * *No deprecations* #### Backwards-incompatible changes -* Outputting a declaration block that has no selectors throws an OuputException instead of outputting an invalid ` {…}` into the CSS document. +* Outputting a declaration block that has no selectors throws an OuputException + instead of outputting an invalid ` {…}` into the CSS document. ## 5.1.2 (2013-10-30) -* Remove the use of consumeUntil in comment parsing. This makes it possible to parse comments such as `/** Perfectly valid **/` +* Remove the use of consumeUntil in comment parsing. This makes it possible to + parse comments such as `/** Perfectly valid **/` * Add fr relative size unit * Fix some issues with HHVM * *No backwards-incompatible changes* @@ -206,13 +436,15 @@ lifting. Thanks! :heart: ## 5.1.0 (2013-10-24) * Performance enhancements by Michael M Slusarz -* More rescue entry points for lenient parsing (unexpected tokens between declaration blocks and unclosed comments) +* More rescue entry points for lenient parsing (unexpected tokens between + declaration blocks and unclosed comments) * *No backwards-incompatible changes* * *No deprecations* ## 5.0.8 (2013-08-15) -* Make default settings’ multibyte parsing option dependent on whether or not the mbstring extension is actually installed. +* Make default settings’ multibyte parsing option dependent on whether or not + the mbstring extension is actually installed. * *No backwards-incompatible changes* * *No deprecations* @@ -230,7 +462,9 @@ lifting. Thanks! :heart: ## 5.0.5 (2013-04-17) -* Initial support for lenient parsing (setting this parser option will catch some exceptions internally and recover the parser’s state as neatly as possible). +* Initial support for lenient parsing (setting this parser option will catch + some exceptions internally and recover the parser’s state as neatly as + possible). * *No backwards-incompatible changes* * *No deprecations* @@ -267,18 +501,22 @@ lifting. Thanks! :heart: ### Backwards-incompatible changes -* `Sabberworm\CSS\Value\Color`’s `__toString` method overrides `CSSList`’s to maybe return something other than `type(value, …)` (see above). +* `Sabberworm\CSS\Value\Color`’s `__toString` method overrides `CSSList`’s to + maybe return something other than `type(value, …)` (see above). ## 4.0.0 (2013-03-19) * Support for more @-rules -* Generic interface `Sabberworm\CSS\Property\AtRule`, implemented by all @-rule classes +* Generic interface `Sabberworm\CSS\Property\AtRule`, implemented by all @-rule + classes * *No deprecations* ### Backwards-incompatible changes * `Sabberworm\CSS\RuleSet\AtRule` renamed to `Sabberworm\CSS\RuleSet\AtRuleSet` -* `Sabberworm\CSS\CSSList\MediaQuery` renamed to `Sabberworm\CSS\RuleSet\CSSList\AtRuleBlockList` with differing semantics and API (which also works for other block-list-based @-rules like `@supports`). +* `Sabberworm\CSS\CSSList\MediaQuery` renamed to + `Sabberworm\CSS\RuleSet\CSSList\AtRuleBlockList` with differing semantics and + API (which also works for other block-list-based @-rules like `@supports`). ## 3.0.0 (2013-03-06) @@ -287,10 +525,18 @@ lifting. Thanks! :heart: ### Backwards-incompatible changes -* All properties (like whether or not to use `mb_`-functions, which default charset to use and – new – whether or not to be forgiving when parsing) are now encapsulated in an instance of `Sabberworm\CSS\Settings` which can be passed as the second argument to `Sabberworm\CSS\Parser->__construct()`. -* Specifying a charset as the second argument to `Sabberworm\CSS\Parser->__construct()` is no longer supported. Use `Sabberworm\CSS\Settings::create()->withDefaultCharset('some-charset')` instead. -* Setting `Sabberworm\CSS\Parser->bUseMbFunctions` has no effect. Use `Sabberworm\CSS\Settings::create()->withMultibyteSupport(true/false)` instead. -* `Sabberworm\CSS\Parser->parse()` may throw a `Sabberworm\CSS\Parsing\UnexpectedTokenException` when in strict parsing mode. +* All properties (like whether or not to use `mb_`-functions, which default + charset to use and – new – whether or not to be forgiving when parsing) are + now encapsulated in an instance of `Sabberworm\CSS\Settings` which can be + passed as the second argument to `Sabberworm\CSS\Parser->__construct()`. +* Specifying a charset as the second argument to + `Sabberworm\CSS\Parser->__construct()` is no longer supported. Use + `Sabberworm\CSS\Settings::create()->withDefaultCharset('some-charset')` + instead. +* Setting `Sabberworm\CSS\Parser->bUseMbFunctions` has no effect. Use + `Sabberworm\CSS\Settings::create()->withMultibyteSupport(true/false)` instead. +* `Sabberworm\CSS\Parser->parse()` may throw a + `Sabberworm\CSS\Parsing\UnexpectedTokenException` when in strict parsing mode. ## 2.0.0 (2013-01-29) @@ -298,8 +544,13 @@ lifting. Thanks! :heart: ### Backwards-incompatible changes -* `Sabberworm\CSS\RuleSet->getRules()` returns an index-based array instead of an associative array. Use `Sabberworm\CSS\RuleSet->getRulesAssoc()` (which eliminates duplicate rules and lets the later rule of the same name win). -* `Sabberworm\CSS\RuleSet->removeRule()` works as it did before except when passed an instance of `Sabberworm\CSS\Rule\Rule`, in which case it would only remove the exact rule given instead of all the rules of the same type. To get the old behaviour, use `Sabberworm\CSS\RuleSet->removeRule($oRule->getRule()`; +* `Sabberworm\CSS\RuleSet->getRules()` returns an index-based array instead of + an associative array. Use `Sabberworm\CSS\RuleSet->getRulesAssoc()` (which + eliminates duplicate rules and lets the later rule of the same name win). +* `Sabberworm\CSS\RuleSet->removeRule()` works as it did before except when + passed an instance of `Sabberworm\CSS\Rule\Rule`, in which case it would only + remove the exact rule given instead of all the rules of the same type. To get + the old behaviour, use `Sabberworm\CSS\RuleSet->removeRule($oRule->getRule()`; ## 1.0 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 552b3aae5..1b87c0935 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,76 +1,119 @@ -# Contributor Code of Conduct +# Contributor Covenant Code of Conduct ## Our Pledge -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, -body size, disability, ethnicity, gender identity and expression, level of -experience, nationality, personal appearance, race, religion, or sexual +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + ## Our Standards -Examples of behavior that contributes to creating a positive environment -include: +Examples of behavior that contributes to a positive environment for our +community include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community -Examples of unacceptable behavior by participants include: +Examples of unacceptable behavior include: -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission +* Publishing others' private information, such as a physical or email address, + without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting -## Our Responsibilities +## Enforcement Responsibilities -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. ## Scope -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an -appointed representative at an online or offline event. Representation of a -project may be further defined and clarified by project maintainers. +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at (emogrifier at myintervals dot com). -All complaints will be reviewed and investigated and will result in a response -that is deemed necessary and appropriate to the circumstances. The project team -is obligated to maintain confidentiality with regard to the reporter of an -incident. Further details of specific enforcement policies may be posted -separately. +reported to the community leaders responsible for enforcement at +(myintervals-coc at gaggle dot email). +All complaints will be reviewed and investigated promptly and fairly. -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. -## Attribution +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. -This Code of Conduct is adapted from the [Contributor Covenant][homepage], -version 1.4, available at -[http://contributor-covenant.org/version/1/4/][version]. +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), +version 2.1, available at +https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..1d0085f3a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,196 @@ +# Contributing to PHP-CSS-Parser + +Those that wish to contribute bug fixes, new features, refactorings and +clean-up to PHP-CSS-Parser are more than welcome. + +When you contribute, please take the following things into account: + +## Contributor Code of Conduct + +Please note that this project is released with a +[Contributor Code of Conduct](../CODE_OF_CONDUCT.md). By participating in this +project, you agree to abide by its terms. + +## General workflow + +This is the workflow for contributing changes to this project:: + +1. [Fork the Git repository](https://docs.github.com/en/get-started/exploring-projects-on-github/contributing-to-a-project). +1. Clone your forked repository locally and install the development + dependencies. +1. Create a local branch for your changes. +1. Add unit tests for your changes. + These tests should fail without your changes. +1. Add your changes. Your added unit tests now should pass, and no other tests + should be broken. Check that your changes follow the same coding style as the + rest of the project. +1. Add a changelog entry, newest on top. +1. Commit and push your changes. +1. [Create a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) + for your changes. +1. Check that the CI build is green. (If it is not, fix the problems listed.) + Please note that for first-time contributors, you will need to wait for a + maintainer to allow your CI build to run. +1. Wait for a review by the maintainers. +1. Polish your changes as needed until they are ready to be merged. + +## About code reviews + +After you have submitted a pull request, the maintainers will review your +changes. This will probably result in quite a few comments on ways to improve +your pull request. This project receives contributions from developers around +the world, so we need the code to be the most consistent, readable, and +maintainable that it can be. + +Please do not feel frustrated by this - instead please view this both as our +contribution to your pull request as well as a way to learn more about +improving code quality. + +If you would like to know whether an idea would fit in the general strategy of +this project or would like to get feedback on the best architecture for your +ideas, we propose you open a ticket first and discuss your ideas there +first before investing a lot of time in writing code. + +## Install the development dependencies + +To install the most important development dependencies, please run the following +command: + +```bash +composer install +``` + +We also have some optional development dependencies that require higher PHP +versions than the lowest PHP version this project supports. Hence they are not +installed by default. + +To install these, you will need to have [PHIVE](https://phar.io/) installed. +You can then run the following command: + +```bash +phive install +``` + +## Unit-test your changes + +Please cover all changes with unit tests and make sure that your code does not +break any existing tests. We will only merge pull requests that include full +code coverage of the fixed bugs and the new features. + +To run the existing PHPUnit tests, run this command: + +```bash +composer ci:tests:unit +``` + +## Coding Style + +Please use the same coding style +([PER 2.0](https://www.php-fig.org/per/coding-style/)) as the rest of the code. +Indentation is four spaces. + +We will only merge pull requests that follow the project's coding style. + +Please check your code with the provided static code analysis tools: + +```bash +composer ci:static +``` + +Please make your code clean, well-readable and easy to understand. + +If you add new methods or fields, please add proper PHPDoc for the new +methods/fields. Please use grammatically correct, complete sentences in the +code documentation. + +You can autoformat your code using the following command: + +```bash +composer fix +``` + +## Git commits + +Commit message should have a <= 50-character summary, optionally followed by a +blank line and a more in depth description of 79 characters per line. + +Please use grammatically correct, complete sentences in the commit messages. + +Also, please prefix the subject line of the commit message with either +`[FEATURE]`, `[TASK]`, `[BUGFIX]` OR `[CLEANUP]`. This makes it faster to see +what a commit is about. + +## Creating pull requests (PRs) + +When you create a pull request, please +[make your PR editable](https://github.com/blog/2247-improving-collaboration-with-forks). + +## Rebasing + +If other PRs have been merged during the time between your initial PR creation +and final approval, it may be required that you rebase your changes against the +latest `main` branch. + +There are potential pitfalls here if you follow the suggestions from `git`, +which could leave your branch in an unrecoverable mess, +and you having to start over with a new branch and new PR. + +The procedure below is tried and tested, and will help you avoid frustration. + +To rebase a feature branch to the latest `main`: + +1. Make sure that your local copy of the repository has the most up-to-date + revisions of `main` (this is important, otherwise you may end up rebasing to + an older base point): + ```bash + git switch main + git pull + ``` +1. Switch to the (feature) branch to be rebased and make sure your copy is up to + date: + ```bash + git switch feature/something-cool + git pull + ``` +1. Consider taking a copy of the folder tree at this stage; this may help when + resolving conflicts in the next step. +1. Begin the rebasing process + ```bash + git rebase main + ``` +1. Resolve the conflicts in the reported files. (This will typically require + reversing the order of the new entries in `CHANGELOG.md`.) You may use a + folder `diff` against the copy taken at step 3 to assist, but bear in mind + that at this stage `git` is partway through rebasing, so some files will have + been merged and include the latest changes from `main`, whilst others might + not. In any case, you should ignore changes to files not reported as having + conflicts. + + If there were no conflicts, skip this and the next step. +1. Mark the conflicting files as resolved and continue the rebase + ```bash + git add . + git rebase --continue + ``` + (You can alternatively use more specific wildcards or specify individual + files with a full relative path.) + + If there were no conflicts reported in the previous step, skip this step. + + If there are more conflicts to resolve, repeat the previous step then this + step again. +1. Force-push the rebased (feature) branch to the remote repository + ```bash + git push --force + ``` + The `--force` option is important. Without it, you'll get an error with a + hint suggesting a `git pull` is required: + ``` + hint: Updates were rejected because the tip of your current branch is behind + hint: its remote counterpart. Integrate the remote changes (e.g. + hint: 'git pull ...') before pushing again. + hint: See the 'Note about fast-forwards' in 'git push --help' for details. + ``` + ***DO NOT*** follow the hint and execute `git pull`. This will result in the + set of all commits on the feature branch being duplicated, and the "patching + base" not being moved at all. diff --git a/README.md b/README.md index 597949e64..176f49ca8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # PHP CSS Parser -[![Build Status](https://github.com/MyIntervals/PHP-CSS-Parser/workflows/CI/badge.svg?branch=main)](https://github.com/MyIntervals/PHP-CSS-Parser/actions/) +[![Build Status](https://github.com/MyIntervals/PHP-CSS-Parser/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/MyIntervals/PHP-CSS-Parser/actions/) +[![Coverage Status](https://coveralls.io/repos/github/MyIntervals/PHP-CSS-Parser/badge.svg?branch=main)](https://coveralls.io/github/MyIntervals/PHP-CSS-Parser?branch=main) A Parser for CSS Files written in PHP. Allows extraction of CSS files into a data structure, manipulation of said structure and output as (optimized) CSS. @@ -220,44 +221,44 @@ html, body { ```php class Sabberworm\CSS\CSSList\Document#4 (2) { - protected $aContents => + protected $contents => array(4) { [0] => class Sabberworm\CSS\Property\Charset#6 (2) { - private $sCharset => + private $charset => class Sabberworm\CSS\Value\CSSString#5 (2) { - private $sString => + private $string => string(5) "utf-8" - protected $iLineNo => + protected $lineNumber => int(1) } - protected $iLineNo => + protected $lineNumber => int(1) } [1] => class Sabberworm\CSS\RuleSet\AtRuleSet#7 (4) { - private $sType => + private $type => string(9) "font-face" - private $sArgs => + private $arguments => string(0) "" - private $aRules => + private $rules => array(2) { 'font-family' => array(1) { [0] => class Sabberworm\CSS\Rule\Rule#8 (4) { - private $sRule => + private $rule => string(11) "font-family" - private $mValue => + private $value => class Sabberworm\CSS\Value\CSSString#9 (2) { - private $sString => + private $string => string(10) "CrassRoots" - protected $iLineNo => + protected $lineNumber => int(4) } - private $bIsImportant => + private $isImportant => bool(false) - protected $iLineNo => + protected $lineNumber => int(4) } } @@ -265,76 +266,76 @@ class Sabberworm\CSS\CSSList\Document#4 (2) { array(1) { [0] => class Sabberworm\CSS\Rule\Rule#10 (4) { - private $sRule => + private $rule => string(3) "src" - private $mValue => + private $value => class Sabberworm\CSS\Value\URL#11 (2) { - private $oURL => + private $url => class Sabberworm\CSS\Value\CSSString#12 (2) { - private $sString => + private $string => string(15) "../media/cr.ttf" - protected $iLineNo => + protected $lineNumber => int(5) } - protected $iLineNo => + protected $lineNumber => int(5) } - private $bIsImportant => + private $isImportant => bool(false) - protected $iLineNo => + protected $lineNumber => int(5) } } } - protected $iLineNo => + protected $lineNumber => int(3) } [2] => class Sabberworm\CSS\RuleSet\DeclarationBlock#13 (3) { - private $aSelectors => + private $selectors => array(2) { [0] => class Sabberworm\CSS\Property\Selector#14 (2) { - private $sSelector => + private $selector => string(4) "html" - private $iSpecificity => + private $specificity => NULL } [1] => class Sabberworm\CSS\Property\Selector#15 (2) { - private $sSelector => + private $selector => string(4) "body" - private $iSpecificity => + private $specificity => NULL } } - private $aRules => + private $rules => array(1) { 'font-size' => array(1) { [0] => class Sabberworm\CSS\Rule\Rule#16 (4) { - private $sRule => + private $rule => string(9) "font-size" - private $mValue => + private $value => class Sabberworm\CSS\Value\Size#17 (4) { - private $fSize => + private $size => double(1.6) - private $sUnit => + private $unit => string(2) "em" - private $bIsColorComponent => + private $isColorComponent => bool(false) - protected $iLineNo => + protected $lineNumber => int(9) } - private $bIsImportant => + private $isImportant => bool(false) - protected $iLineNo => + protected $lineNumber => int(9) } } } - protected $iLineNo => + protected $lineNumber => int(8) } [3] => @@ -343,96 +344,96 @@ class Sabberworm\CSS\CSSList\Document#4 (2) { string(9) "keyframes" private $animationName => string(6) "mymove" - protected $aContents => + protected $contents => array(2) { [0] => class Sabberworm\CSS\RuleSet\DeclarationBlock#19 (3) { - private $aSelectors => + private $selectors => array(1) { [0] => class Sabberworm\CSS\Property\Selector#20 (2) { - private $sSelector => + private $selector => string(4) "from" - private $iSpecificity => + private $specificity => NULL } } - private $aRules => + private $rules => array(1) { 'top' => array(1) { [0] => class Sabberworm\CSS\Rule\Rule#21 (4) { - private $sRule => + private $rule => string(3) "top" - private $mValue => + private $value => class Sabberworm\CSS\Value\Size#22 (4) { - private $fSize => + private $size => double(0) - private $sUnit => + private $unit => string(2) "px" - private $bIsColorComponent => + private $isColorComponent => bool(false) - protected $iLineNo => + protected $lineNumber => int(13) } - private $bIsImportant => + private $isImportant => bool(false) - protected $iLineNo => + protected $lineNumber => int(13) } } } - protected $iLineNo => + protected $lineNumber => int(13) } [1] => class Sabberworm\CSS\RuleSet\DeclarationBlock#23 (3) { - private $aSelectors => + private $selectors => array(1) { [0] => class Sabberworm\CSS\Property\Selector#24 (2) { - private $sSelector => + private $selector => string(2) "to" - private $iSpecificity => + private $specificity => NULL } } - private $aRules => + private $rules => array(1) { 'top' => array(1) { [0] => class Sabberworm\CSS\Rule\Rule#25 (4) { - private $sRule => + private $rule => string(3) "top" - private $mValue => + private $value => class Sabberworm\CSS\Value\Size#26 (4) { - private $fSize => + private $size => double(200) - private $sUnit => + private $unit => string(2) "px" - private $bIsColorComponent => + private $isColorComponent => bool(false) - protected $iLineNo => + protected $lineNumber => int(14) } - private $bIsImportant => + private $isImportant => bool(false) - protected $iLineNo => + protected $lineNumber => int(14) } } } - protected $iLineNo => + protected $lineNumber => int(14) } } - protected $iLineNo => + protected $lineNumber => int(12) } } - protected $iLineNo => + protected $lineNumber => int(1) } @@ -466,85 +467,85 @@ html, body {font-size: 1.6em;} ```php class Sabberworm\CSS\CSSList\Document#4 (2) { - protected $aContents => + protected $contents => array(1) { [0] => class Sabberworm\CSS\RuleSet\DeclarationBlock#5 (3) { - private $aSelectors => + private $selectors => array(1) { [0] => class Sabberworm\CSS\Property\Selector#6 (2) { - private $sSelector => + private $selector => string(7) "#header" - private $iSpecificity => + private $specificity => NULL } } - private $aRules => + private $rules => array(3) { 'margin' => array(1) { [0] => class Sabberworm\CSS\Rule\Rule#7 (4) { - private $sRule => + private $rule => string(6) "margin" - private $mValue => + private $value => class Sabberworm\CSS\Value\RuleValueList#12 (3) { - protected $aComponents => + protected $components => array(4) { [0] => class Sabberworm\CSS\Value\Size#8 (4) { - private $fSize => + private $size => double(10) - private $sUnit => + private $unit => string(2) "px" - private $bIsColorComponent => + private $isColorComponent => bool(false) - protected $iLineNo => + protected $lineNumber => int(2) } [1] => class Sabberworm\CSS\Value\Size#9 (4) { - private $fSize => + private $size => double(2) - private $sUnit => + private $unit => string(2) "em" - private $bIsColorComponent => + private $isColorComponent => bool(false) - protected $iLineNo => + protected $lineNumber => int(2) } [2] => class Sabberworm\CSS\Value\Size#10 (4) { - private $fSize => + private $size => double(1) - private $sUnit => + private $unit => string(2) "cm" - private $bIsColorComponent => + private $isColorComponent => bool(false) - protected $iLineNo => + protected $lineNumber => int(2) } [3] => class Sabberworm\CSS\Value\Size#11 (4) { - private $fSize => + private $size => double(2) - private $sUnit => + private $unit => string(1) "%" - private $bIsColorComponent => + private $isColorComponent => bool(false) - protected $iLineNo => + protected $lineNumber => int(2) } } - protected $sSeparator => + protected $separator => string(1) " " - protected $iLineNo => + protected $lineNumber => int(2) } - private $bIsImportant => + private $isImportant => bool(false) - protected $iLineNo => + protected $lineNumber => int(2) } } @@ -552,11 +553,11 @@ class Sabberworm\CSS\CSSList\Document#4 (2) { array(1) { [0] => class Sabberworm\CSS\Rule\Rule#13 (4) { - private $sRule => + private $rule => string(11) "font-family" - private $mValue => + private $value => class Sabberworm\CSS\Value\RuleValueList#15 (3) { - protected $aComponents => + protected $components => array(4) { [0] => string(7) "Verdana" @@ -564,9 +565,9 @@ class Sabberworm\CSS\CSSList\Document#4 (2) { string(9) "Helvetica" [2] => class Sabberworm\CSS\Value\CSSString#14 (2) { - private $sString => + private $string => string(9) "Gill Sans" - protected $iLineNo => + protected $lineNumber => int(3) } [3] => @@ -574,12 +575,12 @@ class Sabberworm\CSS\CSSList\Document#4 (2) { } protected $sSeparator => string(1) "," - protected $iLineNo => + protected $lineNumber => int(3) } - private $bIsImportant => + private $isImportant => bool(false) - protected $iLineNo => + protected $lineNumber => int(3) } } @@ -587,22 +588,22 @@ class Sabberworm\CSS\CSSList\Document#4 (2) { array(1) { [0] => class Sabberworm\CSS\Rule\Rule#16 (4) { - private $sRule => + private $rule => string(5) "color" - private $mValue => + private $value => string(3) "red" - private $bIsImportant => + private $isImportant => bool(true) - protected $iLineNo => + protected $lineNumber => int(4) } } } - protected $iLineNo => + protected $lineNumber => int(1) } } - protected $iLineNo => + protected $lineNumber => int(1) } @@ -621,164 +622,201 @@ class Sabberworm\CSS\CSSList\Document#4 (2) { classDiagram direction LR - %% Start of the part generated from the PHP code using tasuku43/mermaid-class-diagram - - class Renderable { - <> + class Anchor { } - class DeclarationBlock { + class AtRule { + <> } - class RuleSet { - <> + class AtRuleBlockList { } class AtRuleSet { } - class KeyframeSelector { + class CSSBlockList { + <> } - class AtRule { + class CSSElement { <> } - class Charset { + class CSSFunction { } - class Import { + class CSSList { + <> } - class Selector { + class CSSListItem { + <> } class CSSNamespace { } - class Settings { + class CSSString { } - class Rule { + class CalcFunction { } - class Parser { + class CalcRuleValueList { } - class OutputFormatter { + class Charset { } - class OutputFormat { + class Color { } - class OutputException { + class Comment { } - class UnexpectedEOFException { + class Commentable { + <> } - class SourceException { + class DeclarationBlock { } - class UnexpectedTokenException { + class Document { } - class ParserState { + class Import { } - class Anchor { + class KeyFrame { } - class CSSBlockList { - <> + class KeyframeSelector { } - class Document { + class LineName { } - class CSSList { - <> + class OutputException { } - class KeyFrame { + class OutputFormat { } - class AtRuleBlockList { + class OutputFormatter { } - class Color { + class Parser { } - class URL { + class ParserState { } - class CalcRuleValueList { + class Positionable { + <> } - class ValueList { + class PrimitiveValue { <> } - class CalcFunction { + class Renderable { + <> } - class LineName { + class Rule { } - class Value { - <> + class RuleContainer { + <> + } + class RuleSet { + } + class RuleValueList { + } + class Selector { + } + class Settings { } class Size { } - class CSSString { + class SourceException { } - class PrimitiveValue { - <> + class SpecificityCalculator { } - class CSSFunction { + class URL { } - class RuleValueList { + class UnexpectedEOFException { } - class Commentable { - <> + class UnexpectedTokenException { } - class Comment { + class Value { + <> + } + class ValueList { + <> } - RuleSet <|-- DeclarationBlock: inheritance - Renderable <|.. RuleSet: realization - Commentable <|.. RuleSet: realization - RuleSet <|-- AtRuleSet: inheritance + Anchor ..> ParserState: dependency + CSSListItem <|-- AtRule: inheritance + AtRule <|.. AtRuleBlockList: realization + CSSBlockList <|-- AtRuleBlockList: inheritance AtRule <|.. AtRuleSet: realization - Selector <|-- KeyframeSelector: inheritance - Renderable <|-- AtRule: inheritance - Commentable <|-- AtRule: inheritance - AtRule <|.. Charset: realization - AtRule <|.. Import: realization - AtRule <|.. CSSNamespace: realization - Renderable <|.. Rule: realization - Commentable <|.. Rule: realization - SourceException <|-- OutputException: inheritance - UnexpectedTokenException <|-- UnexpectedEOFException: inheritance - Exception <|-- SourceException: inheritance - SourceException <|-- UnexpectedTokenException: inheritance + RuleSet <|-- AtRuleSet: inheritance CSSList <|-- CSSBlockList: inheritance + Renderable <|-- CSSElement: inheritance + ValueList <|-- CSSFunction: inheritance + CSSElement <|.. CSSList: realization + CSSListItem <|.. CSSList: realization + CSSList ..> Charset: dependency + CSSList ..> Import: dependency + Positionable <|.. CSSList: realization + Commentable <|-- CSSListItem: inheritance + Renderable <|-- CSSListItem: inheritance + AtRule <|.. CSSNamespace: realization + Positionable <|.. CSSNamespace: realization + PrimitiveValue <|-- CSSString: inheritance + CSSFunction <|-- CalcFunction: inheritance + RuleValueList <|-- CalcRuleValueList: inheritance + AtRule <|.. Charset: realization + Charset ..> CSSString: dependency + Positionable <|.. Charset: realization + CSSFunction <|-- Color: inheritance + Positionable <|.. Comment: realization + Renderable <|.. Comment: realization + CSSElement <|.. DeclarationBlock: realization + CSSListItem <|.. DeclarationBlock: realization + Positionable <|.. DeclarationBlock: realization + RuleContainer <|.. DeclarationBlock: realization + DeclarationBlock ..> RuleSet : dependency + DeclarationBlock ..> Selector: dependency CSSBlockList <|-- Document: inheritance - Renderable <|.. CSSList: realization - Commentable <|.. CSSList: realization - CSSList <|-- KeyFrame: inheritance + AtRule <|.. Import: realization + Positionable <|.. Import: realization AtRule <|.. KeyFrame: realization - CSSBlockList <|-- AtRuleBlockList: inheritance - AtRule <|.. AtRuleBlockList: realization - CSSFunction <|-- Color: inheritance - PrimitiveValue <|-- URL: inheritance - RuleValueList <|-- CalcRuleValueList: inheritance - Value <|-- ValueList: inheritance - CSSFunction <|-- CalcFunction: inheritance + CSSList <|-- KeyFrame: inheritance + Selector <|-- KeyframeSelector: inheritance ValueList <|-- LineName: inheritance - Renderable <|.. Value: realization - PrimitiveValue <|-- Size: inheritance - PrimitiveValue <|-- CSSString: inheritance + SourceException <|-- OutputException: inheritance + OutputFormat ..> OutputFormatter: dependency + OutputFormatter ..> OutputFormat: dependency + Parser ..> ParserState: dependency + ParserState ..> Settings: dependency Value <|-- PrimitiveValue: inheritance - ValueList <|-- CSSFunction: inheritance + CSSElement <|.. Rule: realization + Commentable <|.. Rule: realization + Positionable <|.. Rule: realization + Rule ..> RuleValueList: dependency + CSSElement <|.. RuleSet: realization + CSSListItem <|.. RuleSet: realization + Positionable <|.. RuleSet: realization + RuleSet ..> Rule: dependency + RuleContainer <|.. RuleSet: realization ValueList <|-- RuleValueList: inheritance - Renderable <|.. Comment: realization + Renderable <|.. Selector: realization + PrimitiveValue <|-- Size: inheritance + Exception <|-- SourceException: inheritance + Positionable <|.. SourceException: realization + URL ..> CSSString: dependency + PrimitiveValue <|-- URL: inheritance + UnexpectedTokenException <|-- UnexpectedEOFException: inheritance + SourceException <|-- UnexpectedTokenException: inheritance + CSSElement <|.. Value: realization + Positionable <|.. Value: realization + Value <|-- ValueList: inheritance - %% end of the generated part - - - Anchor --> "1" ParserState : oParserState - CSSList --> "*" CSSList : aContents - CSSList --> "*" Charset : aContents - CSSList --> "*" Comment : aComments - CSSList --> "*" Import : aContents - CSSList --> "*" RuleSet : aContents - CSSNamespace --> "*" Comment : aComments - Charset --> "*" Comment : aComments - Charset --> "1" CSSString : oCharset - DeclarationBlock --> "*" Selector : aSelectors - Import --> "*" Comment : aComments - OutputFormat --> "1" OutputFormat : oNextLevelFormat - OutputFormat --> "1" OutputFormatter : oFormatter - OutputFormatter --> "1" OutputFormat : oFormat - Parser --> "1" ParserState : oParserState - ParserState --> "1" Settings : oParserSettings - Rule --> "*" Comment : aComments - Rule --> "1" RuleValueList : mValue - RuleSet --> "*" Comment : aComments - RuleSet --> "*" Rule : aRules - URL --> "1" CSSString : oURL - ValueList --> "*" Value : aComponents + CSSList ..> CSSList: dependency + CSSList ..> Comment: dependency + CSSList ..> RuleSet: dependency + CSSNamespace ..> Comment: dependency + Charset ..> Comment: dependency + Import ..> Comment: dependency + OutputFormat ..> OutputFormat: dependency + Rule ..> Comment: dependency + RuleSet ..> Comment: dependency + ValueList ..> Value: dependency ``` +## API and deprecation policy + +Please have a look at our +[API and deprecation policy](docs/API-and-deprecation-policy.md). + +## Contributing + +Contributions in the form of bug reports, feature requests, or pull requests are +more than welcome. :pray: Please have a look at our +[contribution guidelines](CONTRIBUTING.md) to learn more about how to +contribute to PHP-CSS-Parser. + ## Contributors/Thanks to * [oliverklee](https://github.com/oliverklee) for lots of refactorings, code modernizations and CI integrations @@ -801,14 +839,3 @@ classDiagram ### Legacy Support The latest pre-PSR-0 version of this project can be checked with the `0.9.0` tag. - -### Running Tests - -To run all continuous integration (CI) checks for this project (including unit tests), -* run `composer install` to install the development dependencies managed with Composer; -* run `phive install` to install the development dependencies managed with PHIVE; - * [Installation of PHIVE](https://github.com/phar-io/phive?tab=readme-ov-file#getting-phive) -* run `composer ci` to run all static and dynamic CI checks. - -Details of other Composer scripts available (e.g. to run one specific CI check) are provided with `composer list`. - diff --git a/bin/quickdump.php b/bin/quickdump.php index fa622abd4..dc0fe8697 100755 --- a/bin/quickdump.php +++ b/bin/quickdump.php @@ -1,23 +1,29 @@ #!/usr/bin/env php parse(); +$document = $parser->parse(); echo "\n" . '#### Input' . "\n\n```css\n"; -print $sSource; +print $source; echo "\n```\n\n" . '#### Structure (`var_dump()`)' . "\n\n```php\n"; -\var_dump($oDoc); +\var_dump($document); echo "\n```\n\n" . '#### Output (`render()`)' . "\n\n```css\n"; -print $oDoc->render(); +print $document->render(); echo "\n```\n"; diff --git a/composer.json b/composer.json index 2f85e5565..bec30b48d 100644 --- a/composer.json +++ b/composer.json @@ -1,14 +1,13 @@ { "name": "sabberworm/php-css-parser", - "type": "library", "description": "Parser for CSS Files written in PHP", + "license": "MIT", + "type": "library", "keywords": [ "parser", "css", "stylesheet" ], - "homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser", - "license": "MIT", "authors": [ { "name": "Raphael Schweikert" @@ -22,18 +21,23 @@ "email": "jake.github@qzdesign.co.uk" } ], + "homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser", "require": { - "php": ">=7.2.0", - "ext-iconv": "*" + "php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "ext-iconv": "*", + "thecodingmachine/safe": "^1.3 || ^2.5 || ^3.3" }, "require-dev": { - "codacy/coverage": "^1.4.3", - "php-parallel-lint/php-parallel-lint": "^1.4.0", - "phpstan/extension-installer": "^1.4.1", - "phpstan/phpstan": "^1.11.6", - "phpstan/phpstan-phpunit": "^1.4.0", - "phpunit/phpunit": "^8.5.38", - "rector/rector": "^1.2.0" + "php-parallel-lint/php-parallel-lint": "1.4.0", + "phpstan/extension-installer": "1.4.3", + "phpstan/phpstan": "1.12.28 || 2.1.25", + "phpstan/phpstan-phpunit": "1.4.2 || 2.0.7", + "phpstan/phpstan-strict-rules": "1.6.2 || 2.0.6", + "phpunit/phpunit": "8.5.48", + "rawr/phpunit-data-provider": "3.3.1", + "rector/rector": "1.2.10 || 2.1.7", + "rector/type-perfect": "1.0.0 || 2.1.0", + "thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.1" }, "suggest": { "ext-mbstring": "for parsing UTF-8 CSS" @@ -59,7 +63,7 @@ }, "extra": { "branch-alias": { - "dev-main": "9.0.x-dev" + "dev-main": "9.2.x-dev" } }, "scripts": { @@ -67,6 +71,7 @@ "@ci:static", "@ci:dynamic" ], + "ci:composer:normalize": "\"./.phive/composer-normalize\" --dry-run", "ci:dynamic": [ "@ci:tests" ], @@ -75,6 +80,7 @@ "ci:php:rector": "rector --no-progress-bar --dry-run --config=config/rector.php", "ci:php:stan": "phpstan --no-progress --configuration=config/phpstan.neon", "ci:static": [ + "@ci:composer:normalize", "@ci:php:fixer", "@ci:php:lint", "@ci:php:rector", @@ -83,27 +89,37 @@ "ci:tests": [ "@ci:tests:unit" ], + "ci:tests:coverage": "phpunit --do-not-cache-result --coverage-clover=coverage.xml", "ci:tests:sof": "phpunit --stop-on-failure --do-not-cache-result", "ci:tests:unit": "phpunit --do-not-cache-result", + "fix": [ + "@fix:php" + ], + "fix:composer:normalize": "\"./.phive/composer-normalize\" --no-check-lock", "fix:php": [ - "@fix:php:fixer", - "@fix:php:rector" + "@fix:composer:normalize", + "@fix:php:rector", + "@fix:php:fixer" ], "fix:php:fixer": "\"./.phive/php-cs-fixer\" --config=config/php-cs-fixer.php fix bin src tests", "fix:php:rector": "rector --config=config/rector.php", - "phpstan:baseline": "phpstan --configuration=config/phpstan.neon --generate-baseline=config/phpstan-baseline.neon" + "phpstan:baseline": "phpstan --configuration=config/phpstan.neon --generate-baseline=config/phpstan-baseline.neon --allow-empty-baseline" }, "scripts-descriptions": { "ci": "Runs all dynamic and static code checks.", + "ci:composer:normalize": "Checks the formatting and structure of the composer.json.", "ci:dynamic": "Runs all dynamic code checks (i.e., currently, the unit tests).", "ci:php:fixer": "Checks the code style with PHP CS Fixer.", "ci:php:lint": "Checks the syntax of the PHP code.", - "ci:php:stan": "Checks the types with PHPStan.", "ci:php:rector": "Checks the code for possible code updates and refactoring.", + "ci:php:stan": "Checks the types with PHPStan.", "ci:static": "Runs all static code analysis checks for the code.", "ci:tests": "Runs all dynamic tests (i.e., currently, the unit tests).", + "ci:tests:coverage": "Runs the unit tests with code coverage.", "ci:tests:sof": "Runs the unit tests and stops at the first failure.", "ci:tests:unit": "Runs all unit tests.", + "fix": "Runs all fixers", + "fix:composer:normalize": "Reformats and sorts the composer.json file.", "fix:php": "Autofixes all autofixable issues in the PHP code.", "fix:php:fixer": "Fixes autofixable issues found by PHP CS Fixer.", "fix:php:rector": "Fixes autofixable issues found by Rector.", diff --git a/config/php-cs-fixer.php b/config/php-cs-fixer.php index b91c0a134..c10bf59ae 100644 --- a/config/php-cs-fixer.php +++ b/config/php-cs-fixer.php @@ -40,10 +40,6 @@ 'yoda_style' => ['equal' => false, 'identical' => false, 'less_and_greater' => false], // function notation - 'native_function_invocation' => ['include' => ['@all']], - 'nullable_type_declaration' => [ - 'syntax' => 'question_mark', - ], 'nullable_type_declaration_for_default_null_value' => true, // import @@ -54,6 +50,7 @@ 'combine_consecutive_unsets' => true, 'dir_constant' => true, 'is_null' => true, + 'nullable_type_declaration' => true, // namespace notation 'no_leading_namespace_whitespace' => true, @@ -92,11 +89,14 @@ 'semicolon_after_instruction' => true, // strict - // 'declare_strict_types' => true, // Note: We'll need to add some casts first. + 'declare_strict_types' => true, 'strict_param' => true, // string notation 'single_quote' => true, 'string_implicit_backslashes' => ['single_quoted' => 'escape'], + + // whitespace + 'statement_indentation' => false, ] ); diff --git a/config/phpstan-baseline.neon b/config/phpstan-baseline.neon index 82fcb3f45..6205096ae 100644 --- a/config/phpstan-baseline.neon +++ b/config/phpstan-baseline.neon @@ -1,12 +1,115 @@ parameters: ignoreErrors: - - message: "#^Call to an undefined method Sabberworm\\\\CSS\\\\OutputFormat\\:\\:setIndentation\\(\\)\\.$#" + message: '#^Loose comparison via "\=\=" is not allowed\.$#' + identifier: equal.notAllowed + count: 1 + path: ../src/CSSList/CSSList.php + + - + message: '#^Parameter \#2 \$found of class Sabberworm\\CSS\\Parsing\\UnexpectedTokenException constructor expects string, Sabberworm\\CSS\\Value\\CSSFunction\|Sabberworm\\CSS\\Value\\CSSString\|Sabberworm\\CSS\\Value\\LineName\|Sabberworm\\CSS\\Value\\Size\|Sabberworm\\CSS\\Value\\URL given\.$#' + identifier: argument.type + count: 1 + path: ../src/CSSList/CSSList.php + + - + message: '#^Parameters should have "Sabberworm\\CSS\\CSSList\\CSSListItem\|array" types as the only types passed to this method$#' + identifier: typePerfect.narrowPublicClassMethodParamType + count: 1 + path: ../src/CSSList/CSSList.php + + - + message: '#^Parameters should have "string\|null" types as the only types passed to this method$#' + identifier: typePerfect.narrowPublicClassMethodParamType + count: 1 + path: ../src/CSSList/Document.php + + - + message: '#^Negated boolean expression is always true\.$#' + identifier: booleanNot.alwaysTrue + count: 1 + path: ../src/Parsing/ParserState.php + + - + message: '#^Parameters should have "string" types as the only types passed to this method$#' + identifier: typePerfect.narrowPublicClassMethodParamType + count: 1 + path: ../src/RuleSet/DeclarationBlock.php + + - + message: '#^Parameter \#2 \$arguments of class Sabberworm\\CSS\\Value\\CSSFunction constructor expects array\\|Sabberworm\\CSS\\Value\\RuleValueList, Sabberworm\\CSS\\Value\\Value\|string given\.$#' + identifier: argument.type + count: 1 + path: ../src/Value/CSSFunction.php + + - + message: '#^Parameter \#2 \$offset of method Sabberworm\\CSS\\Parsing\\ParserState\:\:peek\(\) expects int\<0, max\>, \-1 given\.$#' + identifier: argument.type count: 2 - path: ../src/OutputFormat.php + path: ../src/Value/CalcFunction.php + + - + message: '#^Cannot call method getSize\(\) on Sabberworm\\CSS\\Value\\Value\|string\.$#' + identifier: method.nonObject + count: 3 + path: ../src/Value/Color.php + + - + message: '#^Parameters should have "float" types as the only types passed to this method$#' + identifier: typePerfect.narrowPublicClassMethodParamType + count: 1 + path: ../src/Value/Size.php + + - + message: '#^Strict comparison using \!\=\= between non\-empty\-string and null will always evaluate to true\.$#' + identifier: notIdentical.alwaysTrue + count: 1 + path: ../src/Value/Size.php + + - + message: '#^Parameter \#2 \$arguments of class Sabberworm\\CSS\\Value\\CSSFunction constructor expects array\\|Sabberworm\\CSS\\Value\\RuleValueList, Sabberworm\\CSS\\Value\\Value\|string given\.$#' + identifier: argument.type + count: 1 + path: ../src/Value/Value.php + + - + message: '#^Parameter \#2 \$offset of method Sabberworm\\CSS\\Parsing\\ParserState\:\:peek\(\) expects int\<0, max\>, \-1 given\.$#' + identifier: argument.type + count: 1 + path: ../src/Value/Value.php + + - + message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertSame\(\) with ''red'' and Sabberworm\\CSS\\Value\\Value will always evaluate to false\.$#' + identifier: staticMethod.impossibleType + count: 1 + path: ../tests/ParserTest.php - - message: "#^Class Sabberworm\\\\CSS\\\\Value\\\\Size constructor invoked with 5 parameters, 1\\-4 required\\.$#" + message: '#^Parameter \#1 \$value of method Sabberworm\\CSS\\Rule\\Rule\:\:setValue\(\) expects Sabberworm\\CSS\\Value\\RuleValueList\|string\|null, Sabberworm\\CSS\\Value\\Size given\.$#' + identifier: argument.type + count: 3 + path: ../tests/RuleSet/DeclarationBlockTest.php + + - + message: '#^Parameter \#1 \$type of class Sabberworm\\CSS\\CSSList\\AtRuleBlockList constructor expects non\-empty\-string, '''' given\.$#' + identifier: argument.type + count: 3 + path: ../tests/Unit/CSSList/AtRuleBlockListTest.php + + - + message: '#^Parameter \#1 \$value of method Sabberworm\\CSS\\Rule\\Rule\:\:setValue\(\) expects Sabberworm\\CSS\\Value\\RuleValueList\|string\|null, Sabberworm\\CSS\\Value\\CSSFunction given\.$#' + identifier: argument.type count: 2 - path: ../src/RuleSet/DeclarationBlock.php + path: ../tests/Unit/CSSList/CSSBlockListTest.php + - + message: '#^Parameter \#1 \$value of method Sabberworm\\CSS\\Rule\\Rule\:\:setValue\(\) expects Sabberworm\\CSS\\Value\\RuleValueList\|string\|null, Sabberworm\\CSS\\Value\\CSSString given\.$#' + identifier: argument.type + count: 10 + path: ../tests/Unit/CSSList/CSSBlockListTest.php + + - + message: '#^Parameter \#1 \$selectors of method Sabberworm\\CSS\\CSSList\\CSSList\:\:removeDeclarationBlockBySelector\(\) expects array\\|Sabberworm\\CSS\\RuleSet\\DeclarationBlock\|string, array\ given\.$#' + identifier: argument.type + count: 2 + path: ../tests/Unit/CSSList/CSSListTest.php diff --git a/config/phpstan.neon b/config/phpstan.neon index 3d7611a6d..55ba682af 100644 --- a/config/phpstan.neon +++ b/config/phpstan.neon @@ -6,13 +6,23 @@ parameters: # Don't be overly greedy on machines with more CPU's to be a good neighbor especially on CI maximumNumberOfProcesses: 5 - level: 1 + phpVersion: 70200 + + level: 6 - scanDirectories: - - %currentWorkingDirectory%/bin/ - - %currentWorkingDirectory%/src/ - - %currentWorkingDirectory%/tests/ paths: - %currentWorkingDirectory%/bin/ - %currentWorkingDirectory%/src/ - %currentWorkingDirectory%/tests/ + + type_perfect: + no_mixed_property: true + no_mixed_caller: true + null_over_false: true + narrow_param: true + narrow_return: true + + ignoreErrors: + - + message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) .* will always evaluate to#' + path: '../tests/' diff --git a/config/rector.php b/config/rector.php index 978e00642..61df84182 100644 --- a/config/rector.php +++ b/config/rector.php @@ -3,6 +3,9 @@ declare(strict_types=1); use Rector\Config\RectorConfig; +use Rector\PHPUnit\Set\PHPUnitSetList; +use Rector\Set\ValueObject\LevelSetList; +use Rector\Set\ValueObject\SetList; return RectorConfig::configure() ->withPaths( @@ -11,9 +14,24 @@ __DIR__ . '/../tests', ] ) - ->withPhpSets() - ->withRules( - [ - // AddVoidReturnTypeWhereNoReturnRector::class, - ] - ); + ->withSets([ + // Rector sets + + LevelSetList::UP_TO_PHP_72, + + // SetList::CODE_QUALITY, + // SetList::CODING_STYLE, + // SetList::DEAD_CODE, + // SetList::EARLY_RETURN, + // SetList::INSTANCEOF, + // SetList::NAMING, + // SetList::PRIVATIZATION, + SetList::STRICT_BOOLEANS, + SetList::TYPE_DECLARATION, + + // PHPUnit sets + + PHPUnitSetList::PHPUNIT_80, + // PHPUnitSetList::PHPUNIT_CODE_QUALITY, + ]) + ->withImportNames(true, true, false); diff --git a/docs/API-and-deprecation-policy.md b/docs/API-and-deprecation-policy.md new file mode 100644 index 000000000..57e2acec7 --- /dev/null +++ b/docs/API-and-deprecation-policy.md @@ -0,0 +1,52 @@ +# API and Deprecation Policy + +## API Policy + +The code in this library is intended to be called by other projects. It is not +intended to be extended. If you want to extend any classes, you're on your own, +and your code might break with any new release of this library. + +Any classes, methods and properties that are `public` and not marked as +`@internal` are considered to be part of the API. Those methods will continue +working in a compatible way over minor and bug-fix releases according +to [Semantic Versioning](https://semver.org/), though we might change the native +type declarations in a way that could break subclasses. + +Any classes, methods and properties that are `protected` or `private` are _not_ +considered part of the API. Please do not rely on them. If you do, you're on +your own. + +Any code that is marked as `@internal` is subject to change or removal without +notice. Please do not call it. There be dragons. + +If a class is marked as `@internal`, all properties and methods of this class +are by definition considered to be internal as well. + +When we change some code from public to `@internal` in a release, the first +release that might change that code in a breaking way will be the next major +release after that. This will allow you to change your code accordingly. We'll +also add since which version the code is internal. + +For example, we might mark some code as `@internal` in version 8.7.0. The first +version that possibly changes this code in a breaking way will then be version +9.0.0. + +Before you upgrade your code to the next major version of this library, please +update to the latest release of the previous major version and make sure that +your code does not reference any code that is marked as `@internal`. + +## Deprecation Policy + +Code that we plan to remove is marked as `@deprecated`. In the corresponding +annotation, we also note in which release the code will be removed. + +When we mark some code as `@deprecated` in a release, we'll usually remove it in +the next major release. We'll also add since which version the code is +deprecated. + +For example, when we mark some code as `@deprecated` in version 8.7.0, we'll +remove it in version 9.0.0 (or sometimes a later major release). + +Before you upgrade your code to the next major version of this library, please +update to the latest release of the previous major version and make sure that +your code does not reference any code that is marked as `@deprecated`. diff --git a/docs/release-checklist.md b/docs/release-checklist.md new file mode 100644 index 000000000..48d50b7e6 --- /dev/null +++ b/docs/release-checklist.md @@ -0,0 +1,15 @@ +# Steps to release a new version + +1. In the [composer.json](../composer.json), update the `branch-alias` entry to + point to the release _after_ the upcoming release. +1. In the [CHANGELOG.md](../CHANGELOG.md), create a new section with subheadings + for changes _after_ the upcoming release, set the version number for the + upcoming release, and remove any empty sections. +1. Update the target milestone in the Dependabot configuration. +1. Create a pull request "Prepare release of version x.y.z" with those changes. +1. Have the pull request reviewed and merged. +1. Tag the new release. +1. In the + [Releases tab](https://github.com/MyIntervals/PHP-CSS-Parser/releases), + create a new release and copy the change log entries to the new release. +1. Post about the new release on social media. diff --git a/phpunit.xml b/phpunit.xml index 1060f3299..aab1f10c4 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -2,10 +2,13 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/8.5/phpunit.xsd" beStrictAboutChangesToGlobalState="true" - beStrictAboutCoversAnnotation="true" + beStrictAboutOutputDuringTests="true" + beStrictAboutTodoAnnotatedTests="true" cacheResult="false" colors="true" + convertDeprecationsToExceptions="true" forceCoversAnnotation="true" + verbose="true" > diff --git a/src/CSSElement.php b/src/CSSElement.php new file mode 100644 index 000000000..944aabe2c --- /dev/null +++ b/src/CSSElement.php @@ -0,0 +1,17 @@ +|null $lineNumber */ - public function __construct($sType, $sArgs = '', $iLineNo = 0) + public function __construct(string $type, string $arguments = '', ?int $lineNumber = null) { - parent::__construct($iLineNo); - $this->sType = $sType; - $this->sArgs = $sArgs; + parent::__construct($lineNumber); + $this->type = $type; + $this->arguments = $arguments; } /** - * @return string + * @return non-empty-string */ - public function atRuleName() + public function atRuleName(): string { - return $this->sType; + return $this->type; } - /** - * @return string - */ - public function atRuleArgs() + public function atRuleArgs(): string { - return $this->sArgs; + return $this->arguments; } - public function __toString(): string - { - return $this->render(new OutputFormat()); - } - - public function render(OutputFormat $oOutputFormat): string + /** + * @return non-empty-string + */ + public function render(OutputFormat $outputFormat): string { - $sResult = $oOutputFormat->comments($this); - $sResult .= $oOutputFormat->sBeforeAtRuleBlock; - $sArgs = $this->sArgs; - if ($sArgs) { - $sArgs = ' ' . $sArgs; + $formatter = $outputFormat->getFormatter(); + $result = $formatter->comments($this); + $result .= $outputFormat->getContentBeforeAtRuleBlock(); + $arguments = $this->arguments; + if ($arguments !== '') { + $arguments = ' ' . $arguments; } - $sResult .= "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{"; - $sResult .= $this->renderListContents($oOutputFormat); - $sResult .= '}'; - $sResult .= $oOutputFormat->sAfterAtRuleBlock; - return $sResult; + $result .= "@{$this->type}$arguments{$formatter->spaceBeforeOpeningBrace()}{"; + $result .= $this->renderListContents($outputFormat); + $result .= '}'; + $result .= $outputFormat->getContentAfterAtRuleBlock(); + return $result; } public function isRootList(): bool diff --git a/src/CSSList/CSSBlockList.php b/src/CSSList/CSSBlockList.php index 8c58fbf9b..122ac5d1d 100644 --- a/src/CSSList/CSSBlockList.php +++ b/src/CSSList/CSSBlockList.php @@ -1,10 +1,14 @@ $aResult + * Gets all `DeclarationBlock` objects recursively, no matter how deeply nested the selectors are. * - * @return void + * @return list */ - protected function allDeclarationBlocks(array &$aResult) + public function getAllDeclarationBlocks(): array { - foreach ($this->aContents as $mContent) { - if ($mContent instanceof DeclarationBlock) { - $aResult[] = $mContent; - } elseif ($mContent instanceof CSSBlockList) { - $mContent->allDeclarationBlocks($aResult); + $result = []; + + foreach ($this->contents as $item) { + if ($item instanceof DeclarationBlock) { + $result[] = $item; + } elseif ($item instanceof CSSBlockList) { + $result = \array_merge($result, $item->getAllDeclarationBlocks()); } } + + return $result; } /** - * @param array $aResult + * Returns all `RuleSet` objects recursively found in the tree, no matter how deeply nested the rule sets are. * - * @return void + * @return list */ - protected function allRuleSets(array &$aResult) + public function getAllRuleSets(): array { - foreach ($this->aContents as $mContent) { - if ($mContent instanceof RuleSet) { - $aResult[] = $mContent; - } elseif ($mContent instanceof CSSBlockList) { - $mContent->allRuleSets($aResult); + $result = []; + + foreach ($this->contents as $item) { + if ($item instanceof RuleSet) { + $result[] = $item; + } elseif ($item instanceof CSSBlockList) { + $result = \array_merge($result, $item->getAllRuleSets()); + } elseif ($item instanceof DeclarationBlock) { + $result[] = $item->getRuleSet(); } } + + return $result; } /** - * @param CSSList|Rule|RuleSet|Value $oElement - * @param array $aResult - * @param string|null $sSearchString - * @param bool $bSearchInFunctionArguments + * Returns all `Value` objects found recursively in `Rule`s in the tree. + * + * @param CSSElement|null $element + * This is the `CSSList` or `RuleSet` to start the search from (defaults to the whole document). + * @param string|null $ruleSearchPattern + * This allows filtering rules by property name + * (e.g. if "color" is passed, only `Value`s from `color` properties will be returned, + * or if "font-" is provided, `Value`s from all font rules, like `font-size`, and including `font` itself, + * will be returned). + * @param bool $searchInFunctionArguments whether to also return `Value` objects used as `CSSFunction` arguments. * - * @return void + * @return list + * + * @see RuleSet->getRules() */ - protected function allValues($oElement, array &$aResult, $sSearchString = null, $bSearchInFunctionArguments = false) - { - if ($oElement instanceof CSSBlockList) { - foreach ($oElement->getContents() as $oContent) { - $this->allValues($oContent, $aResult, $sSearchString, $bSearchInFunctionArguments); + public function getAllValues( + ?CSSElement $element = null, + ?string $ruleSearchPattern = null, + bool $searchInFunctionArguments = false + ): array { + $element = $element ?? $this; + + $result = []; + if ($element instanceof CSSBlockList) { + foreach ($element->getContents() as $contentItem) { + // Statement at-rules are skipped since they do not contain values. + if ($contentItem instanceof CSSElement) { + $result = \array_merge( + $result, + $this->getAllValues($contentItem, $ruleSearchPattern, $searchInFunctionArguments) + ); + } + } + } elseif ($element instanceof RuleContainer) { + foreach ($element->getRules($ruleSearchPattern) as $rule) { + $result = \array_merge( + $result, + $this->getAllValues($rule, $ruleSearchPattern, $searchInFunctionArguments) + ); } - } elseif ($oElement instanceof RuleSet) { - foreach ($oElement->getRules($sSearchString) as $oRule) { - $this->allValues($oRule, $aResult, $sSearchString, $bSearchInFunctionArguments); + } elseif ($element instanceof Rule) { + $value = $element->getValue(); + // `string` values are discarded. + if ($value instanceof CSSElement) { + $result = \array_merge( + $result, + $this->getAllValues($value, $ruleSearchPattern, $searchInFunctionArguments) + ); } - } elseif ($oElement instanceof Rule) { - $this->allValues($oElement->getValue(), $aResult, $sSearchString, $bSearchInFunctionArguments); - } elseif ($oElement instanceof ValueList) { - if ($bSearchInFunctionArguments || !($oElement instanceof CSSFunction)) { - foreach ($oElement->getListComponents() as $mComponent) { - $this->allValues($mComponent, $aResult, $sSearchString, $bSearchInFunctionArguments); + } elseif ($element instanceof ValueList) { + if ($searchInFunctionArguments || !($element instanceof CSSFunction)) { + foreach ($element->getListComponents() as $component) { + // `string` components are discarded. + if ($component instanceof CSSElement) { + $result = \array_merge( + $result, + $this->getAllValues($component, $ruleSearchPattern, $searchInFunctionArguments) + ); + } } } - } else { - // Non-List `Value` or `CSSString` (CSS identifier) - $aResult[] = $oElement; + } elseif ($element instanceof Value) { + $result[] = $element; } + + return $result; } /** - * @param array $aResult - * @param string|null $sSpecificitySearch - * - * @return void + * @return list */ - protected function allSelectors(array &$aResult, $sSpecificitySearch = null) + protected function getAllSelectors(?string $specificitySearch = null): array { - /** @var array $aDeclarationBlocks */ - $aDeclarationBlocks = []; - $this->allDeclarationBlocks($aDeclarationBlocks); - foreach ($aDeclarationBlocks as $oBlock) { - foreach ($oBlock->getSelectors() as $oSelector) { - if ($sSpecificitySearch === null) { - $aResult[] = $oSelector; + $result = []; + + foreach ($this->getAllDeclarationBlocks() as $declarationBlock) { + foreach ($declarationBlock->getSelectors() as $selector) { + if ($specificitySearch === null) { + $result[] = $selector; } else { - $sComparator = '==='; - $aSpecificitySearch = \explode(' ', $sSpecificitySearch); - $iTargetSpecificity = $aSpecificitySearch[0]; - if (\count($aSpecificitySearch) > 1) { - $sComparator = $aSpecificitySearch[0]; - $iTargetSpecificity = $aSpecificitySearch[1]; + $comparator = '==='; + $expressionParts = \explode(' ', $specificitySearch); + $targetSpecificity = $expressionParts[0]; + if (\count($expressionParts) > 1) { + $comparator = $expressionParts[0]; + $targetSpecificity = $expressionParts[1]; } - $iTargetSpecificity = (int) $iTargetSpecificity; - $iSelectorSpecificity = $oSelector->getSpecificity(); - $bMatches = false; - switch ($sComparator) { + $targetSpecificity = (int) $targetSpecificity; + $selectorSpecificity = $selector->getSpecificity(); + $comparatorMatched = false; + switch ($comparator) { case '<=': - $bMatches = $iSelectorSpecificity <= $iTargetSpecificity; + $comparatorMatched = $selectorSpecificity <= $targetSpecificity; break; case '<': - $bMatches = $iSelectorSpecificity < $iTargetSpecificity; + $comparatorMatched = $selectorSpecificity < $targetSpecificity; break; case '>=': - $bMatches = $iSelectorSpecificity >= $iTargetSpecificity; + $comparatorMatched = $selectorSpecificity >= $targetSpecificity; break; case '>': - $bMatches = $iSelectorSpecificity > $iTargetSpecificity; + $comparatorMatched = $selectorSpecificity > $targetSpecificity; break; default: - $bMatches = $iSelectorSpecificity === $iTargetSpecificity; + $comparatorMatched = $selectorSpecificity === $targetSpecificity; break; } - if ($bMatches) { - $aResult[] = $oSelector; + if ($comparatorMatched) { + $result[] = $selector; } } } } + + return $result; } } diff --git a/src/CSSList/CSSList.php b/src/CSSList/CSSList.php index a99a60499..e09a03a98 100644 --- a/src/CSSList/CSSList.php +++ b/src/CSSList/CSSList.php @@ -1,301 +1,285 @@ - */ - protected $aComments; - - /** - * @var array - */ - protected $aContents; + use CommentContainer; + use Position; /** - * @var int + * @var array, CSSListItem> + * + * @internal since 8.8.0 */ - protected $iLineNo; + protected $contents = []; /** - * @param int $iLineNo + * @param int<1, max>|null $lineNumber */ - public function __construct($iLineNo = 0) + public function __construct(?int $lineNumber = null) { - $this->aComments = []; - $this->aContents = []; - $this->iLineNo = $iLineNo; + $this->setPosition($lineNumber); } /** * @throws UnexpectedTokenException * @throws SourceException + * + * @internal since V8.8.0 */ - public static function parseList(ParserState $oParserState, CSSList $oList): void + public static function parseList(ParserState $parserState, CSSList $list): void { - $bIsRoot = $oList instanceof Document; - if (\is_string($oParserState)) { - $oParserState = new ParserState($oParserState, Settings::create()); - } - $bLenientParsing = $oParserState->getSettings()->bLenientParsing; - $aComments = []; - while (!$oParserState->isEnd()) { - $aComments = \array_merge($aComments, $oParserState->consumeWhiteSpace()); - $oListItem = null; - if ($bLenientParsing) { + $isRoot = $list instanceof Document; + $usesLenientParsing = $parserState->getSettings()->usesLenientParsing(); + $comments = []; + while (!$parserState->isEnd()) { + $comments = \array_merge($comments, $parserState->consumeWhiteSpace()); + $listItem = null; + if ($usesLenientParsing) { try { - $oListItem = self::parseListItem($oParserState, $oList); + $listItem = self::parseListItem($parserState, $list); } catch (UnexpectedTokenException $e) { - $oListItem = false; + $listItem = false; } } else { - $oListItem = self::parseListItem($oParserState, $oList); + $listItem = self::parseListItem($parserState, $list); } - if ($oListItem === null) { + if ($listItem === null) { // List parsing finished return; } - if ($oListItem) { - $oListItem->addComments($aComments); - $oList->append($oListItem); + if ($listItem) { + $listItem->addComments($comments); + $list->append($listItem); } - $aComments = $oParserState->consumeWhiteSpace(); + $comments = $parserState->consumeWhiteSpace(); } - $oList->addComments($aComments); - if (!$bIsRoot && !$bLenientParsing) { - throw new SourceException('Unexpected end of document', $oParserState->currentLine()); + $list->addComments($comments); + if (!$isRoot && !$usesLenientParsing) { + throw new SourceException('Unexpected end of document', $parserState->currentLine()); } } /** - * @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|DeclarationBlock|false|null + * @return CSSListItem|false|null + * If `null` is returned, it means the end of the list has been reached. + * If `false` is returned, it means an invalid item has been encountered, + * but parsing of the next item should proceed. * * @throws SourceException * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ - private static function parseListItem(ParserState $oParserState, CSSList $oList) + private static function parseListItem(ParserState $parserState, CSSList $list) { - $bIsRoot = $oList instanceof Document; - if ($oParserState->comes('@')) { - $oAtRule = self::parseAtRule($oParserState); - if ($oAtRule instanceof Charset) { - if (!$bIsRoot) { + $isRoot = $list instanceof Document; + if ($parserState->comes('@')) { + $atRule = self::parseAtRule($parserState); + if ($atRule instanceof Charset) { + if (!$isRoot) { throw new UnexpectedTokenException( '@charset may only occur in root document', '', 'custom', - $oParserState->currentLine() + $parserState->currentLine() ); } - if (\count($oList->getContents()) > 0) { + if (\count($list->getContents()) > 0) { throw new UnexpectedTokenException( '@charset must be the first parseable token in a document', '', 'custom', - $oParserState->currentLine() + $parserState->currentLine() ); } - $oParserState->setCharset($oAtRule->getCharset()); + $parserState->setCharset($atRule->getCharset()); } - return $oAtRule; - } elseif ($oParserState->comes('}')) { - if ($bIsRoot) { - if ($oParserState->getSettings()->bLenientParsing) { - return DeclarationBlock::parse($oParserState); + return $atRule; + } elseif ($parserState->comes('}')) { + if ($isRoot) { + if ($parserState->getSettings()->usesLenientParsing()) { + return DeclarationBlock::parse($parserState) ?? false; } else { - throw new SourceException('Unopened {', $oParserState->currentLine()); + throw new SourceException('Unopened {', $parserState->currentLine()); } } else { // End of list return null; } } else { - return DeclarationBlock::parse($oParserState, $oList); + return DeclarationBlock::parse($parserState, $list) ?? false; } } /** - * @param ParserState $oParserState - * - * @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|null - * * @throws SourceException * @throws UnexpectedTokenException * @throws UnexpectedEOFException */ - private static function parseAtRule(ParserState $oParserState) + private static function parseAtRule(ParserState $parserState): ?CSSListItem { - $oParserState->consume('@'); - $sIdentifier = $oParserState->parseIdentifier(); - $iIdentifierLineNum = $oParserState->currentLine(); - $oParserState->consumeWhiteSpace(); - if ($sIdentifier === 'import') { - $oLocation = URL::parse($oParserState); - $oParserState->consumeWhiteSpace(); - $sMediaQuery = null; - if (!$oParserState->comes(';')) { - $sMediaQuery = \trim($oParserState->consumeUntil([';', ParserState::EOF])); + $parserState->consume('@'); + $identifier = $parserState->parseIdentifier(); + $identifierLineNumber = $parserState->currentLine(); + $parserState->consumeWhiteSpace(); + if ($identifier === 'import') { + $location = URL::parse($parserState); + $parserState->consumeWhiteSpace(); + $mediaQuery = null; + if (!$parserState->comes(';')) { + $mediaQuery = \trim($parserState->consumeUntil([';', ParserState::EOF])); + if ($mediaQuery === '') { + $mediaQuery = null; + } } - $oParserState->consumeUntil([';', ParserState::EOF], true, true); - return new Import($oLocation, $sMediaQuery ?: null, $iIdentifierLineNum); - } elseif ($sIdentifier === 'charset') { - $oCharsetString = CSSString::parse($oParserState); - $oParserState->consumeWhiteSpace(); - $oParserState->consumeUntil([';', ParserState::EOF], true, true); - return new Charset($oCharsetString, $iIdentifierLineNum); - } elseif (self::identifierIs($sIdentifier, 'keyframes')) { - $oResult = new KeyFrame($iIdentifierLineNum); - $oResult->setVendorKeyFrame($sIdentifier); - $oResult->setAnimationName(\trim($oParserState->consumeUntil('{', false, true))); - CSSList::parseList($oParserState, $oResult); - if ($oParserState->comes('}')) { - $oParserState->consume('}'); + $parserState->consumeUntil([';', ParserState::EOF], true, true); + return new Import($location, $mediaQuery, $identifierLineNumber); + } elseif ($identifier === 'charset') { + $charsetString = CSSString::parse($parserState); + $parserState->consumeWhiteSpace(); + $parserState->consumeUntil([';', ParserState::EOF], true, true); + return new Charset($charsetString, $identifierLineNumber); + } elseif (self::identifierIs($identifier, 'keyframes')) { + $result = new KeyFrame($identifierLineNumber); + $result->setVendorKeyFrame($identifier); + $result->setAnimationName(\trim($parserState->consumeUntil('{', false, true))); + CSSList::parseList($parserState, $result); + if ($parserState->comes('}')) { + $parserState->consume('}'); } - return $oResult; - } elseif ($sIdentifier === 'namespace') { - $sPrefix = null; - $mUrl = Value::parsePrimitiveValue($oParserState); - if (!$oParserState->comes(';')) { - $sPrefix = $mUrl; - $mUrl = Value::parsePrimitiveValue($oParserState); + return $result; + } elseif ($identifier === 'namespace') { + $prefix = null; + $url = Value::parsePrimitiveValue($parserState); + if (!$parserState->comes(';')) { + $prefix = $url; + $url = Value::parsePrimitiveValue($parserState); } - $oParserState->consumeUntil([';', ParserState::EOF], true, true); - if ($sPrefix !== null && !\is_string($sPrefix)) { - throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom', $iIdentifierLineNum); + $parserState->consumeUntil([';', ParserState::EOF], true, true); + if ($prefix !== null && !\is_string($prefix)) { + throw new UnexpectedTokenException('Wrong namespace prefix', $prefix, 'custom', $identifierLineNumber); } - if (!($mUrl instanceof CSSString || $mUrl instanceof URL)) { + if (!($url instanceof CSSString || $url instanceof URL)) { throw new UnexpectedTokenException( 'Wrong namespace url of invalid type', - $mUrl, + $url, 'custom', - $iIdentifierLineNum + $identifierLineNumber ); } - return new CSSNamespace($mUrl, $sPrefix, $iIdentifierLineNum); + return new CSSNamespace($url, $prefix, $identifierLineNumber); } else { // Unknown other at rule (font-face or such) - $sArgs = \trim($oParserState->consumeUntil('{', false, true)); - if (\substr_count($sArgs, '(') != \substr_count($sArgs, ')')) { - if ($oParserState->getSettings()->bLenientParsing) { + $arguments = \trim($parserState->consumeUntil('{', false, true)); + if (\substr_count($arguments, '(') !== \substr_count($arguments, ')')) { + if ($parserState->getSettings()->usesLenientParsing()) { return null; } else { - throw new SourceException('Unmatched brace count in media query', $oParserState->currentLine()); + throw new SourceException('Unmatched brace count in media query', $parserState->currentLine()); } } - $bUseRuleSet = true; - foreach (\explode('/', AtRule::BLOCK_RULES) as $sBlockRuleName) { - if (self::identifierIs($sIdentifier, $sBlockRuleName)) { - $bUseRuleSet = false; + $useRuleSet = true; + foreach (\explode('/', AtRule::BLOCK_RULES) as $blockRuleName) { + if (self::identifierIs($identifier, $blockRuleName)) { + $useRuleSet = false; break; } } - if ($bUseRuleSet) { - $oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum); - RuleSet::parseRuleSet($oParserState, $oAtRule); + if ($useRuleSet) { + $atRule = new AtRuleSet($identifier, $arguments, $identifierLineNumber); + RuleSet::parseRuleSet($parserState, $atRule); } else { - $oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum); - CSSList::parseList($oParserState, $oAtRule); - if ($oParserState->comes('}')) { - $oParserState->consume('}'); + $atRule = new AtRuleBlockList($identifier, $arguments, $identifierLineNumber); + CSSList::parseList($parserState, $atRule); + if ($parserState->comes('}')) { + $parserState->consume('}'); } } - return $oAtRule; + return $atRule; } } /** * Tests an identifier for a given value. Since identifiers are all keywords, they can be vendor-prefixed. * We need to check for these versions too. - * - * @param string $sIdentifier - * @param string $sMatch */ - private static function identifierIs($sIdentifier, $sMatch): bool + private static function identifierIs(string $identifier, string $match): bool { - return (\strcasecmp($sIdentifier, $sMatch) === 0) - ?: \preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1; - } + if (\strcasecmp($identifier, $match) === 0) { + return true; + } - /** - * @return int - */ - public function getLineNo() - { - return $this->iLineNo; + return preg_match("/^(-\\w+-)?$match$/i", $identifier) === 1; } /** * Prepends an item to the list of contents. - * - * @param RuleSet|CSSList|Import|Charset $oItem */ - public function prepend($oItem): void + public function prepend(CSSListItem $item): void { - \array_unshift($this->aContents, $oItem); + \array_unshift($this->contents, $item); } /** * Appends an item to the list of contents. - * - * @param RuleSet|CSSList|Import|Charset $oItem */ - public function append($oItem): void + public function append(CSSListItem $item): void { - $this->aContents[] = $oItem; + $this->contents[] = $item; } /** * Splices the list of contents. * - * @param int $iOffset - * @param int $iLength - * @param array $mReplacement + * @param array $replacement */ - public function splice($iOffset, $iLength = null, $mReplacement = null): void + public function splice(int $offset, ?int $length = null, ?array $replacement = null): void { - \array_splice($this->aContents, $iOffset, $iLength, $mReplacement); + \array_splice($this->contents, $offset, $length, $replacement); } /** * Inserts an item in the CSS list before its sibling. If the desired sibling cannot be found, * the item is appended at the end. - * - * @param RuleSet|CSSList|Import|Charset $item - * @param RuleSet|CSSList|Import|Charset $sibling */ - public function insertBefore($item, $sibling): void + public function insertBefore(CSSListItem $item, CSSListItem $sibling): void { - if (\in_array($sibling, $this->aContents, true)) { + if (\in_array($sibling, $this->contents, true)) { $this->replace($sibling, [$item, $sibling]); } else { $this->append($item); @@ -305,52 +289,53 @@ public function insertBefore($item, $sibling): void /** * Removes an item from the CSS list. * - * @param RuleSet|Import|Charset|CSSList $oItemToRemove - * May be a RuleSet (most likely a DeclarationBlock), a Import, - * a Charset or another CSSList (most likely a MediaQuery) + * @param CSSListItem $itemToRemove + * May be a `RuleSet` (most likely a `DeclarationBlock`), an `Import`, + * a `Charset` or another `CSSList` (most likely a `MediaQuery`) * * @return bool whether the item was removed */ - public function remove($oItemToRemove) + public function remove(CSSListItem $itemToRemove): bool { - $iKey = \array_search($oItemToRemove, $this->aContents, true); - if ($iKey !== false) { - unset($this->aContents[$iKey]); + $key = \array_search($itemToRemove, $this->contents, true); + if ($key !== false) { + unset($this->contents[$key]); return true; } + return false; } /** * Replaces an item from the CSS list. * - * @param RuleSet|Import|Charset|CSSList $oOldItem + * @param CSSListItem $oldItem * May be a `RuleSet` (most likely a `DeclarationBlock`), an `Import`, a `Charset` * or another `CSSList` (most likely a `MediaQuery`) - * - * @return bool + * @param CSSListItem|array $newItem */ - public function replace($oOldItem, $mNewItem) + public function replace(CSSListItem $oldItem, $newItem): bool { - $iKey = \array_search($oOldItem, $this->aContents, true); - if ($iKey !== false) { - if (\is_array($mNewItem)) { - \array_splice($this->aContents, $iKey, 1, $mNewItem); + $key = \array_search($oldItem, $this->contents, true); + if ($key !== false) { + if (\is_array($newItem)) { + \array_splice($this->contents, $key, 1, $newItem); } else { - \array_splice($this->aContents, $iKey, 1, [$mNewItem]); + \array_splice($this->contents, $key, 1, [$newItem]); } return true; } + return false; } /** - * @param array $aContents + * @param array $contents */ - public function setContents(array $aContents): void + public function setContents(array $contents): void { - $this->aContents = []; - foreach ($aContents as $content) { + $this->contents = []; + foreach ($contents as $content) { $this->append($content); } } @@ -358,120 +343,88 @@ public function setContents(array $aContents): void /** * Removes a declaration block from the CSS list if it matches all given selectors. * - * @param DeclarationBlock|array|string $mSelector the selectors to match - * @param bool $bRemoveAll whether to stop at the first declaration block found or remove all blocks + * @param DeclarationBlock|array|string $selectors the selectors to match + * @param bool $removeAll whether to stop at the first declaration block found or remove all blocks */ - public function removeDeclarationBlockBySelector($mSelector, $bRemoveAll = false): void + public function removeDeclarationBlockBySelector($selectors, bool $removeAll = false): void { - if ($mSelector instanceof DeclarationBlock) { - $mSelector = $mSelector->getSelectors(); + if ($selectors instanceof DeclarationBlock) { + $selectors = $selectors->getSelectors(); } - if (!\is_array($mSelector)) { - $mSelector = \explode(',', $mSelector); + if (!\is_array($selectors)) { + $selectors = \explode(',', $selectors); } - foreach ($mSelector as $iKey => &$mSel) { - if (!($mSel instanceof Selector)) { - if (!Selector::isValid($mSel)) { + foreach ($selectors as $key => &$selector) { + if (!($selector instanceof Selector)) { + if (!Selector::isValid($selector)) { throw new UnexpectedTokenException( "Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.", - $mSel, + $selector, 'custom' ); } - $mSel = new Selector($mSel); + $selector = new Selector($selector); } } - foreach ($this->aContents as $iKey => $mItem) { - if (!($mItem instanceof DeclarationBlock)) { + foreach ($this->contents as $key => $item) { + if (!($item instanceof DeclarationBlock)) { continue; } - if ($mItem->getSelectors() == $mSelector) { - unset($this->aContents[$iKey]); - if (!$bRemoveAll) { + if ($item->getSelectors() == $selectors) { + unset($this->contents[$key]); + if (!$removeAll) { return; } } } } - public function __toString(): string - { - return $this->render(new OutputFormat()); - } - - /** - * @return string - */ - protected function renderListContents(OutputFormat $oOutputFormat) + protected function renderListContents(OutputFormat $outputFormat): string { - $sResult = ''; - $bIsFirst = true; - $oNextLevel = $oOutputFormat; + $result = ''; + $isFirst = true; + $nextLevelFormat = $outputFormat; if (!$this->isRootList()) { - $oNextLevel = $oOutputFormat->nextLevel(); + $nextLevelFormat = $outputFormat->nextLevel(); } - foreach ($this->aContents as $oContent) { - $sRendered = $oOutputFormat->safely(function () use ($oNextLevel, $oContent) { - return $oContent->render($oNextLevel); + $nextLevelFormatter = $nextLevelFormat->getFormatter(); + $formatter = $outputFormat->getFormatter(); + foreach ($this->contents as $listItem) { + $renderedCss = $formatter->safely(static function () use ($nextLevelFormat, $listItem): string { + return $listItem->render($nextLevelFormat); }); - if ($sRendered === null) { + if ($renderedCss === null) { continue; } - if ($bIsFirst) { - $bIsFirst = false; - $sResult .= $oNextLevel->spaceBeforeBlocks(); + if ($isFirst) { + $isFirst = false; + $result .= $nextLevelFormatter->spaceBeforeBlocks(); } else { - $sResult .= $oNextLevel->spaceBetweenBlocks(); + $result .= $nextLevelFormatter->spaceBetweenBlocks(); } - $sResult .= $sRendered; + $result .= $renderedCss; } - if (!$bIsFirst) { + if (!$isFirst) { // Had some output - $sResult .= $oOutputFormat->spaceAfterBlocks(); + $result .= $formatter->spaceAfterBlocks(); } - return $sResult; + return $result; } /** * Return true if the list can not be further outdented. Only important when rendering. - * - * @return bool */ - abstract public function isRootList(); + abstract public function isRootList(): bool; /** * Returns the stored items. * - * @return array - */ - public function getContents() - { - return $this->aContents; - } - - /** - * @param array $aComments - */ - public function addComments(array $aComments): void - { - $this->aComments = \array_merge($this->aComments, $aComments); - } - - /** - * @return array - */ - public function getComments() - { - return $this->aComments; - } - - /** - * @param array $aComments + * @return array, CSSListItem> */ - public function setComments(array $aComments): void + public function getContents(): array { - $this->aComments = $aComments; + return $this->contents; } } diff --git a/src/CSSList/CSSListItem.php b/src/CSSList/CSSListItem.php new file mode 100644 index 000000000..3cf2509b6 --- /dev/null +++ b/src/CSSList/CSSListItem.php @@ -0,0 +1,18 @@ +currentLine()); - CSSList::parseList($oParserState, $oDocument); - return $oDocument; - } - - /** - * Gets all `DeclarationBlock` objects recursively, no matter how deeply nested the selectors are. - * Aliased as `getAllSelectors()`. * - * @return array + * @internal since V8.8.0 */ - public function getAllDeclarationBlocks() + public static function parse(ParserState $parserState): Document { - /** @var array $aResult */ - $aResult = []; - $this->allDeclarationBlocks($aResult); - return $aResult; - } + $document = new Document($parserState->currentLine()); + CSSList::parseList($parserState, $document); - /** - * Returns all `RuleSet` objects recursively found in the tree, no matter how deeply nested the rule sets are. - * - * @return array - */ - public function getAllRuleSets() - { - /** @var array $aResult */ - $aResult = []; - $this->allRuleSets($aResult); - return $aResult; - } - - /** - * Returns all `Value` objects found recursively in `Rule`s in the tree. - * - * @param CSSList|RuleSet|string $mElement - * the `CSSList` or `RuleSet` to start the search from (defaults to the whole document). - * If a string is given, it is used as rule name filter. - * @param bool $bSearchInFunctionArguments whether to also return Value objects used as Function arguments. - * - * @return array - * - * @see RuleSet->getRules() - */ - public function getAllValues($mElement = null, $bSearchInFunctionArguments = false) - { - $sSearchString = null; - if ($mElement === null) { - $mElement = $this; - } elseif (\is_string($mElement)) { - $sSearchString = $mElement; - $mElement = $this; - } - /** @var array $aResult */ - $aResult = []; - $this->allValues($mElement, $aResult, $sSearchString, $bSearchInFunctionArguments); - return $aResult; + return $document; } /** @@ -94,56 +34,28 @@ public function getAllValues($mElement = null, $bSearchInFunctionArguments = fal * Note that this does not yield the full `DeclarationBlock` that the selector belongs to * (and, currently, there is no way to get to that). * - * @param string|null $sSpecificitySearch + * @param string|null $specificitySearch * An optional filter by specificity. * May contain a comparison operator and a number or just a number (defaults to "=="). * - * @return array - * @example `getSelectorsBySpecificity('>= 100')` - */ - public function getSelectorsBySpecificity($sSpecificitySearch = null) - { - /** @var array $aResult */ - $aResult = []; - $this->allSelectors($aResult, $sSpecificitySearch); - return $aResult; - } - - /** - * Expands all shorthand properties to their long value. - * - * @deprecated This will be removed without substitution in version 10.0. - */ - public function expandShorthands(): void - { - foreach ($this->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->expandShorthands(); - } - } - - /** - * Create shorthands properties whenever possible. + * @return list * - * @deprecated This will be removed without substitution in version 10.0. + * @example `getSelectorsBySpecificity('>= 100')` */ - public function createShorthands(): void + public function getSelectorsBySpecificity(?string $specificitySearch = null): array { - foreach ($this->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->createShorthands(); - } + return $this->getAllSelectors($specificitySearch); } /** * Overrides `render()` to make format argument optional. - * - * @param OutputFormat|null $oOutputFormat */ - public function render(?OutputFormat $oOutputFormat = null): string + public function render(?OutputFormat $outputFormat = null): string { - if ($oOutputFormat === null) { - $oOutputFormat = new OutputFormat(); + if ($outputFormat === null) { + $outputFormat = new OutputFormat(); } - return $oOutputFormat->comments($this) . $this->renderListContents($oOutputFormat); + return $outputFormat->getFormatter()->comments($this) . $this->renderListContents($outputFormat); } public function isRootList(): bool diff --git a/src/CSSList/KeyFrame.php b/src/CSSList/KeyFrame.php index 557c3e74a..e632d088b 100644 --- a/src/CSSList/KeyFrame.php +++ b/src/CSSList/KeyFrame.php @@ -1,5 +1,7 @@ vendorKeyFrame = null; - $this->animationName = null; - } + private $animationName = 'none'; /** - * @param string $vendorKeyFrame + * @param non-empty-string $vendorKeyFrame */ - public function setVendorKeyFrame($vendorKeyFrame): void + public function setVendorKeyFrame(string $vendorKeyFrame): void { $this->vendorKeyFrame = $vendorKeyFrame; } /** - * @return string|null + * @return non-empty-string */ - public function getVendorKeyFrame() + public function getVendorKeyFrame(): string { return $this->vendorKeyFrame; } /** - * @param string $animationName + * @param non-empty-string $animationName */ - public function setAnimationName($animationName): void + public function setAnimationName(string $animationName): void { $this->animationName = $animationName; } /** - * @return string|null + * @return non-empty-string */ - public function getAnimationName() + public function getAnimationName(): string { return $this->animationName; } - public function __toString(): string - { - return $this->render(new OutputFormat()); - } - - public function render(OutputFormat $oOutputFormat): string + /** + * @return non-empty-string + */ + public function render(OutputFormat $outputFormat): string { - $sResult = $oOutputFormat->comments($this); - $sResult .= "@{$this->vendorKeyFrame} {$this->animationName}{$oOutputFormat->spaceBeforeOpeningBrace()}{"; - $sResult .= $this->renderListContents($oOutputFormat); - $sResult .= '}'; - return $sResult; + $formatter = $outputFormat->getFormatter(); + $result = $formatter->comments($this); + $result .= "@{$this->vendorKeyFrame} {$this->animationName}{$formatter->spaceBeforeOpeningBrace()}{"; + $result .= $this->renderListContents($outputFormat); + $result .= '}'; + return $result; } public function isRootList(): bool @@ -79,17 +70,17 @@ public function isRootList(): bool } /** - * @return string|null + * @return non-empty-string */ - public function atRuleName() + public function atRuleName(): string { return $this->vendorKeyFrame; } /** - * @return string|null + * @return non-empty-string */ - public function atRuleArgs() + public function atRuleArgs(): string { return $this->animationName; } diff --git a/src/Comment/Comment.php b/src/Comment/Comment.php index c698018a6..33188988f 100644 --- a/src/Comment/Comment.php +++ b/src/Comment/Comment.php @@ -1,63 +1,49 @@ |null $lineNumber */ - public function __construct($sComment = '', $iLineNo = 0) + public function __construct(string $commentText = '', ?int $lineNumber = null) { - $this->sComment = $sComment; - $this->iLineNo = $iLineNo; + $this->commentText = $commentText; + $this->setPosition($lineNumber); } - /** - * @return string - */ - public function getComment() + public function getComment(): string { - return $this->sComment; + return $this->commentText; } - /** - * @return int - */ - public function getLineNo() + public function setComment(string $commentText): void { - return $this->iLineNo; + $this->commentText = $commentText; } /** - * @param string $sComment + * @return non-empty-string */ - public function setComment($sComment): void - { - $this->sComment = $sComment; - } - - public function __toString(): string - { - return $this->render(new OutputFormat()); - } - - public function render(OutputFormat $oOutputFormat): string + public function render(OutputFormat $outputFormat): string { - return '/*' . $this->sComment . '*/'; + return '/*' . $this->commentText . '*/'; } } diff --git a/src/Comment/CommentContainer.php b/src/Comment/CommentContainer.php new file mode 100644 index 000000000..87f6ff46c --- /dev/null +++ b/src/Comment/CommentContainer.php @@ -0,0 +1,44 @@ + + */ + protected $comments = []; + + /** + * @param list $comments + */ + public function addComments(array $comments): void + { + $this->comments = \array_merge($this->comments, $comments); + } + + /** + * @return list + */ + public function getComments(): array + { + return $this->comments; + } + + /** + * @param list $comments + */ + public function setComments(array $comments): void + { + $this->comments = $comments; + } +} diff --git a/src/Comment/Commentable.php b/src/Comment/Commentable.php index 5e450bfb3..5f28021de 100644 --- a/src/Comment/Commentable.php +++ b/src/Comment/Commentable.php @@ -1,25 +1,26 @@ $aComments - * - * @return void + * @param list $comments */ - public function addComments(array $aComments); + public function addComments(array $comments): void; /** - * @return array + * @return list */ - public function getComments(); + public function getComments(): array; /** - * @param array $aComments - * - * @return void + * @param list $comments */ - public function setComments(array $aComments); + public function setComments(array $comments): void; } diff --git a/src/OutputFormat.php b/src/OutputFormat.php index 7ada59819..5c493865a 100644 --- a/src/OutputFormat.php +++ b/src/OutputFormat.php @@ -1,28 +1,24 @@ set('Space*Rules', "\n");`) + * The triples (After, Before, Between) can be set using a wildcard + * (e.g. `$outputFormat->set('Space*Rules', "\n");`) + * + * @var string */ - public $sSpaceAfterRuleName = ' '; + private $spaceAfterRuleName = ' '; /** * @var string */ - public $sSpaceBeforeRules = ''; + private $spaceBeforeRules = ''; /** * @var string */ - public $sSpaceAfterRules = ''; + private $spaceAfterRules = ''; /** * @var string */ - public $sSpaceBetweenRules = ''; + private $spaceBetweenRules = ''; /** * @var string */ - public $sSpaceBeforeBlocks = ''; + private $spaceBeforeBlocks = ''; /** * @var string */ - public $sSpaceAfterBlocks = ''; + private $spaceAfterBlocks = ''; /** * @var string */ - public $sSpaceBetweenBlocks = "\n"; + private $spaceBetweenBlocks = "\n"; /** * Content injected in and around at-rule blocks. * * @var string */ - public $sBeforeAtRuleBlock = ''; + private $contentBeforeAtRuleBlock = ''; /** * @var string */ - public $sAfterAtRuleBlock = ''; + private $contentAfterAtRuleBlock = ''; /** * This is what’s printed before and after the comma if a declaration block contains multiple selectors. * * @var string */ - public $sSpaceBeforeSelectorSeparator = ''; + private $spaceBeforeSelectorSeparator = ''; /** * @var string */ - public $sSpaceAfterSelectorSeparator = ' '; + private $spaceAfterSelectorSeparator = ' '; /** - * This is what’s printed after the comma of value lists + * This is what’s inserted before the separator in value lists, by default. * * @var string */ - public $sSpaceBeforeListArgumentSeparator = ''; + private $spaceBeforeListArgumentSeparator = ''; + + /** + * Keys are separators (e.g. `,`). Values are the space sequence to insert, or an empty string. + * + * @var array + */ + private $spaceBeforeListArgumentSeparators = []; /** + * This is what’s inserted after the separator in value lists, by default. + * * @var string */ - public $sSpaceAfterListArgumentSeparator = ''; + private $spaceAfterListArgumentSeparator = ''; + + /** + * Keys are separators (e.g. `,`). Values are the space sequence to insert, or an empty string. + * + * @var array + */ + private $spaceAfterListArgumentSeparators = []; /** * @var string */ - public $sSpaceBeforeOpeningBrace = ' '; + private $spaceBeforeOpeningBrace = ' '; /** * Content injected in and around declaration blocks. * * @var string */ - public $sBeforeDeclarationBlock = ''; + private $contentBeforeDeclarationBlock = ''; /** * @var string */ - public $sAfterDeclarationBlockSelectors = ''; + private $contentAfterDeclarationBlockSelectors = ''; /** * @var string */ - public $sAfterDeclarationBlock = ''; + private $contentAfterDeclarationBlock = ''; /** * Indentation character(s) per level. Only applicable if newlines are used in any of the spacing settings. * * @var string */ - public $sIndentation = "\t"; + private $indentation = "\t"; /** * Output exceptions. * * @var bool */ - public $bIgnoreExceptions = false; + private $shouldIgnoreExceptions = false; /** * Render comments for lists and RuleSets * * @var bool */ - public $bRenderComments = false; + private $shouldRenderComments = false; /** * @var OutputFormatter|null */ - private $oFormatter = null; + private $outputFormatter; /** * @var OutputFormat|null */ - private $oNextLevelFormat = null; + private $nextLevelFormat; /** - * @var int + * @var int<0, max> */ - private $iIndentationLevel = 0; + private $indentationLevel = 0; - public function __construct() {} + /** + * @return non-empty-string + * + * @internal + */ + public function getStringQuotingType(): string + { + return $this->stringQuotingType; + } /** - * @param string $sName + * @param non-empty-string $quotingType * - * @return string|null + * @return $this fluent interface */ - public function get($sName) + public function setStringQuotingType(string $quotingType): self { - $aVarPrefixes = ['a', 's', 'm', 'b', 'f', 'o', 'c', 'i']; - foreach ($aVarPrefixes as $sPrefix) { - $sFieldName = $sPrefix . \ucfirst($sName); - if (isset($this->$sFieldName)) { - return $this->$sFieldName; - } - } - return null; + $this->stringQuotingType = $quotingType; + + return $this; + } + + /** + * @internal + */ + public function usesRgbHashNotation(): bool + { + return $this->usesRgbHashNotation; + } + + /** + * @return $this fluent interface + */ + public function setRGBHashNotation(bool $usesRgbHashNotation): self + { + $this->usesRgbHashNotation = $usesRgbHashNotation; + + return $this; + } + + /** + * @internal + */ + public function shouldRenderSemicolonAfterLastRule(): bool + { + return $this->renderSemicolonAfterLastRule; + } + + /** + * @return $this fluent interface + */ + public function setSemicolonAfterLastRule(bool $renderSemicolonAfterLastRule): self + { + $this->renderSemicolonAfterLastRule = $renderSemicolonAfterLastRule; + + return $this; + } + + /** + * @internal + */ + public function getSpaceAfterRuleName(): string + { + return $this->spaceAfterRuleName; + } + + /** + * @return $this fluent interface + */ + public function setSpaceAfterRuleName(string $whitespace): self + { + $this->spaceAfterRuleName = $whitespace; + + return $this; + } + + /** + * @internal + */ + public function getSpaceBeforeRules(): string + { + return $this->spaceBeforeRules; + } + + /** + * @return $this fluent interface + */ + public function setSpaceBeforeRules(string $whitespace): self + { + $this->spaceBeforeRules = $whitespace; + + return $this; + } + + /** + * @internal + */ + public function getSpaceAfterRules(): string + { + return $this->spaceAfterRules; + } + + /** + * @return $this fluent interface + */ + public function setSpaceAfterRules(string $whitespace): self + { + $this->spaceAfterRules = $whitespace; + + return $this; + } + + /** + * @internal + */ + public function getSpaceBetweenRules(): string + { + return $this->spaceBetweenRules; + } + + /** + * @return $this fluent interface + */ + public function setSpaceBetweenRules(string $whitespace): self + { + $this->spaceBetweenRules = $whitespace; + + return $this; + } + + /** + * @internal + */ + public function getSpaceBeforeBlocks(): string + { + return $this->spaceBeforeBlocks; + } + + /** + * @return $this fluent interface + */ + public function setSpaceBeforeBlocks(string $whitespace): self + { + $this->spaceBeforeBlocks = $whitespace; + + return $this; + } + + /** + * @internal + */ + public function getSpaceAfterBlocks(): string + { + return $this->spaceAfterBlocks; + } + + /** + * @return $this fluent interface + */ + public function setSpaceAfterBlocks(string $whitespace): self + { + $this->spaceAfterBlocks = $whitespace; + + return $this; + } + + /** + * @internal + */ + public function getSpaceBetweenBlocks(): string + { + return $this->spaceBetweenBlocks; + } + + /** + * @return $this fluent interface + */ + public function setSpaceBetweenBlocks(string $whitespace): self + { + $this->spaceBetweenBlocks = $whitespace; + + return $this; + } + + /** + * @internal + */ + public function getContentBeforeAtRuleBlock(): string + { + return $this->contentBeforeAtRuleBlock; + } + + /** + * @return $this fluent interface + */ + public function setBeforeAtRuleBlock(string $content): self + { + $this->contentBeforeAtRuleBlock = $content; + + return $this; + } + + /** + * @internal + */ + public function getContentAfterAtRuleBlock(): string + { + return $this->contentAfterAtRuleBlock; + } + + /** + * @return $this fluent interface + */ + public function setAfterAtRuleBlock(string $content): self + { + $this->contentAfterAtRuleBlock = $content; + + return $this; + } + + /** + * @internal + */ + public function getSpaceBeforeSelectorSeparator(): string + { + return $this->spaceBeforeSelectorSeparator; + } + + /** + * @return $this fluent interface + */ + public function setSpaceBeforeSelectorSeparator(string $whitespace): self + { + $this->spaceBeforeSelectorSeparator = $whitespace; + + return $this; } /** - * @param array|string $aNames - * @param mixed $mValue + * @internal + */ + public function getSpaceAfterSelectorSeparator(): string + { + return $this->spaceAfterSelectorSeparator; + } + + /** + * @return $this fluent interface + */ + public function setSpaceAfterSelectorSeparator(string $whitespace): self + { + $this->spaceAfterSelectorSeparator = $whitespace; + + return $this; + } + + /** + * @internal + */ + public function getSpaceBeforeListArgumentSeparator(): string + { + return $this->spaceBeforeListArgumentSeparator; + } + + /** + * @return $this fluent interface + */ + public function setSpaceBeforeListArgumentSeparator(string $whitespace): self + { + $this->spaceBeforeListArgumentSeparator = $whitespace; + + return $this; + } + + /** + * @return array * - * @return self|false - */ - public function set($aNames, $mValue) - { - $aVarPrefixes = ['a', 's', 'm', 'b', 'f', 'o', 'c', 'i']; - if (\is_string($aNames) && \strpos($aNames, '*') !== false) { - $aNames = - [ - \str_replace('*', 'Before', $aNames), - \str_replace('*', 'Between', $aNames), - \str_replace('*', 'After', $aNames), - ]; - } elseif (!\is_array($aNames)) { - $aNames = [$aNames]; - } - foreach ($aVarPrefixes as $sPrefix) { - $bDidReplace = false; - foreach ($aNames as $sName) { - $sFieldName = $sPrefix . \ucfirst($sName); - if (isset($this->$sFieldName)) { - $this->$sFieldName = $mValue; - $bDidReplace = true; - } - } - if ($bDidReplace) { - return $this; - } - } - // Break the chain so the user knows this option is invalid - return false; + * @internal + */ + public function getSpaceBeforeListArgumentSeparators(): array + { + return $this->spaceBeforeListArgumentSeparators; } /** - * @param string $sMethodName - * @param array $aArguments + * @param array $separatorSpaces * - * @return mixed + * @return $this fluent interface + */ + public function setSpaceBeforeListArgumentSeparators(array $separatorSpaces): self + { + $this->spaceBeforeListArgumentSeparators = $separatorSpaces; + + return $this; + } + + /** + * @internal + */ + public function getSpaceAfterListArgumentSeparator(): string + { + return $this->spaceAfterListArgumentSeparator; + } + + /** + * @return $this fluent interface + */ + public function setSpaceAfterListArgumentSeparator(string $whitespace): self + { + $this->spaceAfterListArgumentSeparator = $whitespace; + + return $this; + } + + /** + * @return array * - * @throws \Exception - */ - public function __call($sMethodName, array $aArguments) - { - if (\strpos($sMethodName, 'set') === 0) { - return $this->set(\substr($sMethodName, 3), $aArguments[0]); - } elseif (\strpos($sMethodName, 'get') === 0) { - return $this->get(\substr($sMethodName, 3)); - } elseif (\method_exists(OutputFormatter::class, $sMethodName)) { - return \call_user_func_array([$this->getFormatter(), $sMethodName], $aArguments); - } else { - throw new \Exception('Unknown OutputFormat method called: ' . $sMethodName); - } + * @internal + */ + public function getSpaceAfterListArgumentSeparators(): array + { + return $this->spaceAfterListArgumentSeparators; } /** - * @param int $iNumber + * @param array $separatorSpaces * - * @return self + * @return $this fluent interface */ - public function indentWithTabs($iNumber = 1) + public function setSpaceAfterListArgumentSeparators(array $separatorSpaces): self { - return $this->setIndentation(\str_repeat("\t", $iNumber)); + $this->spaceAfterListArgumentSeparators = $separatorSpaces; + + return $this; } /** - * @param int $iNumber + * @internal + */ + public function getSpaceBeforeOpeningBrace(): string + { + return $this->spaceBeforeOpeningBrace; + } + + /** + * @return $this fluent interface + */ + public function setSpaceBeforeOpeningBrace(string $whitespace): self + { + $this->spaceBeforeOpeningBrace = $whitespace; + + return $this; + } + + /** + * @internal + */ + public function getContentBeforeDeclarationBlock(): string + { + return $this->contentBeforeDeclarationBlock; + } + + /** + * @return $this fluent interface + */ + public function setBeforeDeclarationBlock(string $content): self + { + $this->contentBeforeDeclarationBlock = $content; + + return $this; + } + + /** + * @internal + */ + public function getContentAfterDeclarationBlockSelectors(): string + { + return $this->contentAfterDeclarationBlockSelectors; + } + + /** + * @return $this fluent interface + */ + public function setAfterDeclarationBlockSelectors(string $content): self + { + $this->contentAfterDeclarationBlockSelectors = $content; + + return $this; + } + + /** + * @internal + */ + public function getContentAfterDeclarationBlock(): string + { + return $this->contentAfterDeclarationBlock; + } + + /** + * @return $this fluent interface + */ + public function setAfterDeclarationBlock(string $content): self + { + $this->contentAfterDeclarationBlock = $content; + + return $this; + } + + /** + * @internal + */ + public function getIndentation(): string + { + return $this->indentation; + } + + /** + * @return $this fluent interface + */ + public function setIndentation(string $indentation): self + { + $this->indentation = $indentation; + + return $this; + } + + /** + * @internal + */ + public function shouldIgnoreExceptions(): bool + { + return $this->shouldIgnoreExceptions; + } + + /** + * @return $this fluent interface + */ + public function setIgnoreExceptions(bool $ignoreExceptions): self + { + $this->shouldIgnoreExceptions = $ignoreExceptions; + + return $this; + } + + /** + * @internal + */ + public function shouldRenderComments(): bool + { + return $this->shouldRenderComments; + } + + /** + * @return $this fluent interface + */ + public function setRenderComments(bool $renderComments): self + { + $this->shouldRenderComments = $renderComments; + + return $this; + } + + /** + * @return int<0, max> * - * @return self + * @internal */ - public function indentWithSpaces($iNumber = 2) + public function getIndentationLevel(): int { - return $this->setIndentation(\str_repeat(' ', $iNumber)); + return $this->indentationLevel; } /** - * @return OutputFormat + * @param int<1, max> $numberOfTabs + * + * @return $this fluent interface */ - public function nextLevel() + public function indentWithTabs(int $numberOfTabs = 1): self { - if ($this->oNextLevelFormat === null) { - $this->oNextLevelFormat = clone $this; - $this->oNextLevelFormat->iIndentationLevel++; - $this->oNextLevelFormat->oFormatter = null; - } - return $this->oNextLevelFormat; + return $this->setIndentation(\str_repeat("\t", $numberOfTabs)); } - public function beLenient(): void + /** + * @param int<1, max> $numberOfSpaces + * + * @return $this fluent interface + */ + public function indentWithSpaces(int $numberOfSpaces = 2): self { - $this->bIgnoreExceptions = true; + return $this->setIndentation(\str_repeat(' ', $numberOfSpaces)); } /** - * @return OutputFormatter + * @internal since V8.8.0 */ - public function getFormatter() + public function nextLevel(): self { - if ($this->oFormatter === null) { - $this->oFormatter = new OutputFormatter($this); + if ($this->nextLevelFormat === null) { + $this->nextLevelFormat = clone $this; + $this->nextLevelFormat->indentationLevel++; + $this->nextLevelFormat->outputFormatter = null; } - return $this->oFormatter; + return $this->nextLevelFormat; + } + + public function beLenient(): void + { + $this->shouldIgnoreExceptions = true; } /** - * @return int + * @internal since 8.8.0 */ - public function level() + public function getFormatter(): OutputFormatter { - return $this->iIndentationLevel; + if ($this->outputFormatter === null) { + $this->outputFormatter = new OutputFormatter($this); + } + + return $this->outputFormatter; } /** * Creates an instance of this class without any particular formatting settings. */ - public static function create(): OutputFormat + public static function create(): self { return new OutputFormat(); } /** * Creates an instance of this class with a preset for compact formatting. - * - * @return self */ - public static function createCompact() + public static function createCompact(): self { $format = self::create(); - $format->set('Space*Rules', '') - ->set('Space*Blocks', '') + $format + ->setSpaceBeforeRules('') + ->setSpaceBetweenRules('') + ->setSpaceAfterRules('') + ->setSpaceBeforeBlocks('') + ->setSpaceBetweenBlocks('') + ->setSpaceAfterBlocks('') ->setSpaceAfterRuleName('') ->setSpaceBeforeOpeningBrace('') ->setSpaceAfterSelectorSeparator('') + ->setSemicolonAfterLastRule(false) ->setRenderComments(false); + return $format; } /** * Creates an instance of this class with a preset for pretty formatting. - * - * @return self */ - public static function createPretty() + public static function createPretty(): self { $format = self::create(); - $format->set('Space*Rules', "\n") - ->set('Space*Blocks', "\n") + $format + ->setSpaceBeforeRules("\n") + ->setSpaceBetweenRules("\n") + ->setSpaceAfterRules("\n") + ->setSpaceBeforeBlocks("\n") ->setSpaceBetweenBlocks("\n\n") - ->set('SpaceAfterListArgumentSeparator', ['default' => '', ',' => ' ']) + ->setSpaceAfterBlocks("\n") + ->setSpaceAfterListArgumentSeparators([',' => ' ']) ->setRenderComments(true); + return $format; } } diff --git a/src/OutputFormatter.php b/src/OutputFormatter.php index 75e1532d3..09918c38d 100644 --- a/src/OutputFormatter.php +++ b/src/OutputFormatter.php @@ -1,39 +1,76 @@ oFormat = $oFormat; + $this->outputFormat = $outputFormat; } /** - * @param string $sName - * @param string|null $sType + * @param non-empty-string $name + * + * @throws \InvalidArgumentException */ - public function space($sName, $sType = null): string - { - $sSpaceString = $this->oFormat->get("Space$sName"); - // If $sSpaceString is an array, we have multiple values configured - // depending on the type of object the space applies to - if (\is_array($sSpaceString)) { - if ($sType !== null && isset($sSpaceString[$sType])) { - $sSpaceString = $sSpaceString[$sType]; - } else { - $sSpaceString = \reset($sSpaceString); - } + public function space(string $name): string + { + switch ($name) { + case 'AfterRuleName': + $spaceString = $this->outputFormat->getSpaceAfterRuleName(); + break; + case 'BeforeRules': + $spaceString = $this->outputFormat->getSpaceBeforeRules(); + break; + case 'AfterRules': + $spaceString = $this->outputFormat->getSpaceAfterRules(); + break; + case 'BetweenRules': + $spaceString = $this->outputFormat->getSpaceBetweenRules(); + break; + case 'BeforeBlocks': + $spaceString = $this->outputFormat->getSpaceBeforeBlocks(); + break; + case 'AfterBlocks': + $spaceString = $this->outputFormat->getSpaceAfterBlocks(); + break; + case 'BetweenBlocks': + $spaceString = $this->outputFormat->getSpaceBetweenBlocks(); + break; + case 'BeforeSelectorSeparator': + $spaceString = $this->outputFormat->getSpaceBeforeSelectorSeparator(); + break; + case 'AfterSelectorSeparator': + $spaceString = $this->outputFormat->getSpaceAfterSelectorSeparator(); + break; + case 'BeforeOpeningBrace': + $spaceString = $this->outputFormat->getSpaceBeforeOpeningBrace(); + break; + case 'BeforeListArgumentSeparator': + $spaceString = $this->outputFormat->getSpaceBeforeListArgumentSeparator(); + break; + case 'AfterListArgumentSeparator': + $spaceString = $this->outputFormat->getSpaceAfterListArgumentSeparator(); + break; + default: + throw new \InvalidArgumentException("Unknown space type: $name", 1740049248); } - return $this->prepareSpace($sSpaceString); + + return $this->prepareSpace($spaceString); } public function spaceAfterRuleName(): string @@ -76,28 +113,29 @@ public function spaceBeforeSelectorSeparator(): string return $this->space('BeforeSelectorSeparator'); } - /** - * @return string - */ public function spaceAfterSelectorSeparator(): string { return $this->space('AfterSelectorSeparator'); } /** - * @param string $sSeparator + * @param non-empty-string $separator */ - public function spaceBeforeListArgumentSeparator($sSeparator): string + public function spaceBeforeListArgumentSeparator(string $separator): string { - return $this->space('BeforeListArgumentSeparator', $sSeparator); + $spaceForSeparator = $this->outputFormat->getSpaceBeforeListArgumentSeparators(); + + return $spaceForSeparator[$separator] ?? $this->space('BeforeListArgumentSeparator'); } /** - * @param string $sSeparator + * @param non-empty-string $separator */ - public function spaceAfterListArgumentSeparator($sSeparator): string + public function spaceAfterListArgumentSeparator(string $separator): string { - return $this->space('AfterListArgumentSeparator', $sSeparator); + $spaceForSeparator = $this->outputFormat->getSpaceAfterListArgumentSeparators(); + + return $spaceForSeparator[$separator] ?? $this->space('AfterListArgumentSeparator'); } public function spaceBeforeOpeningBrace(): string @@ -106,112 +144,92 @@ public function spaceBeforeOpeningBrace(): string } /** - * Runs the given code, either swallowing or passing exceptions, depending on the `bIgnoreExceptions` setting. - * - * @param string $cCode the name of the function to call - * - * @return string|null + * Runs the given code, either swallowing or passing exceptions, depending on the `ignoreExceptions` setting. */ - public function safely($cCode) + public function safely(callable $callable): ?string { - if ($this->oFormat->get('IgnoreExceptions')) { + if ($this->outputFormat->shouldIgnoreExceptions()) { // If output exceptions are ignored, run the code with exception guards try { - return $cCode(); + return $callable(); } catch (OutputException $e) { return null; } // Do nothing } else { // Run the code as-is - return $cCode(); + return $callable(); } } /** - * Clone of the `implode` function, but calls `render` with the current output format instead of `__toString()`. + * Clone of the `implode` function, but calls `render` with the current output format. * - * @param string $sSeparator - * @param array $aValues - * @param bool $bIncreaseLevel + * @param array $values */ - public function implode($sSeparator, array $aValues, $bIncreaseLevel = false): string + public function implode(string $separator, array $values, bool $increaseLevel = false): string { - $sResult = ''; - $oFormat = $this->oFormat; - if ($bIncreaseLevel) { - $oFormat = $oFormat->nextLevel(); + $result = ''; + $outputFormat = $this->outputFormat; + if ($increaseLevel) { + $outputFormat = $outputFormat->nextLevel(); } - $bIsFirst = true; - foreach ($aValues as $mValue) { - if ($bIsFirst) { - $bIsFirst = false; + $isFirst = true; + foreach ($values as $value) { + if ($isFirst) { + $isFirst = false; } else { - $sResult .= $sSeparator; + $result .= $separator; } - if ($mValue instanceof Renderable) { - $sResult .= $mValue->render($oFormat); + if ($value instanceof Renderable) { + $result .= $value->render($outputFormat); } else { - $sResult .= $mValue; + $result .= $value; } } - return $sResult; + return $result; } - /** - * @param string $sString - * - * @return string - */ - public function removeLastSemicolon($sString) + public function removeLastSemicolon(string $string): string { - if ($this->oFormat->get('SemicolonAfterLastRule')) { - return $sString; + if ($this->outputFormat->shouldRenderSemicolonAfterLastRule()) { + return $string; } - $sString = \explode(';', $sString); - if (\count($sString) < 2) { - return $sString[0]; + + $parts = \explode(';', $string); + if (\count($parts) < 2) { + return $parts[0]; } - $sLast = \array_pop($sString); - $sNextToLast = \array_pop($sString); - \array_push($sString, $sNextToLast . $sLast); - return \implode(';', $sString); + $lastPart = \array_pop($parts); + $nextToLastPart = \array_pop($parts); + \array_push($parts, $nextToLastPart . $lastPart); + + return \implode(';', $parts); } - /** - * @param array $aComments - * - * @return string - */ - public function comments(Commentable $oCommentable): string + public function comments(Commentable $commentable): string { - if (!$this->oFormat->bRenderComments) { + if (!$this->outputFormat->shouldRenderComments()) { return ''; } - $sResult = ''; - $aComments = $oCommentable->getComments(); - $iLastCommentIndex = \count($aComments) - 1; + $result = ''; + $comments = $commentable->getComments(); + $lastCommentIndex = \count($comments) - 1; - foreach ($aComments as $i => $oComment) { - $sResult .= $oComment->render($this->oFormat); - $sResult .= $i === $iLastCommentIndex ? $this->spaceAfterBlocks() : $this->spaceBetweenBlocks(); + foreach ($comments as $i => $comment) { + $result .= $comment->render($this->outputFormat); + $result .= $i === $lastCommentIndex ? $this->spaceAfterBlocks() : $this->spaceBetweenBlocks(); } - return $sResult; + return $result; } - /** - * @param string $sSpaceString - */ - private function prepareSpace($sSpaceString): string + private function prepareSpace(string $spaceString): string { - return \str_replace("\n", "\n" . $this->indent(), $sSpaceString); + return \str_replace("\n", "\n" . $this->indent(), $spaceString); } - /** - * @return string - */ private function indent(): string { - return \str_repeat($this->oFormat->sIndentation, $this->oFormat->level()); + return \str_repeat($this->outputFormat->getIndentation(), $this->outputFormat->getIndentationLevel()); } } diff --git a/src/Parser.php b/src/Parser.php index 7cb044003..b34a5107c 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -1,5 +1,7 @@ $lineNumber the line number (starting from 1, not from 0) */ - public function __construct($sText, ?Settings $oParserSettings = null, $iLineNo = 1) + public function __construct(string $text, ?Settings $parserSettings = null, int $lineNumber = 1) { - if ($oParserSettings === null) { - $oParserSettings = Settings::create(); + if ($parserSettings === null) { + $parserSettings = Settings::create(); } - $this->oParserState = new ParserState($sText, $oParserSettings, $iLineNo); - } - - /** - * Sets the charset to be used if the CSS does not contain an `@charset` declaration. - * - * @param string $sCharset - */ - public function setCharset($sCharset): void - { - $this->oParserState->setCharset($sCharset); - } - - /** - * Returns the charset that is used if the CSS does not contain an `@charset` declaration. - */ - public function getCharset(): void - { - // Note: The `return` statement is missing here. This is a bug that needs to be fixed. - $this->oParserState->getCharset(); + $this->parserState = new ParserState($text, $parserSettings, $lineNumber); } /** @@ -55,6 +37,6 @@ public function getCharset(): void */ public function parse(): Document { - return Document::parse($this->oParserState); + return Document::parse($this->parserState); } } diff --git a/src/Parsing/Anchor.php b/src/Parsing/Anchor.php index 022d7502d..c27f436ad 100644 --- a/src/Parsing/Anchor.php +++ b/src/Parsing/Anchor.php @@ -1,31 +1,35 @@ */ - private $iPosition; + private $position; /** - * @var \Sabberworm\CSS\Parsing\ParserState + * @var ParserState */ - private $oParserState; + private $parserState; /** - * @param int $iPosition - * @param \Sabberworm\CSS\Parsing\ParserState $oParserState + * @param int<0, max> $position */ - public function __construct($iPosition, ParserState $oParserState) + public function __construct(int $position, ParserState $parserState) { - $this->iPosition = $iPosition; - $this->oParserState = $oParserState; + $this->position = $position; + $this->parserState = $parserState; } public function backtrack(): void { - $this->oParserState->setPosition($this->iPosition); + $this->parserState->setPosition($this->position); } } diff --git a/src/Parsing/OutputException.php b/src/Parsing/OutputException.php index 9bfbc75fb..0a20dc967 100644 --- a/src/Parsing/OutputException.php +++ b/src/Parsing/OutputException.php @@ -1,18 +1,10 @@ */ - private $aText; + private $characters; /** - * @var int + * @var int<0, max> */ - private $iCurrentPosition; + private $currentPosition = 0; /** * will only be used if the CSS does not contain an `@charset` declaration * * @var string */ - private $sCharset; - - /** - * @var int - */ - private $iLength; + private $charset; /** - * @var int + * @var int<1, max> $lineNumber */ - private $iLineNo; + private $lineNumber; /** - * @param string $sText the complete CSS as text (i.e., usually the contents of a CSS file) - * @param int $iLineNo + * @param string $text the complete CSS as text (i.e., usually the contents of a CSS file) + * @param int<1, max> $lineNumber */ - public function __construct($sText, Settings $oParserSettings, $iLineNo = 1) + public function __construct(string $text, Settings $parserSettings, int $lineNumber = 1) { - $this->oParserSettings = $oParserSettings; - $this->sText = $sText; - $this->iCurrentPosition = 0; - $this->iLineNo = $iLineNo; - $this->setCharset($this->oParserSettings->sDefaultCharset); + $this->parserSettings = $parserSettings; + $this->text = $text; + $this->lineNumber = $lineNumber; + $this->setCharset($this->parserSettings->getDefaultCharset()); } /** * Sets the charset to be used if the CSS does not contain an `@charset` declaration. - * - * @param string $sCharset */ - public function setCharset($sCharset): void + public function setCharset(string $charset): void { - $this->sCharset = $sCharset; - $this->aText = $this->strsplit($this->sText); - if (\is_array($this->aText)) { - $this->iLength = \count($this->aText); - } + $this->charset = $charset; + $this->characters = $this->strsplit($this->text); } /** - * Returns the charset that is used if the CSS does not contain an `@charset` declaration. - * - * @return string + * @return int<1, max> */ - public function getCharset() + public function currentLine(): int { - return $this->sCharset; + return $this->lineNumber; } /** - * @return int + * @return int<0, max> */ - public function currentLine() + public function currentColumn(): int { - return $this->iLineNo; + return $this->currentPosition; } - /** - * @return int - */ - public function currentColumn() + public function getSettings(): Settings { - return $this->iCurrentPosition; + return $this->parserSettings; } - /** - * @return Settings - */ - public function getSettings() - { - return $this->oParserSettings; - } - - public function anchor(): Anchor { - return new Anchor($this->iCurrentPosition, $this); + return new Anchor($this->currentPosition, $this); } /** - * @param int $iPosition + * @param int<0, max> $position */ - public function setPosition($iPosition): void + public function setPosition(int $position): void { - $this->iCurrentPosition = $iPosition; + $this->currentPosition = $position; } /** - * @param bool $bIgnoreCase - * - * @return string + * @return non-empty-string * * @throws UnexpectedTokenException */ - public function parseIdentifier($bIgnoreCase = true) + public function parseIdentifier(bool $ignoreCase = true): string { if ($this->isEnd()) { - throw new UnexpectedEOFException('', '', 'identifier', $this->iLineNo); + throw new UnexpectedEOFException('', '', 'identifier', $this->lineNumber); } - $sResult = $this->parseCharacter(true); - if ($sResult === null) { - throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo); + $result = $this->parseCharacter(true); + if ($result === null) { + throw new UnexpectedTokenException('', $this->peek(5), 'identifier', $this->lineNumber); } - $sCharacter = null; - while (!$this->isEnd() && ($sCharacter = $this->parseCharacter(true)) !== null) { - if (\preg_match('/[a-zA-Z0-9\\x{00A0}-\\x{FFFF}_-]/Sux', $sCharacter)) { - $sResult .= $sCharacter; + $character = null; + while (!$this->isEnd() && ($character = $this->parseCharacter(true)) !== null) { + if (preg_match('/[a-zA-Z0-9\\x{00A0}-\\x{FFFF}_-]/Sux', $character) !== 0) { + $result .= $character; } else { - $sResult .= '\\' . $sCharacter; + $result .= '\\' . $character; } } - if ($bIgnoreCase) { - $sResult = $this->strtolower($sResult); + if ($ignoreCase) { + $result = $this->strtolower($result); } - return $sResult; + + return $result; } /** - * @param bool $bIsForIdentifier - * - * @return string|null - * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ - public function parseCharacter($bIsForIdentifier) + public function parseCharacter(bool $isForIdentifier): ?string { if ($this->peek() === '\\') { - if ( - $bIsForIdentifier && $this->oParserSettings->bLenientParsing - && ($this->comes('\\0') || $this->comes('\\9')) - ) { - // Non-strings can contain \0 or \9 which is an IE hack supported in lenient parsing. - return null; - } $this->consume('\\'); if ($this->comes('\\n') || $this->comes('\\r')) { return ''; } - if (\preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) { + if (preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) { return $this->consume(1); } - $sUnicode = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6); - if ($this->strlen($sUnicode) < 6) { + $hexCodePoint = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6); + if ($this->strlen($hexCodePoint) < 6) { // Consume whitespace after incomplete unicode escape - if (\preg_match('/\\s/isSu', $this->peek())) { + if (preg_match('/\\s/isSu', $this->peek()) !== 0) { if ($this->comes('\\r\\n')) { $this->consume(2); } else { @@ -192,15 +162,15 @@ public function parseCharacter($bIsForIdentifier) } } } - $iUnicode = \intval($sUnicode, 16); - $sUtf32 = ''; + $codePoint = \intval($hexCodePoint, 16); + $utf32EncodedCharacter = ''; for ($i = 0; $i < 4; ++$i) { - $sUtf32 .= \chr($iUnicode & 0xff); - $iUnicode = $iUnicode >> 8; + $utf32EncodedCharacter .= \chr($codePoint & 0xff); + $codePoint = $codePoint >> 8; } - return \iconv('utf-32le', $this->sCharset, $sUtf32); + return iconv('utf-32le', $this->charset, $utf32EncodedCharacter); } - if ($bIsForIdentifier) { + if ($isForIdentifier) { $peek = \ord($this->peek()); // Ranges: a-z A-Z 0-9 - _ if ( @@ -216,108 +186,118 @@ public function parseCharacter($bIsForIdentifier) } else { return $this->consume(1); } + return null; } /** - * @return array|void + * @return list * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ public function consumeWhiteSpace(): array { - $aComments = []; + $comments = []; do { - while (\preg_match('/\\s/isSu', $this->peek()) === 1) { + while (preg_match('/\\s/isSu', $this->peek()) === 1) { $this->consume(1); } - if ($this->oParserSettings->bLenientParsing) { + if ($this->parserSettings->usesLenientParsing()) { try { - $oComment = $this->consumeComment(); + $comment = $this->consumeComment(); } catch (UnexpectedEOFException $e) { - $this->iCurrentPosition = $this->iLength; - return $aComments; + $this->currentPosition = \count($this->characters); + break; } } else { - $oComment = $this->consumeComment(); + $comment = $this->consumeComment(); } - if ($oComment !== false) { - $aComments[] = $oComment; + if ($comment instanceof Comment) { + $comments[] = $comment; } - } while ($oComment !== false); - return $aComments; + } while ($comment instanceof Comment); + + return $comments; } /** - * @param string $sString - * @param bool $bCaseInsensitive + * @param non-empty-string $string */ - public function comes($sString, $bCaseInsensitive = false): bool + public function comes(string $string, bool $caseInsensitive = false): bool { - $sPeek = $this->peek(\strlen($sString)); - return ($sPeek == '') - ? false - : $this->streql($sPeek, $sString, $bCaseInsensitive); + $peek = $this->peek(\strlen($string)); + + return ($peek !== '') && $this->streql($peek, $string, $caseInsensitive); } /** - * @param int $iLength - * @param int $iOffset + * @param int<1, max> $length + * @param int<0, max> $offset */ - public function peek($iLength = 1, $iOffset = 0): string + public function peek(int $length = 1, int $offset = 0): string { - $iOffset += $this->iCurrentPosition; - if ($iOffset >= $this->iLength) { + $offset += $this->currentPosition; + if ($offset >= \count($this->characters)) { return ''; } - return $this->substr($iOffset, $iLength); + + return $this->substr($offset, $length); } /** - * @param int $mValue + * @param string|int<1, max> $value * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ - public function consume($mValue = 1): string + public function consume($value = 1): string { - if (\is_string($mValue)) { - $iLineCount = \substr_count($mValue, "\n"); - $iLength = $this->strlen($mValue); - if (!$this->streql($this->substr($this->iCurrentPosition, $iLength), $mValue)) { - throw new UnexpectedTokenException($mValue, $this->peek(\max($iLength, 5)), $this->iLineNo); + if (\is_string($value)) { + $numberOfLines = \substr_count($value, "\n"); + $length = $this->strlen($value); + if (!$this->streql($this->substr($this->currentPosition, $length), $value)) { + throw new UnexpectedTokenException( + $value, + $this->peek(\max($length, 5)), + 'literal', + $this->lineNumber + ); } - $this->iLineNo += $iLineCount; - $this->iCurrentPosition += $this->strlen($mValue); - return $mValue; + + $this->lineNumber += $numberOfLines; + $this->currentPosition += $this->strlen($value); + $result = $value; } else { - if ($this->iCurrentPosition + $mValue > $this->iLength) { - throw new UnexpectedEOFException($mValue, $this->peek(5), 'count', $this->iLineNo); + if ($this->currentPosition + $value > \count($this->characters)) { + throw new UnexpectedEOFException((string) $value, $this->peek(5), 'count', $this->lineNumber); } - $sResult = $this->substr($this->iCurrentPosition, $mValue); - $iLineCount = \substr_count($sResult, "\n"); - $this->iLineNo += $iLineCount; - $this->iCurrentPosition += $mValue; - return $sResult; + + $result = $this->substr($this->currentPosition, $value); + $numberOfLines = \substr_count($result, "\n"); + $this->lineNumber += $numberOfLines; + $this->currentPosition += $value; } + + return $result; } /** - * @param string $mExpression - * @param int|null $iMaxLength + * @param string $expression + * @param int<1, max>|null $maximumLength * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ - public function consumeExpression($mExpression, $iMaxLength = null): string + public function consumeExpression(string $expression, ?int $maximumLength = null): string { - $aMatches = null; - $sInput = $iMaxLength !== null ? $this->peek($iMaxLength) : $this->inputLeft(); - if (\preg_match($mExpression, $sInput, $aMatches, PREG_OFFSET_CAPTURE) === 1) { - return $this->consume($aMatches[0][0]); + $matches = null; + $input = ($maximumLength !== null) ? $this->peek($maximumLength) : $this->inputLeft(); + if (preg_match($expression, $input, $matches, PREG_OFFSET_CAPTURE) !== 1) { + throw new UnexpectedTokenException($expression, $this->peek(5), 'expression', $this->lineNumber); } - throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression', $this->iLineNo); + + return $this->consume($matches[0][0]); } /** @@ -325,13 +305,14 @@ public function consumeExpression($mExpression, $iMaxLength = null): string */ public function consumeComment() { - $mComment = false; + $lineNumber = $this->lineNumber; + $comment = null; + if ($this->comes('/*')) { - $iLineNo = $this->iLineNo; $this->consume(1); - $mComment = ''; + $comment = ''; while (($char = $this->consume(1)) !== '') { - $mComment .= $char; + $comment .= $char; if ($this->comes('*/')) { $this->consume(2); break; @@ -339,177 +320,142 @@ public function consumeComment() } } - if ($mComment !== false) { - // We skip the * which was included in the comment. - return new Comment(\substr($mComment, 1), $iLineNo); - } - - return $mComment; + // We skip the * which was included in the comment. + return \is_string($comment) ? new Comment(\substr($comment, 1), $lineNumber) : false; } public function isEnd(): bool { - return $this->iCurrentPosition >= $this->iLength; + return $this->currentPosition >= \count($this->characters); } /** - * @param array|string $aEnd - * @param string $bIncludeEnd - * @param string $consumeEnd + * @param list|string|self::EOF $stopCharacters * @param array $comments * - * @return string - * * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ - public function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false, array &$comments = []) - { - $aEnd = \is_array($aEnd) ? $aEnd : [$aEnd]; - $out = ''; - $start = $this->iCurrentPosition; + public function consumeUntil( + $stopCharacters, + bool $includeEnd = false, + bool $consumeEnd = false, + array &$comments = [] + ): string { + $stopCharacters = \is_array($stopCharacters) ? $stopCharacters : [$stopCharacters]; + $consumedCharacters = ''; + $start = $this->currentPosition; while (!$this->isEnd()) { - $char = $this->consume(1); - if (\in_array($char, $aEnd, true)) { - if ($bIncludeEnd) { - $out .= $char; + $character = $this->consume(1); + if (\in_array($character, $stopCharacters, true)) { + if ($includeEnd) { + $consumedCharacters .= $character; } elseif (!$consumeEnd) { - $this->iCurrentPosition -= $this->strlen($char); + $this->currentPosition -= $this->strlen($character); } - return $out; + return $consumedCharacters; } - $out .= $char; - if ($comment = $this->consumeComment()) { + $consumedCharacters .= $character; + $comment = $this->consumeComment(); + if ($comment instanceof Comment) { $comments[] = $comment; } } - if (\in_array(self::EOF, $aEnd, true)) { - return $out; + if (\in_array(self::EOF, $stopCharacters, true)) { + return $consumedCharacters; } - $this->iCurrentPosition = $start; + $this->currentPosition = $start; throw new UnexpectedEOFException( - 'One of ("' . \implode('","', $aEnd) . '")', + 'One of ("' . \implode('","', $stopCharacters) . '")', $this->peek(5), 'search', - $this->iLineNo + $this->lineNumber ); } private function inputLeft(): string { - return $this->substr($this->iCurrentPosition, -1); + return $this->substr($this->currentPosition, -1); } - /** - * @param string $sString1 - * @param string $sString2 - * @param bool $bCaseInsensitive - */ - public function streql($sString1, $sString2, $bCaseInsensitive = true): bool + public function streql(string $string1, string $string2, bool $caseInsensitive = true): bool { - if ($bCaseInsensitive) { - return $this->strtolower($sString1) === $this->strtolower($sString2); - } else { - return $sString1 === $sString2; - } + return $caseInsensitive + ? ($this->strtolower($string1) === $this->strtolower($string2)) + : ($string1 === $string2); } /** - * @param int $iAmount + * @param int<1, max> $numberOfCharacters */ - public function backtrack($iAmount): void + public function backtrack(int $numberOfCharacters): void { - $this->iCurrentPosition -= $iAmount; + $this->currentPosition -= $numberOfCharacters; } /** - * @param string $sString + * @return int<0, max> */ - public function strlen($sString): int + public function strlen(string $string): int { - if ($this->oParserSettings->bMultibyteSupport) { - return \mb_strlen($sString, $this->sCharset); - } else { - return \strlen($sString); - } + return $this->parserSettings->hasMultibyteSupport() + ? \mb_strlen($string, $this->charset) + : \strlen($string); } /** - * @param int $iStart - * @param int $iLength + * @param int<0, max> $offset */ - private function substr($iStart, $iLength): string + private function substr(int $offset, int $length): string { - if ($iLength < 0) { - $iLength = $this->iLength - $iStart + $iLength; + if ($length < 0) { + $length = \count($this->characters) - $offset + $length; } - if ($iStart + $iLength > $this->iLength) { - $iLength = $this->iLength - $iStart; + if ($offset + $length > \count($this->characters)) { + $length = \count($this->characters) - $offset; } - $sResult = ''; - while ($iLength > 0) { - $sResult .= $this->aText[$iStart]; - $iStart++; - $iLength--; + $result = ''; + while ($length > 0) { + $result .= $this->characters[$offset]; + $offset++; + $length--; } - return $sResult; + + return $result; } /** - * @param string $sString + * @return ($string is non-empty-string ? non-empty-string : string) */ - private function strtolower($sString): string + private function strtolower(string $string): string { - if ($this->oParserSettings->bMultibyteSupport) { - return \mb_strtolower($sString, $this->sCharset); - } else { - return \strtolower($sString); - } + return $this->parserSettings->hasMultibyteSupport() + ? \mb_strtolower($string, $this->charset) + : \strtolower($string); } /** - * @param string $sString - * - * @return array + * @return list */ - private function strsplit($sString) + private function strsplit(string $string): array { - if ($this->oParserSettings->bMultibyteSupport) { - if ($this->streql($this->sCharset, 'utf-8')) { - return \preg_split('//u', $sString, -1, PREG_SPLIT_NO_EMPTY); + if ($this->parserSettings->hasMultibyteSupport()) { + if ($this->streql($this->charset, 'utf-8')) { + $result = preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY); } else { - $iLength = \mb_strlen($sString, $this->sCharset); - $aResult = []; - for ($i = 0; $i < $iLength; ++$i) { - $aResult[] = \mb_substr($sString, $i, 1, $this->sCharset); + $length = \mb_strlen($string, $this->charset); + $result = []; + for ($i = 0; $i < $length; ++$i) { + $result[] = \mb_substr($string, $i, 1, $this->charset); } - return $aResult; } } else { - if ($sString === '') { - return []; - } else { - return \str_split($sString); - } + $result = ($string !== '') ? \str_split($string) : []; } - } - /** - * @param string $sString - * @param string $sNeedle - * @param int $iOffset - * - * @return int|false - */ - private function strpos($sString, $sNeedle, $iOffset) - { - if ($this->oParserSettings->bMultibyteSupport) { - return \mb_strpos($sString, $sNeedle, $iOffset, $this->sCharset); - } else { - return \strpos($sString, $sNeedle, $iOffset); - } + return $result; } } diff --git a/src/Parsing/SourceException.php b/src/Parsing/SourceException.php index 1ca668a99..43b3faf05 100644 --- a/src/Parsing/SourceException.php +++ b/src/Parsing/SourceException.php @@ -1,32 +1,25 @@ |null $lineNumber */ - public function __construct($sMessage, $iLineNo = 0) + public function __construct(string $message, ?int $lineNumber = null) { - $this->iLineNo = $iLineNo; - if (!empty($iLineNo)) { - $sMessage .= " [line no: $iLineNo]"; + $this->setPosition($lineNumber); + if ($lineNumber !== null) { + $message .= " [line no: $lineNumber]"; } - parent::__construct($sMessage); - } - - /** - * @return int - */ - public function getLineNo() - { - return $this->iLineNo; + parent::__construct($message); } } diff --git a/src/Parsing/UnexpectedEOFException.php b/src/Parsing/UnexpectedEOFException.php index 825cc0c4f..17e2a2152 100644 --- a/src/Parsing/UnexpectedEOFException.php +++ b/src/Parsing/UnexpectedEOFException.php @@ -1,5 +1,7 @@ |null $lineNumber */ - public function __construct($sExpected, $sFound, $sMatchType = 'literal', $iLineNo = 0) + public function __construct(string $expected, string $found, string $matchType = 'literal', ?int $lineNumber = null) { - $this->sExpected = $sExpected; - $this->sFound = $sFound; - $this->sMatchType = $sMatchType; - $sMessage = "Token “{$sExpected}” ({$sMatchType}) not found. Got “{$sFound}”."; - if ($this->sMatchType === 'search') { - $sMessage = "Search for “{$sExpected}” returned no results. Context: “{$sFound}”."; - } elseif ($this->sMatchType === 'count') { - $sMessage = "Next token was expected to have {$sExpected} chars. Context: “{$sFound}”."; - } elseif ($this->sMatchType === 'identifier') { - $sMessage = "Identifier expected. Got “{$sFound}”"; - } elseif ($this->sMatchType === 'custom') { - $sMessage = \trim("$sExpected $sFound"); + $message = "Token “{$expected}” ({$matchType}) not found. Got “{$found}”."; + if ($matchType === 'search') { + $message = "Search for “{$expected}” returned no results. Context: “{$found}”."; + } elseif ($matchType === 'count') { + $message = "Next token was expected to have {$expected} chars. Context: “{$found}”."; + } elseif ($matchType === 'identifier') { + $message = "Identifier expected. Got “{$found}”"; + } elseif ($matchType === 'custom') { + $message = \trim("$expected $found"); } - parent::__construct($sMessage, $iLineNo); + parent::__construct($message, $lineNumber); } } diff --git a/src/Position/Position.php b/src/Position/Position.php new file mode 100644 index 000000000..7b75f8eac --- /dev/null +++ b/src/Position/Position.php @@ -0,0 +1,55 @@ +|null + */ + protected $lineNumber; + + /** + * @var int<0, max>|null + */ + protected $columnNumber; + + /** + * @return int<1, max>|null + */ + public function getLineNumber(): ?int + { + return $this->lineNumber; + } + + /** + * @return int<0, max>|null + */ + public function getColumnNumber(): ?int + { + return $this->columnNumber; + } + + /** + * @param int<1, max>|null $lineNumber + * @param int<0, max>|null $columnNumber + * + * @return $this fluent interface + */ + public function setPosition(?int $lineNumber, ?int $columnNumber = null): Positionable + { + $this->lineNumber = $lineNumber; + $this->columnNumber = $columnNumber; + + return $this; + } +} diff --git a/src/Position/Positionable.php b/src/Position/Positionable.php new file mode 100644 index 000000000..d23d26a32 --- /dev/null +++ b/src/Position/Positionable.php @@ -0,0 +1,31 @@ +|null + */ + public function getLineNumber(): ?int; + + /** + * @return int<0, max>|null + */ + public function getColumnNumber(): ?int; + + /** + * @param int<1, max>|null $lineNumber + * @param int<0, max>|null $columnNumber + * + * @return $this fluent interface + */ + public function setPosition(?int $lineNumber, ?int $columnNumber = null): Positionable; +} diff --git a/src/Property/AtRule.php b/src/Property/AtRule.php index 64efd4ded..49a160a1a 100644 --- a/src/Property/AtRule.php +++ b/src/Property/AtRule.php @@ -1,38 +1,29 @@ + * @var string|null */ - protected $aComments; + private $prefix; /** - * @param string $mUrl - * @param string|null $sPrefix - * @param int $iLineNo + * @param CSSString|URL $url + * @param int<1, max>|null $lineNumber */ - public function __construct($mUrl, $sPrefix = null, $iLineNo = 0) + public function __construct($url, ?string $prefix = null, ?int $lineNumber = null) { - $this->mUrl = $mUrl; - $this->sPrefix = $sPrefix; - $this->iLineNo = $iLineNo; - $this->aComments = []; + $this->url = $url; + $this->prefix = $prefix; + $this->setPosition($lineNumber); } /** - * @return int + * @return non-empty-string */ - public function getLineNo() - { - return $this->iLineNo; - } - - public function __toString(): string - { - return $this->render(new OutputFormat()); - } - - public function render(OutputFormat $oOutputFormat): string + public function render(OutputFormat $outputFormat): string { - return '@namespace ' . ($this->sPrefix === null ? '' : $this->sPrefix . ' ') - . $this->mUrl->render($oOutputFormat) . ';'; + return '@namespace ' . ($this->prefix === null ? '' : $this->prefix . ' ') + . $this->url->render($outputFormat) . ';'; } /** - * @return string + * @return CSSString|URL */ public function getUrl() { - return $this->mUrl; + return $this->url; } - /** - * @return string|null - */ - public function getPrefix() + public function getPrefix(): ?string { - return $this->sPrefix; + return $this->prefix; } /** - * @param string $mUrl + * @param CSSString|URL $url */ - public function setUrl($mUrl): void + public function setUrl($url): void { - $this->mUrl = $mUrl; + $this->url = $url; } - /** - * @param string $sPrefix - */ - public function setPrefix($sPrefix): void + public function setPrefix(string $prefix): void { - $this->sPrefix = $sPrefix; + $this->prefix = $prefix; } /** - * @return string + * @return non-empty-string */ public function atRuleName(): string { @@ -103,38 +84,14 @@ public function atRuleName(): string } /** - * @return array + * @return array{0: CSSString|URL|non-empty-string, 1?: CSSString|URL} */ public function atRuleArgs(): array { - $aResult = [$this->mUrl]; - if ($this->sPrefix) { - \array_unshift($aResult, $this->sPrefix); + $result = [$this->url]; + if (\is_string($this->prefix) && $this->prefix !== '') { + \array_unshift($result, $this->prefix); } - return $aResult; - } - - /** - * @param array $aComments - */ - public function addComments(array $aComments): void - { - $this->aComments = \array_merge($this->aComments, $aComments); - } - - /** - * @return array - */ - public function getComments() - { - return $this->aComments; - } - - /** - * @param array $aComments - */ - public function setComments(array $aComments): void - { - $this->aComments = $aComments; + return $result; } } diff --git a/src/Property/Charset.php b/src/Property/Charset.php index 93a459ed5..c9488ad1e 100644 --- a/src/Property/Charset.php +++ b/src/Property/Charset.php @@ -1,9 +1,13 @@ + * @var CSSString */ - protected $aComments; + private $charset; /** - * @param CSSString $oCharset - * @param int $iLineNo + * @param int<1, max>|null $lineNumber */ - public function __construct(CSSString $oCharset, $iLineNo = 0) + public function __construct(CSSString $charset, ?int $lineNumber = null) { - $this->oCharset = $oCharset; - $this->iLineNo = $iLineNo; - $this->aComments = []; + $this->charset = $charset; + $this->setPosition($lineNumber); } /** - * @return int + * @param string|CSSString $charset */ - public function getLineNo() + public function setCharset($charset): void { - return $this->iLineNo; + $charset = $charset instanceof CSSString ? $charset : new CSSString($charset); + $this->charset = $charset; } - /** - * @param string|CSSString $oCharset - * - * @return void - */ - public function setCharset($sCharset): void + public function getCharset(): string { - $sCharset = $sCharset instanceof CSSString ? $sCharset : new CSSString($sCharset); - $this->oCharset = $sCharset; + return $this->charset->getString(); } /** - * @return string + * @return non-empty-string */ - public function getCharset() + public function render(OutputFormat $outputFormat): string { - return $this->oCharset->getString(); - } - - public function __toString(): string - { - return $this->render(new OutputFormat()); - } - - public function render(OutputFormat $oOutputFormat): string - { - return "{$oOutputFormat->comments($this)}@charset {$this->oCharset->render($oOutputFormat)};"; - } - - public function atRuleName(): string - { - return 'charset'; + return "{$outputFormat->getFormatter()->comments($this)}@charset {$this->charset->render($outputFormat)};"; } /** - * @return string + * @return non-empty-string */ - public function atRuleArgs() - { - return $this->oCharset; - } - - /** - * @param array $aComments - * - * @return void - */ - public function addComments(array $aComments): void - { - $this->aComments = \array_merge($this->aComments, $aComments); - } - - /** - * @return array - */ - public function getComments() + public function atRuleName(): string { - return $this->aComments; + return 'charset'; } - /** - * @param array $aComments - * - * @return void - */ - public function setComments(array $aComments): void + public function atRuleArgs(): CSSString { - $this->aComments = $aComments; + return $this->charset; } } diff --git a/src/Property/Import.php b/src/Property/Import.php index 04e801912..a9dabe979 100644 --- a/src/Property/Import.php +++ b/src/Property/Import.php @@ -1,130 +1,85 @@ + * @var string|null */ - protected $aComments; + private $mediaQuery; /** - * @param URL $oLocation - * @param string $sMediaQuery - * @param int $iLineNo + * @param int<1, max>|null $lineNumber */ - public function __construct(URL $oLocation, $sMediaQuery, $iLineNo = 0) + public function __construct(URL $location, ?string $mediaQuery, ?int $lineNumber = null) { - $this->oLocation = $oLocation; - $this->sMediaQuery = $sMediaQuery; - $this->iLineNo = $iLineNo; - $this->aComments = []; + $this->location = $location; + $this->mediaQuery = $mediaQuery; + $this->setPosition($lineNumber); } - /** - * @return int - */ - public function getLineNo() + public function setLocation(URL $location): void { - return $this->iLineNo; + $this->location = $location; } - /** - * @param URL $oLocation - */ - public function setLocation($oLocation): void + public function getLocation(): URL { - $this->oLocation = $oLocation; + return $this->location; } /** - * @return URL + * @return non-empty-string */ - public function getLocation() - { - return $this->oLocation; - } - - public function __toString(): string - { - return $this->render(new OutputFormat()); - } - - public function render(OutputFormat $oOutputFormat): string + public function render(OutputFormat $outputFormat): string { - return $oOutputFormat->comments($this) . '@import ' . $this->oLocation->render($oOutputFormat) - . ($this->sMediaQuery === null ? '' : ' ' . $this->sMediaQuery) . ';'; + return $outputFormat->getFormatter()->comments($this) . '@import ' . $this->location->render($outputFormat) + . ($this->mediaQuery === null ? '' : ' ' . $this->mediaQuery) . ';'; } + /** + * @return non-empty-string + */ public function atRuleName(): string { return 'import'; } /** - * @return array + * @return array{0: URL, 1?: non-empty-string} */ public function atRuleArgs(): array { - $aResult = [$this->oLocation]; - if ($this->sMediaQuery) { - \array_push($aResult, $this->sMediaQuery); + $result = [$this->location]; + if (\is_string($this->mediaQuery) && $this->mediaQuery !== '') { + $result[] = $this->mediaQuery; } - return $aResult; - } - - /** - * @param array $aComments - */ - public function addComments(array $aComments): void - { - $this->aComments = \array_merge($this->aComments, $aComments); - } - - /** - * @return array - */ - public function getComments() - { - return $this->aComments; - } - /** - * @param array $aComments - */ - public function setComments(array $aComments): void - { - $this->aComments = $aComments; + return $result; } - /** - * @return string - */ - public function getMediaQuery() + public function getMediaQuery(): ?string { - return $this->sMediaQuery; + return $this->mediaQuery; } } diff --git a/src/Property/KeyframeSelector.php b/src/Property/KeyframeSelector.php index e58633622..47881771d 100644 --- a/src/Property/KeyframeSelector.php +++ b/src/Property/KeyframeSelector.php @@ -1,25 +1,47 @@ ]* # any sequence of valid unescaped characters - (?:\\\\.)? # a single escaped character - (?:([\'"]).*?(?]++ + | + # one or more escaped characters + (?:\\\\.)++ + | + # quoted text, like in `[id="example"]` + (?: + # opening quote + ([\'"]) + (?: + # sequence of characters except closing quote or backslash + (?:(?!\\g{-1}|\\\\).)++ + | + # one or more escaped characters + (?:\\\\.)++ + )*+ # zero or more times + # closing quote or end (unmatched quote is currently allowed) + (?:\\g{-1}|$) + ) + )*+ # zero or more times + | + # keyframe animation progress percentage (e.g. 50%), untrimmed + \\s*+(\\d++%)\\s*+ + )$ + /ux'; } diff --git a/src/Property/Selector.php b/src/Property/Selector.php index cf07a8f59..a647378e5 100644 --- a/src/Property/Selector.php +++ b/src/Property/Selector.php @@ -1,137 +1,94 @@ \\~]+)[\\w]+ # elements - | - \\:{1,2}( # pseudo-elements - after|before|first-letter|first-line|selection - )) - /ix'; - - /** - * regexp for specificity calculations - * - * @var string - * - * @internal + * @internal since 8.5.2 */ public const SELECTOR_VALIDATION_RX = '/ ^( (?: - [a-zA-Z0-9\\x{00A0}-\\x{FFFF}_^$|*="\'~\\[\\]()\\-\\s\\.:#+>]* # any sequence of valid unescaped characters - (?:\\\\.)? # a single escaped character - (?:([\'"]).*?(?,]++ + | + # one or more escaped characters + (?:\\\\.)++ + | + # quoted text, like in `[id="example"]` + (?: + # opening quote + ([\'"]) + (?: + # sequence of characters except closing quote or backslash + (?:(?!\\g{-1}|\\\\).)++ + | + # one or more escaped characters + (?:\\\\.)++ + )*+ # zero or more times + # closing quote or end (unmatched quote is currently allowed) + (?:\\g{-1}|$) + ) + )*+ # zero or more times )$ /ux'; /** * @var string */ - private $sSelector; + private $selector; /** - * @var int|null + * @internal since V8.8.0 */ - private $iSpecificity; + public static function isValid(string $selector): bool + { + // Note: We need to use `static::` here as the constant is overridden in the `KeyframeSelector` class. + $numberOfMatches = preg_match(static::SELECTOR_VALIDATION_RX, $selector); - /** - * @param string $sSelector - * - * @return bool - */ - public static function isValid($sSelector) + return $numberOfMatches === 1; + } + + public function __construct(string $selector) { - return \preg_match(static::SELECTOR_VALIDATION_RX, $sSelector); + $this->setSelector($selector); } - /** - * @param string $sSelector - * @param bool $bCalculateSpecificity - */ - public function __construct($sSelector, $bCalculateSpecificity = false) + public function getSelector(): string { - $this->setSelector($sSelector); - if ($bCalculateSpecificity) { - $this->getSpecificity(); - } + return $this->selector; } - /** - * @return string - */ - public function getSelector() + public function setSelector(string $selector): void { - return $this->sSelector; + $this->selector = \trim($selector); } /** - * @param string $sSelector - * - * @return void + * @return int<0, max> */ - public function setSelector($sSelector): void + public function getSpecificity(): int { - $this->sSelector = \trim($sSelector); - $this->iSpecificity = null; + return SpecificityCalculator::calculate($this->selector); } - public function __toString(): string + public function render(OutputFormat $outputFormat): string { return $this->getSelector(); } - - /** - * @return int - */ - public function getSpecificity() - { - if ($this->iSpecificity === null) { - $a = 0; - /// @todo should exclude \# as well as "#" - $aMatches = null; - $b = \substr_count($this->sSelector, '#'); - $c = \preg_match_all(self::NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX, $this->sSelector, $aMatches); - $d = \preg_match_all(self::ELEMENTS_AND_PSEUDO_ELEMENTS_RX, $this->sSelector, $aMatches); - $this->iSpecificity = ($a * 1000) + ($b * 100) + ($c * 10) + $d; - } - return $this->iSpecificity; - } } diff --git a/src/Property/Selector/SpecificityCalculator.php b/src/Property/Selector/SpecificityCalculator.php new file mode 100644 index 000000000..b2f1323e5 --- /dev/null +++ b/src/Property/Selector/SpecificityCalculator.php @@ -0,0 +1,87 @@ +\\~]+)[\\w]+ # elements + | + \\:{1,2}( # pseudo-elements + after|before|first-letter|first-line|selection + )) + /ix'; + + /** + * @var array> + */ + private static $cache = []; + + /** + * Calculates the specificity of the given CSS selector. + * + * @return int<0, max> + * + * @internal + */ + public static function calculate(string $selector): int + { + if (!isset(self::$cache[$selector])) { + $a = 0; + /// @todo should exclude \# as well as "#" + $matches = null; + $b = \substr_count($selector, '#'); + $c = preg_match_all(self::NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX, $selector, $matches); + $d = preg_match_all(self::ELEMENTS_AND_PSEUDO_ELEMENTS_RX, $selector, $matches); + self::$cache[$selector] = ($a * 1000) + ($b * 100) + ($c * 10) + $d; + } + + return self::$cache[$selector]; + } + + /** + * Clears the cache in order to lower memory usage. + */ + public static function clearCache(): void + { + self::$cache = []; + } +} diff --git a/src/Renderable.php b/src/Renderable.php index dc1bff3c1..9ebf9a9b9 100644 --- a/src/Renderable.php +++ b/src/Renderable.php @@ -1,21 +1,10 @@ - */ - private $aIeHack; - - /** - * @var int - */ - protected $iLineNo; + private $isImportant = false; /** - * @var int + * @param non-empty-string $rule + * @param int<1, max>|null $lineNumber + * @param int<0, max>|null $columnNumber */ - protected $iColNo; - - /** - * @var array - */ - protected $aComments; - - /** - * @param string $sRule - * @param int $iLineNo - * @param int $iColNo - */ - public function __construct($sRule, $iLineNo = 0, $iColNo = 0) + public function __construct(string $rule, ?int $lineNumber = null, ?int $columnNumber = null) { - $this->sRule = $sRule; - $this->mValue = null; - $this->bIsImportant = false; - $this->aIeHack = []; - $this->iLineNo = $iLineNo; - $this->iColNo = $iColNo; - $this->aComments = []; + $this->rule = $rule; + $this->setPosition($lineNumber, $columnNumber); } /** + * @param list $commentsBeforeRule + * * @throws UnexpectedEOFException * @throws UnexpectedTokenException + * + * @internal since V8.8.0 */ - public static function parse(ParserState $oParserState): Rule + public static function parse(ParserState $parserState, array $commentsBeforeRule = []): Rule { - $aComments = $oParserState->consumeWhiteSpace(); - $oRule = new Rule( - $oParserState->parseIdentifier(!$oParserState->comes('--')), - $oParserState->currentLine(), - $oParserState->currentColumn() + $comments = \array_merge($commentsBeforeRule, $parserState->consumeWhiteSpace()); + $rule = new Rule( + $parserState->parseIdentifier(!$parserState->comes('--')), + $parserState->currentLine(), + $parserState->currentColumn() ); - $oRule->setComments($aComments); - $oRule->addComments($oParserState->consumeWhiteSpace()); - $oParserState->consume(':'); - $oValue = Value::parseValue($oParserState, self::listDelimiterForRule($oRule->getRule())); - $oRule->setValue($oValue); - if ($oParserState->getSettings()->bLenientParsing) { - while ($oParserState->comes('\\')) { - $oParserState->consume('\\'); - $oRule->addIeHack($oParserState->consume()); - $oParserState->consumeWhiteSpace(); - } + $rule->setComments($comments); + $rule->addComments($parserState->consumeWhiteSpace()); + $parserState->consume(':'); + $value = Value::parseValue($parserState, self::listDelimiterForRule($rule->getRule())); + $rule->setValue($value); + $parserState->consumeWhiteSpace(); + if ($parserState->comes('!')) { + $parserState->consume('!'); + $parserState->consumeWhiteSpace(); + $parserState->consume('important'); + $rule->setIsImportant(true); } - $oParserState->consumeWhiteSpace(); - if ($oParserState->comes('!')) { - $oParserState->consume('!'); - $oParserState->consumeWhiteSpace(); - $oParserState->consume('important'); - $oRule->setIsImportant(true); + $parserState->consumeWhiteSpace(); + while ($parserState->comes(';')) { + $parserState->consume(';'); } - $oParserState->consumeWhiteSpace(); - while ($oParserState->comes(';')) { - $oParserState->consume(';'); - } - $oParserState->consumeWhiteSpace(); - return $oRule; + return $rule; } /** - * @param string $sRule + * Returns a list of delimiters (or separators). + * The first item is the innermost separator (or, put another way, the highest-precedence operator). + * The sequence continues to the outermost separator (or lowest-precedence operator). + * + * @param non-empty-string $rule * - * @return array + * @return list */ - private static function listDelimiterForRule($sRule): array + private static function listDelimiterForRule(string $rule): array { - if (\preg_match('/^font($|-)/', $sRule)) { + if (preg_match('/^font($|-)/', $rule) === 1) { return [',', '/', ' ']; } - return [',', ' ', '/']; - } - - /** - * @return int - */ - public function getLineNo() - { - return $this->iLineNo; - } - - /** - * @return int - */ - public function getColNo() - { - return $this->iColNo; - } - /** - * @param int $iLine - * @param int $iColumn - */ - public function setPosition($iLine, $iColumn): void - { - $this->iColNo = $iColumn; - $this->iLineNo = $iLine; + switch ($rule) { + case 'src': + return [' ', ',']; + default: + return [',', ' ', '/']; + } } /** - * @param string $sRule + * @param non-empty-string $rule */ - public function setRule($sRule): void + public function setRule(string $rule): void { - $this->sRule = $sRule; + $this->rule = $rule; } /** - * @return string + * @return non-empty-string */ - public function getRule() + public function getRule(): string { - return $this->sRule; + return $this->rule; } /** @@ -170,127 +135,66 @@ public function getRule() */ public function getValue() { - return $this->mValue; + return $this->value; } /** - * @param RuleValueList|string|null $mValue + * @param RuleValueList|string|null $value */ - public function setValue($mValue): void + public function setValue($value): void { - $this->mValue = $mValue; + $this->value = $value; } /** * Adds a value to the existing value. Value will be appended if a `RuleValueList` exists of the given type. * Otherwise, the existing value will be wrapped by one. * - * @param RuleValueList|array $mValue - * @param string $sType + * @param RuleValueList|array $value */ - public function addValue($mValue, $sType = ' '): void + public function addValue($value, string $type = ' '): void { - if (!\is_array($mValue)) { - $mValue = [$mValue]; + if (!\is_array($value)) { + $value = [$value]; } - if (!$this->mValue instanceof RuleValueList || $this->mValue->getListSeparator() !== $sType) { - $mCurrentValue = $this->mValue; - $this->mValue = new RuleValueList($sType, $this->iLineNo); - if ($mCurrentValue) { - $this->mValue->addListComponent($mCurrentValue); + if (!($this->value instanceof RuleValueList) || $this->value->getListSeparator() !== $type) { + $currentValue = $this->value; + $this->value = new RuleValueList($type, $this->getLineNumber()); + if ($currentValue !== null && $currentValue !== '') { + $this->value->addListComponent($currentValue); } } - foreach ($mValue as $mValueItem) { - $this->mValue->addListComponent($mValueItem); + foreach ($value as $valueItem) { + $this->value->addListComponent($valueItem); } } - /** - * @param int $iModifier - */ - public function addIeHack($iModifier): void - { - $this->aIeHack[] = $iModifier; - } - - /** - * @param array $aModifiers - * - * @return void - */ - public function setIeHack(array $aModifiers): void - { - $this->aIeHack = $aModifiers; - } - - /** - * @return array - */ - public function getIeHack() + public function setIsImportant(bool $isImportant): void { - return $this->aIeHack; + $this->isImportant = $isImportant; } - /** - * @param bool $bIsImportant - */ - public function setIsImportant($bIsImportant): void + public function getIsImportant(): bool { - $this->bIsImportant = $bIsImportant; + return $this->isImportant; } /** - * @return bool + * @return non-empty-string */ - public function getIsImportant() - { - return $this->bIsImportant; - } - - public function __toString(): string - { - return $this->render(new OutputFormat()); - } - - public function render(OutputFormat $oOutputFormat): string + public function render(OutputFormat $outputFormat): string { - $sResult = "{$oOutputFormat->comments($this)}{$this->sRule}:{$oOutputFormat->spaceAfterRuleName()}"; - if ($this->mValue instanceof Value) { // Can also be a ValueList - $sResult .= $this->mValue->render($oOutputFormat); + $formatter = $outputFormat->getFormatter(); + $result = "{$formatter->comments($this)}{$this->rule}:{$formatter->spaceAfterRuleName()}"; + if ($this->value instanceof Value) { // Can also be a ValueList + $result .= $this->value->render($outputFormat); } else { - $sResult .= $this->mValue; - } - if (!empty($this->aIeHack)) { - $sResult .= ' \\' . \implode('\\', $this->aIeHack); + $result .= $this->value; } - if ($this->bIsImportant) { - $sResult .= ' !important'; + if ($this->isImportant) { + $result .= ' !important'; } - $sResult .= ';'; - return $sResult; - } - - /** - * @param array $aComments - */ - public function addComments(array $aComments): void - { - $this->aComments = \array_merge($this->aComments, $aComments); - } - - /** - * @return array - */ - public function getComments() - { - return $this->aComments; - } - - /** - * @param array $aComments - */ - public function setComments(array $aComments): void - { - $this->aComments = $aComments; + $result .= ';'; + return $result; } } diff --git a/src/RuleSet/AtRuleSet.php b/src/RuleSet/AtRuleSet.php index 6cc97e4dd..4cd5acc2a 100644 --- a/src/RuleSet/AtRuleSet.php +++ b/src/RuleSet/AtRuleSet.php @@ -1,5 +1,7 @@ |null $lineNumber */ - public function __construct($sType, $sArgs = '', $iLineNo = 0) + public function __construct(string $type, string $arguments = '', ?int $lineNumber = null) { - parent::__construct($iLineNo); - $this->sType = $sType; - $this->sArgs = $sArgs; + parent::__construct($lineNumber); + $this->type = $type; + $this->arguments = $arguments; } /** - * @return string + * @return non-empty-string */ - public function atRuleName() + public function atRuleName(): string { - return $this->sType; + return $this->type; } - /** - * @return string - */ - public function atRuleArgs() + public function atRuleArgs(): string { - return $this->sArgs; + return $this->arguments; } - public function __toString(): string - { - return $this->render(new OutputFormat()); - } - - public function render(OutputFormat $oOutputFormat): string + /** + * @return non-empty-string + */ + public function render(OutputFormat $outputFormat): string { - $sResult = $oOutputFormat->comments($this); - $sArgs = $this->sArgs; - if ($sArgs) { - $sArgs = ' ' . $sArgs; + $formatter = $outputFormat->getFormatter(); + $result = $formatter->comments($this); + $arguments = $this->arguments; + if ($arguments !== '') { + $arguments = ' ' . $arguments; } - $sResult .= "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{"; - $sResult .= $this->renderRules($oOutputFormat); - $sResult .= '}'; - return $sResult; + $result .= "@{$this->type}$arguments{$formatter->spaceBeforeOpeningBrace()}{"; + $result .= $this->renderRules($outputFormat); + $result .= '}'; + return $result; } } diff --git a/src/RuleSet/DeclarationBlock.php b/src/RuleSet/DeclarationBlock.php index 6232a7faa..4d0775460 100644 --- a/src/RuleSet/DeclarationBlock.php +++ b/src/RuleSet/DeclarationBlock.php @@ -1,22 +1,24 @@ + */ + private $selectors = []; + /** - * @var array + * @var RuleSet */ - private $aSelectors; + private $ruleSet; /** - * @param int $iLineNo + * @param int<1, max>|null $lineNumber */ - public function __construct($iLineNo = 0) + public function __construct(?int $lineNumber = null) { - parent::__construct($iLineNo); - $this->aSelectors = []; + $this->ruleSet = new RuleSet($lineNumber); + $this->setPosition($lineNumber); } /** - * @param CSSList|null $oList - * - * @return DeclarationBlock|false - * * @throws UnexpectedTokenException * @throws UnexpectedEOFException + * + * @internal since V8.8.0 */ - public static function parse(ParserState $oParserState, $oList = null) + public static function parse(ParserState $parserState, ?CSSList $list = null): ?DeclarationBlock { - $aComments = []; - $oResult = new DeclarationBlock($oParserState->currentLine()); + $comments = []; + $result = new DeclarationBlock($parserState->currentLine()); try { - $aSelectorParts = []; - $sStringWrapperChar = false; + $selectors = []; + $selectorParts = []; + $stringWrapperCharacter = null; + $functionNestingLevel = 0; + $consumedNextCharacter = false; + static $stopCharacters = ['{', '}', '\'', '"', '(', ')', ',']; do { - $aSelectorParts[] = $oParserState->consume(1) - . $oParserState->consumeUntil(['{', '}', '\'', '"'], false, false, $aComments); - if (\in_array($oParserState->peek(), ['\'', '"'], true) && \substr(\end($aSelectorParts), -1) != '\\') { - if ($sStringWrapperChar === false) { - $sStringWrapperChar = $oParserState->peek(); - } elseif ($sStringWrapperChar == $oParserState->peek()) { - $sStringWrapperChar = false; - } + if (!$consumedNextCharacter) { + $selectorParts[] = $parserState->consume(1); } - } while (!\in_array($oParserState->peek(), ['{', '}'], true) || $sStringWrapperChar !== false); - $oResult->setSelectors(\implode('', $aSelectorParts), $oList); - if ($oParserState->comes('{')) { - $oParserState->consume(1); + $selectorParts[] = $parserState->consumeUntil($stopCharacters, false, false, $comments); + $nextCharacter = $parserState->peek(); + $consumedNextCharacter = false; + switch ($nextCharacter) { + case '\'': + // The fallthrough is intentional. + case '"': + if (!\is_string($stringWrapperCharacter)) { + $stringWrapperCharacter = $nextCharacter; + } elseif ($stringWrapperCharacter === $nextCharacter) { + if (\substr(\end($selectorParts), -1) !== '\\') { + $stringWrapperCharacter = null; + } + } + break; + case '(': + if (!\is_string($stringWrapperCharacter)) { + ++$functionNestingLevel; + } + break; + case ')': + if (!\is_string($stringWrapperCharacter)) { + if ($functionNestingLevel <= 0) { + throw new UnexpectedTokenException('anything but', ')'); + } + --$functionNestingLevel; + } + break; + case ',': + if (!\is_string($stringWrapperCharacter) && $functionNestingLevel === 0) { + $selectors[] = \implode('', $selectorParts); + $selectorParts = []; + $parserState->consume(1); + $consumedNextCharacter = true; + } + break; + } + } while (!\in_array($nextCharacter, ['{', '}'], true) || \is_string($stringWrapperCharacter)); + if ($functionNestingLevel !== 0) { + throw new UnexpectedTokenException(')', $nextCharacter); + } + $selectors[] = \implode('', $selectorParts); // add final or only selector + $result->setSelectors($selectors, $list); + if ($parserState->comes('{')) { + $parserState->consume(1); } } catch (UnexpectedTokenException $e) { - if ($oParserState->getSettings()->bLenientParsing) { - if (!$oParserState->comes('}')) { - $oParserState->consumeUntil('}', false, true); + if ($parserState->getSettings()->usesLenientParsing()) { + if (!$parserState->comes('}')) { + $parserState->consumeUntil('}', false, true); } - return false; + return null; } else { throw $e; } } - $oResult->setComments($aComments); - RuleSet::parseRuleSet($oParserState, $oResult); - return $oResult; + $result->setComments($comments); + + RuleSet::parseRuleSet($parserState, $result->getRuleSet()); + + return $result; } /** - * @param array|string $mSelector - * @param CSSList|null $oList + * @param array|string $selectors * * @throws UnexpectedTokenException */ - public function setSelectors($mSelector, $oList = null): void + public function setSelectors($selectors, ?CSSList $list = null): void { - if (\is_array($mSelector)) { - $this->aSelectors = $mSelector; + if (\is_array($selectors)) { + $this->selectors = $selectors; } else { - $this->aSelectors = \explode(',', $mSelector); + $this->selectors = \explode(',', $selectors); } - foreach ($this->aSelectors as $iKey => $mSelector) { - if (!($mSelector instanceof Selector)) { - if ($oList === null || !($oList instanceof KeyFrame)) { - if (!Selector::isValid($mSelector)) { + foreach ($this->selectors as $key => $selector) { + if (!($selector instanceof Selector)) { + if ($list === null || !($list instanceof KeyFrame)) { + if (!Selector::isValid($selector)) { throw new UnexpectedTokenException( "Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.", - $mSelector, + $selector, 'custom' ); } - $this->aSelectors[$iKey] = new Selector($mSelector); + $this->selectors[$key] = new Selector($selector); } else { - if (!KeyframeSelector::isValid($mSelector)) { + if (!KeyframeSelector::isValid($selector)) { throw new UnexpectedTokenException( "Selector did not match '" . KeyframeSelector::SELECTOR_VALIDATION_RX . "'.", - $mSelector, + $selector, 'custom' ); } - $this->aSelectors[$iKey] = new KeyframeSelector($mSelector); + $this->selectors[$key] = new KeyframeSelector($selector); } } } @@ -128,16 +178,16 @@ public function setSelectors($mSelector, $oList = null): void /** * Remove one of the selectors of the block. * - * @param Selector|string $mSelector + * @param Selector|string $selectorToRemove */ - public function removeSelector($mSelector): bool + public function removeSelector($selectorToRemove): bool { - if ($mSelector instanceof Selector) { - $mSelector = $mSelector->getSelector(); + if ($selectorToRemove instanceof Selector) { + $selectorToRemove = $selectorToRemove->getSelector(); } - foreach ($this->aSelectors as $iKey => $oSelector) { - if ($oSelector->getSelector() === $mSelector) { - unset($this->aSelectors[$iKey]); + foreach ($this->selectors as $key => $selector) { + if ($selector->getSelector() === $selectorToRemove) { + unset($this->selectors[$key]); return true; } } @@ -145,662 +195,107 @@ public function removeSelector($mSelector): bool } /** - * @return array + * @return array */ - public function getSelectors() + public function getSelectors(): array { - return $this->aSelectors; + return $this->selectors; } - /** - * Splits shorthand declarations (e.g. `margin` or `font`) into their constituent parts. - * - * @deprecated This will be removed without substitution in version 10.0. - */ - public function expandShorthands(): void + public function getRuleSet(): RuleSet { - // border must be expanded before dimensions - $this->expandBorderShorthand(); - $this->expandDimensionsShorthand(); - $this->expandFontShorthand(); - $this->expandBackgroundShorthand(); - $this->expandListStyleShorthand(); + return $this->ruleSet; } /** - * Creates shorthand declarations (e.g. `margin` or `font`) whenever possible. - * - * @deprecated This will be removed without substitution in version 10.0. + * @see RuleSet::addRule() */ - public function createShorthands(): void + public function addRule(Rule $ruleToAdd, ?Rule $sibling = null): void { - $this->createBackgroundShorthand(); - $this->createDimensionsShorthand(); - // border must be shortened after dimensions - $this->createBorderShorthand(); - $this->createFontShorthand(); - $this->createListStyleShorthand(); + $this->ruleSet->addRule($ruleToAdd, $sibling); } /** - * Splits shorthand border declarations (e.g. `border: 1px red;`). - * - * Additional splitting happens in expandDimensionsShorthand. - * - * Multiple borders are not yet supported as of 3. + * @see RuleSet::getRules() * - * @deprecated This will be removed without substitution in version 10.0. + * @return array, Rule> */ - public function expandBorderShorthand(): void + public function getRules(?string $searchPattern = null): array { - $aBorderRules = [ - 'border', - 'border-left', - 'border-right', - 'border-top', - 'border-bottom', - ]; - $aBorderSizes = [ - 'thin', - 'medium', - 'thick', - ]; - $aRules = $this->getRulesAssoc(); - foreach ($aBorderRules as $sBorderRule) { - if (!isset($aRules[$sBorderRule])) { - continue; - } - $oRule = $aRules[$sBorderRule]; - $mRuleValue = $oRule->getValue(); - $aValues = []; - if (!$mRuleValue instanceof RuleValueList) { - $aValues[] = $mRuleValue; - } else { - $aValues = $mRuleValue->getListComponents(); - } - foreach ($aValues as $mValue) { - if ($mValue instanceof Value) { - $mNewValue = clone $mValue; - } else { - $mNewValue = $mValue; - } - if ($mValue instanceof Size) { - $sNewRuleName = $sBorderRule . '-width'; - } elseif ($mValue instanceof Color) { - $sNewRuleName = $sBorderRule . '-color'; - } else { - if (\in_array($mValue, $aBorderSizes, true)) { - $sNewRuleName = $sBorderRule . '-width'; - } else { - $sNewRuleName = $sBorderRule . '-style'; - } - } - $oNewRule = new Rule($sNewRuleName, $oRule->getLineNo(), $oRule->getColNo()); - $oNewRule->setIsImportant($oRule->getIsImportant()); - $oNewRule->addValue([$mNewValue]); - $this->addRule($oNewRule); - } - $this->removeRule($sBorderRule); - } + return $this->ruleSet->getRules($searchPattern); } /** - * Splits shorthand dimensional declarations (e.g. `margin: 0px auto;`) - * into their constituent parts. - * - * Handles `margin`, `padding`, `border-color`, `border-style` and `border-width`. + * @see RuleSet::setRules() * - * @deprecated This will be removed without substitution in version 10.0. + * @param array $rules */ - public function expandDimensionsShorthand(): void + public function setRules(array $rules): void { - $aExpansions = [ - 'margin' => 'margin-%s', - 'padding' => 'padding-%s', - 'border-color' => 'border-%s-color', - 'border-style' => 'border-%s-style', - 'border-width' => 'border-%s-width', - ]; - $aRules = $this->getRulesAssoc(); - foreach ($aExpansions as $sProperty => $sExpanded) { - if (!isset($aRules[$sProperty])) { - continue; - } - $oRule = $aRules[$sProperty]; - $mRuleValue = $oRule->getValue(); - $aValues = []; - if (!$mRuleValue instanceof RuleValueList) { - $aValues[] = $mRuleValue; - } else { - $aValues = $mRuleValue->getListComponents(); - } - $top = $right = $bottom = $left = null; - switch (\count($aValues)) { - case 1: - $top = $right = $bottom = $left = $aValues[0]; - break; - case 2: - $top = $bottom = $aValues[0]; - $left = $right = $aValues[1]; - break; - case 3: - $top = $aValues[0]; - $left = $right = $aValues[1]; - $bottom = $aValues[2]; - break; - case 4: - $top = $aValues[0]; - $right = $aValues[1]; - $bottom = $aValues[2]; - $left = $aValues[3]; - break; - } - foreach (['top', 'right', 'bottom', 'left'] as $sPosition) { - $oNewRule = new Rule(\sprintf($sExpanded, $sPosition), $oRule->getLineNo(), $oRule->getColNo()); - $oNewRule->setIsImportant($oRule->getIsImportant()); - $oNewRule->addValue(${$sPosition}); - $this->addRule($oNewRule); - } - $this->removeRule($sProperty); - } + $this->ruleSet->setRules($rules); } /** - * Converts shorthand font declarations - * (e.g. `font: 300 italic 11px/14px verdana, helvetica, sans-serif;`) - * into their constituent parts. + * @see RuleSet::getRulesAssoc() * - * @deprecated This will be removed without substitution in version 10.0. + * @return array */ - public function expandFontShorthand(): void + public function getRulesAssoc(?string $searchPattern = null): array { - $aRules = $this->getRulesAssoc(); - if (!isset($aRules['font'])) { - return; - } - $oRule = $aRules['font']; - // reset properties to 'normal' per http://www.w3.org/TR/21/fonts.html#font-shorthand - $aFontProperties = [ - 'font-style' => 'normal', - 'font-variant' => 'normal', - 'font-weight' => 'normal', - 'font-size' => 'normal', - 'line-height' => 'normal', - ]; - $mRuleValue = $oRule->getValue(); - $aValues = []; - if (!$mRuleValue instanceof RuleValueList) { - $aValues[] = $mRuleValue; - } else { - $aValues = $mRuleValue->getListComponents(); - } - foreach ($aValues as $mValue) { - if (!$mValue instanceof Value) { - $mValue = \mb_strtolower($mValue); - } - if (\in_array($mValue, ['normal', 'inherit'], true)) { - foreach (['font-style', 'font-weight', 'font-variant'] as $sProperty) { - if (!isset($aFontProperties[$sProperty])) { - $aFontProperties[$sProperty] = $mValue; - } - } - } elseif (\in_array($mValue, ['italic', 'oblique'], true)) { - $aFontProperties['font-style'] = $mValue; - } elseif ($mValue == 'small-caps') { - $aFontProperties['font-variant'] = $mValue; - } elseif ( - \in_array($mValue, ['bold', 'bolder', 'lighter'], true) - || ($mValue instanceof Size && \in_array($mValue->getSize(), \range(100.0, 900.0, 100.0), true)) - ) { - $aFontProperties['font-weight'] = $mValue; - } elseif ($mValue instanceof RuleValueList && $mValue->getListSeparator() == '/') { - [$oSize, $oHeight] = $mValue->getListComponents(); - $aFontProperties['font-size'] = $oSize; - $aFontProperties['line-height'] = $oHeight; - } elseif ($mValue instanceof Size && $mValue->getUnit() !== null) { - $aFontProperties['font-size'] = $mValue; - } else { - $aFontProperties['font-family'] = $mValue; - } - } - foreach ($aFontProperties as $sProperty => $mValue) { - $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo()); - $oNewRule->addValue($mValue); - $oNewRule->setIsImportant($oRule->getIsImportant()); - $this->addRule($oNewRule); - } - $this->removeRule('font'); + return $this->ruleSet->getRulesAssoc($searchPattern); } /** - * Converts shorthand background declarations - * (e.g. `background: url("chess.png") gray 50% repeat fixed;`) - * into their constituent parts. - * - * @see http://www.w3.org/TR/21/colors.html#propdef-background - * - * @deprecated This will be removed without substitution in version 10.0. + * @see RuleSet::removeRule() */ - public function expandBackgroundShorthand(): void + public function removeRule(Rule $ruleToRemove): void { - $aRules = $this->getRulesAssoc(); - if (!isset($aRules['background'])) { - return; - } - $oRule = $aRules['background']; - $aBgProperties = [ - 'background-color' => ['transparent'], - 'background-image' => ['none'], - 'background-repeat' => ['repeat'], - 'background-attachment' => ['scroll'], - 'background-position' => [ - new Size(0, '%', null, false, $this->iLineNo), - new Size(0, '%', null, false, $this->iLineNo), - ], - ]; - $mRuleValue = $oRule->getValue(); - $aValues = []; - if (!$mRuleValue instanceof RuleValueList) { - $aValues[] = $mRuleValue; - } else { - $aValues = $mRuleValue->getListComponents(); - } - if (\count($aValues) == 1 && $aValues[0] == 'inherit') { - foreach ($aBgProperties as $sProperty => $mValue) { - $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo()); - $oNewRule->addValue('inherit'); - $oNewRule->setIsImportant($oRule->getIsImportant()); - $this->addRule($oNewRule); - } - $this->removeRule('background'); - return; - } - $iNumBgPos = 0; - foreach ($aValues as $mValue) { - if (!$mValue instanceof Value) { - $mValue = \mb_strtolower($mValue); - } - if ($mValue instanceof URL) { - $aBgProperties['background-image'] = $mValue; - } elseif ($mValue instanceof Color) { - $aBgProperties['background-color'] = $mValue; - } elseif (\in_array($mValue, ['scroll', 'fixed'], true)) { - $aBgProperties['background-attachment'] = $mValue; - } elseif (\in_array($mValue, ['repeat', 'no-repeat', 'repeat-x', 'repeat-y'], true)) { - $aBgProperties['background-repeat'] = $mValue; - } elseif ( - \in_array($mValue, ['left', 'center', 'right', 'top', 'bottom'], true) - || $mValue instanceof Size - ) { - if ($iNumBgPos == 0) { - $aBgProperties['background-position'][0] = $mValue; - $aBgProperties['background-position'][1] = 'center'; - } else { - $aBgProperties['background-position'][$iNumBgPos] = $mValue; - } - $iNumBgPos++; - } - } - foreach ($aBgProperties as $sProperty => $mValue) { - $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo()); - $oNewRule->setIsImportant($oRule->getIsImportant()); - $oNewRule->addValue($mValue); - $this->addRule($oNewRule); - } - $this->removeRule('background'); + $this->ruleSet->removeRule($ruleToRemove); } /** - * @deprecated This will be removed without substitution in version 10.0. + * @see RuleSet::removeMatchingRules() */ - public function expandListStyleShorthand(): void + public function removeMatchingRules(string $searchPattern): void { - $aListProperties = [ - 'list-style-type' => 'disc', - 'list-style-position' => 'outside', - 'list-style-image' => 'none', - ]; - $aListStyleTypes = [ - 'none', - 'disc', - 'circle', - 'square', - 'decimal-leading-zero', - 'decimal', - 'lower-roman', - 'upper-roman', - 'lower-greek', - 'lower-alpha', - 'lower-latin', - 'upper-alpha', - 'upper-latin', - 'hebrew', - 'armenian', - 'georgian', - 'cjk-ideographic', - 'hiragana', - 'hira-gana-iroha', - 'katakana-iroha', - 'katakana', - ]; - $aListStylePositions = [ - 'inside', - 'outside', - ]; - $aRules = $this->getRulesAssoc(); - if (!isset($aRules['list-style'])) { - return; - } - $oRule = $aRules['list-style']; - $mRuleValue = $oRule->getValue(); - $aValues = []; - if (!$mRuleValue instanceof RuleValueList) { - $aValues[] = $mRuleValue; - } else { - $aValues = $mRuleValue->getListComponents(); - } - if (\count($aValues) == 1 && $aValues[0] == 'inherit') { - foreach ($aListProperties as $sProperty => $mValue) { - $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo()); - $oNewRule->addValue('inherit'); - $oNewRule->setIsImportant($oRule->getIsImportant()); - $this->addRule($oNewRule); - } - $this->removeRule('list-style'); - return; - } - foreach ($aValues as $mValue) { - if (!$mValue instanceof Value) { - $mValue = \mb_strtolower($mValue); - } - if ($mValue instanceof Url) { - $aListProperties['list-style-image'] = $mValue; - } elseif (\in_array($mValue, $aListStyleTypes, true)) { - $aListProperties['list-style-types'] = $mValue; - } elseif (\in_array($mValue, $aListStylePositions, true)) { - $aListProperties['list-style-position'] = $mValue; - } - } - foreach ($aListProperties as $sProperty => $mValue) { - $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo()); - $oNewRule->setIsImportant($oRule->getIsImportant()); - $oNewRule->addValue($mValue); - $this->addRule($oNewRule); - } - $this->removeRule('list-style'); + $this->ruleSet->removeMatchingRules($searchPattern); } /** - * @param array $aProperties - * @param string $sShorthand - * - * @deprecated This will be removed without substitution in version 10.0. + * @see RuleSet::removeAllRules() */ - public function createShorthandProperties(array $aProperties, $sShorthand): void + public function removeAllRules(): void { - $aRules = $this->getRulesAssoc(); - $oRule = null; - $aNewValues = []; - foreach ($aProperties as $sProperty) { - if (!isset($aRules[$sProperty])) { - continue; - } - $oRule = $aRules[$sProperty]; - if (!$oRule->getIsImportant()) { - $mRuleValue = $oRule->getValue(); - $aValues = []; - if (!$mRuleValue instanceof RuleValueList) { - $aValues[] = $mRuleValue; - } else { - $aValues = $mRuleValue->getListComponents(); - } - foreach ($aValues as $mValue) { - $aNewValues[] = $mValue; - } - $this->removeRule($sProperty); - } - } - if ($aNewValues !== [] && $oRule instanceof Rule) { - $oNewRule = new Rule($sShorthand, $oRule->getLineNo(), $oRule->getColNo()); - foreach ($aNewValues as $mValue) { - $oNewRule->addValue($mValue); - } - $this->addRule($oNewRule); - } + $this->ruleSet->removeAllRules(); } /** - * @deprecated This will be removed without substitution in version 10.0. - */ - public function createBackgroundShorthand(): void - { - $aProperties = [ - 'background-color', - 'background-image', - 'background-repeat', - 'background-position', - 'background-attachment', - ]; - $this->createShorthandProperties($aProperties, 'background'); - } - - /** - * @deprecated This will be removed without substitution in version 10.0. - */ - public function createListStyleShorthand(): void - { - $aProperties = [ - 'list-style-type', - 'list-style-position', - 'list-style-image', - ]; - $this->createShorthandProperties($aProperties, 'list-style'); - } - - /** - * Combines `border-color`, `border-style` and `border-width` into `border`. - * - * Should be run after `create_dimensions_shorthand`! + * @return non-empty-string * - * @deprecated This will be removed without substitution in version 10.0. - */ - public function createBorderShorthand(): void - { - $aProperties = [ - 'border-width', - 'border-style', - 'border-color', - ]; - $this->createShorthandProperties($aProperties, 'border'); - } - - /** - * Looks for long format CSS dimensional properties - * (margin, padding, border-color, border-style and border-width) - * and converts them into shorthand CSS properties. - * - * @deprecated This will be removed without substitution in version 10.0. - */ - public function createDimensionsShorthand(): void - { - $aPositions = ['top', 'right', 'bottom', 'left']; - $aExpansions = [ - 'margin' => 'margin-%s', - 'padding' => 'padding-%s', - 'border-color' => 'border-%s-color', - 'border-style' => 'border-%s-style', - 'border-width' => 'border-%s-width', - ]; - $aRules = $this->getRulesAssoc(); - foreach ($aExpansions as $sProperty => $sExpanded) { - $aFoldable = []; - foreach ($aRules as $sRuleName => $oRule) { - foreach ($aPositions as $sPosition) { - if ($sRuleName == \sprintf($sExpanded, $sPosition)) { - $aFoldable[$sRuleName] = $oRule; - } - } - } - // All four dimensions must be present - if (\count($aFoldable) == 4) { - $aValues = []; - foreach ($aPositions as $sPosition) { - $oRule = $aRules[\sprintf($sExpanded, $sPosition)]; - $mRuleValue = $oRule->getValue(); - $aRuleValues = []; - if (!$mRuleValue instanceof RuleValueList) { - $aRuleValues[] = $mRuleValue; - } else { - $aRuleValues = $mRuleValue->getListComponents(); - } - $aValues[$sPosition] = $aRuleValues; - } - $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo()); - if ((string) $aValues['left'][0] == (string) $aValues['right'][0]) { - if ((string) $aValues['top'][0] == (string) $aValues['bottom'][0]) { - if ((string) $aValues['top'][0] == (string) $aValues['left'][0]) { - // All 4 sides are equal - $oNewRule->addValue($aValues['top']); - } else { - // Top and bottom are equal, left and right are equal - $oNewRule->addValue($aValues['top']); - $oNewRule->addValue($aValues['left']); - } - } else { - // Only left and right are equal - $oNewRule->addValue($aValues['top']); - $oNewRule->addValue($aValues['left']); - $oNewRule->addValue($aValues['bottom']); - } - } else { - // No sides are equal - $oNewRule->addValue($aValues['top']); - $oNewRule->addValue($aValues['left']); - $oNewRule->addValue($aValues['bottom']); - $oNewRule->addValue($aValues['right']); - } - $this->addRule($oNewRule); - foreach ($aPositions as $sPosition) { - $this->removeRule(\sprintf($sExpanded, $sPosition)); - } - } - } - } - - /** - * Looks for long format CSS font properties (e.g. `font-weight`) and - * tries to convert them into a shorthand CSS `font` property. - * - * At least `font-size` AND `font-family` must be present in order to create a shorthand declaration. - * - * @deprecated This will be removed without substitution in version 10.0. - */ - public function createFontShorthand(): void - { - $aFontProperties = [ - 'font-style', - 'font-variant', - 'font-weight', - 'font-size', - 'line-height', - 'font-family', - ]; - $aRules = $this->getRulesAssoc(); - if (!isset($aRules['font-size']) || !isset($aRules['font-family'])) { - return; - } - $oOldRule = $aRules['font-size'] ?? $aRules['font-family']; - $oNewRule = new Rule('font', $oOldRule->getLineNo(), $oOldRule->getColNo()); - unset($oOldRule); - foreach (['font-style', 'font-variant', 'font-weight'] as $sProperty) { - if (isset($aRules[$sProperty])) { - $oRule = $aRules[$sProperty]; - $mRuleValue = $oRule->getValue(); - $aValues = []; - if (!$mRuleValue instanceof RuleValueList) { - $aValues[] = $mRuleValue; - } else { - $aValues = $mRuleValue->getListComponents(); - } - if ($aValues[0] !== 'normal') { - $oNewRule->addValue($aValues[0]); - } - } - } - // Get the font-size value - $oRule = $aRules['font-size']; - $mRuleValue = $oRule->getValue(); - $aFSValues = []; - if (!$mRuleValue instanceof RuleValueList) { - $aFSValues[] = $mRuleValue; - } else { - $aFSValues = $mRuleValue->getListComponents(); - } - // But wait to know if we have line-height to add it - if (isset($aRules['line-height'])) { - $oRule = $aRules['line-height']; - $mRuleValue = $oRule->getValue(); - $aLHValues = []; - if (!$mRuleValue instanceof RuleValueList) { - $aLHValues[] = $mRuleValue; - } else { - $aLHValues = $mRuleValue->getListComponents(); - } - if ($aLHValues[0] !== 'normal') { - $val = new RuleValueList('/', $this->iLineNo); - $val->addListComponent($aFSValues[0]); - $val->addListComponent($aLHValues[0]); - $oNewRule->addValue($val); - } - } else { - $oNewRule->addValue($aFSValues[0]); - } - $oRule = $aRules['font-family']; - $mRuleValue = $oRule->getValue(); - $aFFValues = []; - if (!$mRuleValue instanceof RuleValueList) { - $aFFValues[] = $mRuleValue; - } else { - $aFFValues = $mRuleValue->getListComponents(); - } - $oFFValue = new RuleValueList(',', $this->iLineNo); - $oFFValue->setListComponents($aFFValues); - $oNewRule->addValue($oFFValue); - - $this->addRule($oNewRule); - foreach ($aFontProperties as $sProperty) { - $this->removeRule($sProperty); - } - } - - /** * @throws OutputException */ - public function __toString(): string + public function render(OutputFormat $outputFormat): string { - return $this->render(new OutputFormat()); - } - - /** - * @throws OutputException - */ - public function render(OutputFormat $oOutputFormat): string - { - $sResult = $oOutputFormat->comments($this); - if (\count($this->aSelectors) === 0) { + $formatter = $outputFormat->getFormatter(); + $result = $formatter->comments($this); + if (\count($this->selectors) === 0) { // If all the selectors have been removed, this declaration block becomes invalid - throw new OutputException('Attempt to print declaration block with missing selector', $this->iLineNo); + throw new OutputException( + 'Attempt to print declaration block with missing selector', + $this->getLineNumber() + ); } - $sResult .= $oOutputFormat->sBeforeDeclarationBlock; - $sResult .= $oOutputFormat->implode( - $oOutputFormat->spaceBeforeSelectorSeparator() . ',' . $oOutputFormat->spaceAfterSelectorSeparator(), - $this->aSelectors + $result .= $outputFormat->getContentBeforeDeclarationBlock(); + $result .= $formatter->implode( + $formatter->spaceBeforeSelectorSeparator() . ',' . $formatter->spaceAfterSelectorSeparator(), + $this->selectors ); - $sResult .= $oOutputFormat->sAfterDeclarationBlockSelectors; - $sResult .= $oOutputFormat->spaceBeforeOpeningBrace() . '{'; - $sResult .= $this->renderRules($oOutputFormat); - $sResult .= '}'; - $sResult .= $oOutputFormat->sAfterDeclarationBlock; - return $sResult; + $result .= $outputFormat->getContentAfterDeclarationBlockSelectors(); + $result .= $formatter->spaceBeforeOpeningBrace() . '{'; + $result .= $this->ruleSet->render($outputFormat); + $result .= '}'; + $result .= $outputFormat->getContentAfterDeclarationBlock(); + + return $result; } } diff --git a/src/RuleSet/RuleContainer.php b/src/RuleSet/RuleContainer.php new file mode 100644 index 000000000..0c6c5936c --- /dev/null +++ b/src/RuleSet/RuleContainer.php @@ -0,0 +1,36 @@ + $rules + */ + public function setRules(array $rules): void; + + /** + * @return array, Rule> + */ + public function getRules(?string $searchPattern = null): array; + + /** + * @return array + */ + public function getRulesAssoc(?string $searchPattern = null): array; +} diff --git a/src/RuleSet/RuleSet.php b/src/RuleSet/RuleSet.php index 9bf080b69..521a6ae9b 100644 --- a/src/RuleSet/RuleSet.php +++ b/src/RuleSet/RuleSet.php @@ -1,14 +1,18 @@ - */ - private $aRules; + use CommentContainer; + use Position; /** - * @var int - */ - protected $iLineNo; - - /** - * @var array + * the rules in this rule set, using the property name as the key, + * with potentially multiple rules per property name. + * + * @var array, Rule>> */ - protected $aComments; + private $rules = []; /** - * @param int $iLineNo + * @param int<1, max>|null $lineNumber */ - public function __construct($iLineNo = 0) + public function __construct(?int $lineNumber = null) { - $this->aRules = []; - $this->iLineNo = $iLineNo; - $this->aComments = []; + $this->setPosition($lineNumber); } /** * @throws UnexpectedTokenException * @throws UnexpectedEOFException + * + * @internal since V8.8.0 */ - public static function parseRuleSet(ParserState $oParserState, RuleSet $oRuleSet): void + public static function parseRuleSet(ParserState $parserState, RuleSet $ruleSet): void { - while ($oParserState->comes(';')) { - $oParserState->consume(';'); + while ($parserState->comes(';')) { + $parserState->consume(';'); } - while (!$oParserState->comes('}')) { - $oRule = null; - if ($oParserState->getSettings()->bLenientParsing) { + while (true) { + $commentsBeforeRule = $parserState->consumeWhiteSpace(); + if ($parserState->comes('}')) { + break; + } + $rule = null; + if ($parserState->getSettings()->usesLenientParsing()) { try { - $oRule = Rule::parse($oParserState); + $rule = Rule::parse($parserState, $commentsBeforeRule); } catch (UnexpectedTokenException $e) { try { - $sConsume = $oParserState->consumeUntil(["\n", ';', '}'], true); + $consumedText = $parserState->consumeUntil(["\n", ';', '}'], true); // We need to “unfind” the matches to the end of the ruleSet as this will be matched later - if ($oParserState->streql(\substr($sConsume, -1), '}')) { - $oParserState->backtrack(1); + if ($parserState->streql(\substr($consumedText, -1), '}')) { + $parserState->backtrack(1); } else { - while ($oParserState->comes(';')) { - $oParserState->consume(';'); + while ($parserState->comes(';')) { + $parserState->consume(';'); } } } catch (UnexpectedTokenException $e) { @@ -78,109 +84,137 @@ public static function parseRuleSet(ParserState $oParserState, RuleSet $oRuleSet } } } else { - $oRule = Rule::parse($oParserState); + $rule = Rule::parse($parserState, $commentsBeforeRule); } - if ($oRule) { - $oRuleSet->addRule($oRule); + if ($rule instanceof Rule) { + $ruleSet->addRule($rule); } } - $oParserState->consume('}'); - } - - /** - * @return int - */ - public function getLineNo() - { - return $this->iLineNo; + $parserState->consume('}'); } /** - * @param Rule|null $oSibling + * @throws \UnexpectedValueException + * if the last `Rule` is needed as a basis for setting position, but does not have a valid position, + * which should never happen */ - public function addRule(Rule $oRule, ?Rule $oSibling = null): void + public function addRule(Rule $ruleToAdd, ?Rule $sibling = null): void { - $sRule = $oRule->getRule(); - if (!isset($this->aRules[$sRule])) { - $this->aRules[$sRule] = []; + $propertyName = $ruleToAdd->getRule(); + if (!isset($this->rules[$propertyName])) { + $this->rules[$propertyName] = []; } - $iPosition = \count($this->aRules[$sRule]); + $position = \count($this->rules[$propertyName]); - if ($oSibling !== null) { - $iSiblingPos = \array_search($oSibling, $this->aRules[$sRule], true); - if ($iSiblingPos !== false) { - $iPosition = $iSiblingPos; - $oRule->setPosition($oSibling->getLineNo(), $oSibling->getColNo() - 1); + if ($sibling !== null) { + $siblingIsInSet = false; + $siblingPosition = \array_search($sibling, $this->rules[$propertyName], true); + if ($siblingPosition !== false) { + $siblingIsInSet = true; + $position = $siblingPosition; + } else { + $siblingIsInSet = $this->hasRule($sibling); + if ($siblingIsInSet) { + // Maintain ordering within `$this->rules[$propertyName]` + // by inserting before first `Rule` with a same-or-later position than the sibling. + foreach ($this->rules[$propertyName] as $index => $rule) { + if (self::comparePositionable($rule, $sibling) >= 0) { + $position = $index; + break; + } + } + } + } + if ($siblingIsInSet) { + // Increment column number of all existing rules on same line, starting at sibling + $siblingLineNumber = $sibling->getLineNumber(); + $siblingColumnNumber = $sibling->getColumnNumber(); + foreach ($this->rules as $rulesForAProperty) { + foreach ($rulesForAProperty as $rule) { + if ( + $rule->getLineNumber() === $siblingLineNumber && + $rule->getColumnNumber() >= $siblingColumnNumber + ) { + $rule->setPosition($siblingLineNumber, $rule->getColumnNumber() + 1); + } + } + } + $ruleToAdd->setPosition($siblingLineNumber, $siblingColumnNumber); } } - if ($oRule->getLineNo() === 0 && $oRule->getColNo() === 0) { + + if ($ruleToAdd->getLineNumber() === null) { //this node is added manually, give it the next best line + $columnNumber = $ruleToAdd->getColumnNumber() ?? 0; $rules = $this->getRules(); - $pos = \count($rules); - if ($pos > 0) { - $last = $rules[$pos - 1]; - $oRule->setPosition($last->getLineNo() + 1, 0); + $rulesCount = \count($rules); + if ($rulesCount > 0) { + $last = $rules[$rulesCount - 1]; + $lastsLineNumber = $last->getLineNumber(); + if (!\is_int($lastsLineNumber)) { + throw new \UnexpectedValueException( + 'A Rule without a line number was found during addRule', + 1750718399 + ); + } + $ruleToAdd->setPosition($lastsLineNumber + 1, $columnNumber); + } else { + $ruleToAdd->setPosition(1, $columnNumber); } + } elseif ($ruleToAdd->getColumnNumber() === null) { + $ruleToAdd->setPosition($ruleToAdd->getLineNumber(), 0); } - \array_splice($this->aRules[$sRule], $iPosition, 0, [$oRule]); + \array_splice($this->rules[$propertyName], $position, 0, [$ruleToAdd]); } /** * Returns all rules matching the given rule name * - * @example $oRuleSet->getRules('font') // returns array(0 => $oRule, …) or array(). + * @example $ruleSet->getRules('font') // returns array(0 => $rule, …) or array(). * - * @example $oRuleSet->getRules('font-') + * @example $ruleSet->getRules('font-') * //returns an array of all rules either beginning with font- or matching font. * - * @param Rule|string|null $mRule + * @param string|null $searchPattern * Pattern to search for. If null, returns all rules. * If the pattern ends with a dash, all rules starting with the pattern are returned * as well as one matching the pattern with the dash excluded. - * Passing a Rule behaves like calling `getRules($mRule->getRule())`. * - * @return array + * @return array, Rule> */ - public function getRules($mRule = null) + public function getRules(?string $searchPattern = null): array { - if ($mRule instanceof Rule) { - $mRule = $mRule->getRule(); - } - /** @var array $aResult */ - $aResult = []; - foreach ($this->aRules as $sName => $aRules) { + $result = []; + foreach ($this->rules as $propertyName => $rules) { // Either no search rule is given or the search rule matches the found rule exactly // or the search rule ends in “-” and the found rule starts with the search rule. if ( - !$mRule || $sName === $mRule + $searchPattern === null || $propertyName === $searchPattern || ( - \strrpos($mRule, '-') === \strlen($mRule) - \strlen('-') - && (\strpos($sName, $mRule) === 0 || $sName === \substr($mRule, 0, -1)) + \strrpos($searchPattern, '-') === \strlen($searchPattern) - \strlen('-') + && (\strpos($propertyName, $searchPattern) === 0 + || $propertyName === \substr($searchPattern, 0, -1)) ) ) { - $aResult = \array_merge($aResult, $aRules); + $result = \array_merge($result, $rules); } } - \usort($aResult, function (Rule $first, Rule $second) { - if ($first->getLineNo() === $second->getLineNo()) { - return $first->getColNo() - $second->getColNo(); - } - return $first->getLineNo() - $second->getLineNo(); - }); - return $aResult; + \usort($result, [self::class, 'comparePositionable']); + + return $result; } /** * Overrides all the rules of this set. * - * @param array $aRules The rules to override with. + * @param array $rules The rules to override with. */ - public function setRules(array $aRules): void + public function setRules(array $rules): void { - $this->aRules = []; - foreach ($aRules as $rule) { + $this->rules = []; + foreach ($rules as $rule) { $this->addRule($rule); } } @@ -193,125 +227,149 @@ public function setRules(array $aRules): void * like `{ background-color: green; background-color; rgba(0, 127, 0, 0.7); }` will only yield an associative array * containing the rgba-valued rule while `getRules()` would yield an indexed array containing both. * - * @param Rule|string|null $mRule $mRule + * @param string|null $searchPattern * Pattern to search for. If null, returns all rules. If the pattern ends with a dash, * all rules starting with the pattern are returned as well as one matching the pattern with the dash - * excluded. Passing a Rule behaves like calling `getRules($mRule->getRule())`. + * excluded. * * @return array */ - public function getRulesAssoc($mRule = null) + public function getRulesAssoc(?string $searchPattern = null): array { - /** @var array $aResult */ - $aResult = []; - foreach ($this->getRules($mRule) as $oRule) { - $aResult[$oRule->getRule()] = $oRule; + /** @var array $result */ + $result = []; + foreach ($this->getRules($searchPattern) as $rule) { + $result[$rule->getRule()] = $rule; } - return $aResult; + + return $result; } /** - * Removes a rule from this RuleSet. This accepts all the possible values that `getRules()` accepts. - * - * If given a Rule, it will only remove this particular rule (by identity). - * If given a name, it will remove all rules by that name. - * - * Note: this is different from pre-v.2.0 behaviour of PHP-CSS-Parser, where passing a Rule instance would - * remove all rules with the same name. To get the old behaviour, use `removeRule($oRule->getRule())`. + * Removes a `Rule` from this `RuleSet` by identity. + */ + public function removeRule(Rule $ruleToRemove): void + { + $nameOfPropertyToRemove = $ruleToRemove->getRule(); + if (!isset($this->rules[$nameOfPropertyToRemove])) { + return; + } + foreach ($this->rules[$nameOfPropertyToRemove] as $key => $rule) { + if ($rule === $ruleToRemove) { + unset($this->rules[$nameOfPropertyToRemove][$key]); + } + } + } + + /** + * Removes rules by property name or search pattern. * - * @param Rule|string|null $mRule - * pattern to remove. If $mRule is null, all rules are removed. If the pattern ends in a dash, + * @param string $searchPattern + * pattern to remove. + * If the pattern ends in a dash, * all rules starting with the pattern are removed as well as one matching the pattern with the dash - * excluded. Passing a Rule behaves matches by identity. + * excluded. */ - public function removeRule($mRule): void + public function removeMatchingRules(string $searchPattern): void { - if ($mRule instanceof Rule) { - $sRule = $mRule->getRule(); - if (!isset($this->aRules[$sRule])) { - return; - } - foreach ($this->aRules[$sRule] as $iKey => $oRule) { - if ($oRule === $mRule) { - unset($this->aRules[$sRule][$iKey]); - } - } - } else { - foreach ($this->aRules as $sName => $aRules) { - // Either no search rule is given or the search rule matches the found rule exactly - // or the search rule ends in “-” and the found rule starts with the search rule or equals it - // (without the trailing dash). - if ( - !$mRule || $sName === $mRule - || (\strrpos($mRule, '-') === \strlen($mRule) - \strlen('-') - && (\strpos($sName, $mRule) === 0 || $sName === \substr($mRule, 0, -1))) - ) { - unset($this->aRules[$sName]); - } + foreach ($this->rules as $propertyName => $rules) { + // Either the search rule matches the found rule exactly + // or the search rule ends in “-” and the found rule starts with the search rule or equals it + // (without the trailing dash). + if ( + $propertyName === $searchPattern + || (\strrpos($searchPattern, '-') === \strlen($searchPattern) - \strlen('-') + && (\strpos($propertyName, $searchPattern) === 0 + || $propertyName === \substr($searchPattern, 0, -1))) + ) { + unset($this->rules[$propertyName]); } } } - public function __toString(): string + public function removeAllRules(): void { - return $this->render(new OutputFormat()); + $this->rules = []; } /** - * @return string + * @internal */ - protected function renderRules(OutputFormat $oOutputFormat) + public function render(OutputFormat $outputFormat): string { - $sResult = ''; - $bIsFirst = true; - $oNextLevel = $oOutputFormat->nextLevel(); - foreach ($this->aRules as $aRules) { - foreach ($aRules as $oRule) { - $sRendered = $oNextLevel->safely(function () use ($oRule, $oNextLevel) { - return $oRule->render($oNextLevel); - }); - if ($sRendered === null) { - continue; - } - if ($bIsFirst) { - $bIsFirst = false; - $sResult .= $oNextLevel->spaceBeforeRules(); - } else { - $sResult .= $oNextLevel->spaceBetweenRules(); - } - $sResult .= $sRendered; + return $this->renderRules($outputFormat); + } + + protected function renderRules(OutputFormat $outputFormat): string + { + $result = ''; + $isFirst = true; + $nextLevelFormat = $outputFormat->nextLevel(); + foreach ($this->getRules() as $rule) { + $nextLevelFormatter = $nextLevelFormat->getFormatter(); + $renderedRule = $nextLevelFormatter->safely(static function () use ($rule, $nextLevelFormat): string { + return $rule->render($nextLevelFormat); + }); + if ($renderedRule === null) { + continue; } + if ($isFirst) { + $isFirst = false; + $result .= $nextLevelFormatter->spaceBeforeRules(); + } else { + $result .= $nextLevelFormatter->spaceBetweenRules(); + } + $result .= $renderedRule; } - if (!$bIsFirst) { + $formatter = $outputFormat->getFormatter(); + if (!$isFirst) { // Had some output - $sResult .= $oOutputFormat->spaceAfterRules(); + $result .= $formatter->spaceAfterRules(); } - return $oOutputFormat->removeLastSemicolon($sResult); + return $formatter->removeLastSemicolon($result); } /** - * @param array $aComments + * @return int negative if `$first` is before `$second`; zero if they have the same position; positive otherwise + * + * @throws \UnexpectedValueException if either argument does not have a valid position, which should never happen */ - public function addComments(array $aComments): void + private static function comparePositionable(Positionable $first, Positionable $second): int { - $this->aComments = \array_merge($this->aComments, $aComments); - } + $firstsLineNumber = $first->getLineNumber(); + $secondsLineNumber = $second->getLineNumber(); + if (!\is_int($firstsLineNumber) || !\is_int($secondsLineNumber)) { + throw new \UnexpectedValueException( + 'A Rule without a line number was passed to comparePositionable', + 1750637683 + ); + } - /** - * @return array - */ - public function getComments() - { - return $this->aComments; + if ($firstsLineNumber === $secondsLineNumber) { + $firstsColumnNumber = $first->getColumnNumber(); + $secondsColumnNumber = $second->getColumnNumber(); + if (!\is_int($firstsColumnNumber) || !\is_int($secondsColumnNumber)) { + throw new \UnexpectedValueException( + 'A Rule without a column number was passed to comparePositionable', + 1750637761 + ); + } + return $firstsColumnNumber - $secondsColumnNumber; + } + + return $firstsLineNumber - $secondsLineNumber; } - /** - * @param array $aComments - */ - public function setComments(array $aComments): void + private function hasRule(Rule $rule): bool { - $this->aComments = $aComments; + foreach ($this->rules as $rulesForAProperty) { + if (\in_array($rule, $rulesForAProperty, true)) { + return true; + } + } + + return false; } } diff --git a/src/Settings.php b/src/Settings.php index 487c25f71..a26d10e9e 100644 --- a/src/Settings.php +++ b/src/Settings.php @@ -1,5 +1,7 @@ bMultibyteSupport = \extension_loaded('mbstring'); + $this->multibyteSupport = \extension_loaded('mbstring'); } - /** - * @return self new instance - */ - public static function create(): Settings + public static function create(): self { return new Settings(); } @@ -52,49 +51,74 @@ public static function create(): Settings * If `true` (`mbstring` extension must be enabled), will use (slower) `mb_strlen`, `mb_convert_case`, `mb_substr` * and `mb_strpos` functions. Otherwise, the normal (ASCII-Only) functions will be used. * - * @param bool $bMultibyteSupport - * - * @return self fluent interface + * @return $this fluent interface */ - public function withMultibyteSupport($bMultibyteSupport = true) + public function withMultibyteSupport(bool $multibyteSupport = true): self { - $this->bMultibyteSupport = $bMultibyteSupport; + $this->multibyteSupport = $multibyteSupport; + return $this; } /** * Sets the charset to be used if the CSS does not contain an `@charset` declaration. * - * @param string $sDefaultCharset + * @param non-empty-string $defaultCharset * - * @return self fluent interface + * @return $this fluent interface */ - public function withDefaultCharset($sDefaultCharset) + public function withDefaultCharset(string $defaultCharset): self { - $this->sDefaultCharset = $sDefaultCharset; + $this->defaultCharset = $defaultCharset; + return $this; } /** * Configures whether the parser should silently ignore invalid rules. * - * @param bool $bLenientParsing - * - * @return self fluent interface + * @return $this fluent interface */ - public function withLenientParsing($bLenientParsing = true) + public function withLenientParsing(bool $usesLenientParsing = true): self { - $this->bLenientParsing = $bLenientParsing; + $this->lenientParsing = $usesLenientParsing; + return $this; } /** * Configures the parser to choke on invalid rules. * - * @return self fluent interface + * @return $this fluent interface */ - public function beStrict() + public function beStrict(): self { return $this->withLenientParsing(false); } + + /** + * @internal + */ + public function hasMultibyteSupport(): bool + { + return $this->multibyteSupport; + } + + /** + * @return non-empty-string + * + * @internal + */ + public function getDefaultCharset(): string + { + return $this->defaultCharset; + } + + /** + * @internal + */ + public function usesLenientParsing(): bool + { + return $this->lenientParsing; + } } diff --git a/src/Value/CSSFunction.php b/src/Value/CSSFunction.php index 7ee7b91d4..86b56d9b1 100644 --- a/src/Value/CSSFunction.php +++ b/src/Value/CSSFunction.php @@ -1,9 +1,14 @@ $aArguments - * @param string $sSeparator - * @param int $iLineNo + * @param non-empty-string $name + * @param RuleValueList|array $arguments + * @param non-empty-string $separator + * @param int<1, max>|null $lineNumber */ - public function __construct($sName, $aArguments, $sSeparator = ',', $iLineNo = 0) + public function __construct(string $name, $arguments, string $separator = ',', ?int $lineNumber = null) { - if ($aArguments instanceof RuleValueList) { - $sSeparator = $aArguments->getListSeparator(); - $aArguments = $aArguments->getListComponents(); + if ($arguments instanceof RuleValueList) { + $separator = $arguments->getListSeparator(); + $arguments = $arguments->getListComponents(); } - $this->sName = $sName; - $this->iLineNo = $iLineNo; - parent::__construct($aArguments, $sSeparator, $iLineNo); + $this->name = $name; + $this->setPosition($lineNumber); // TODO: redundant? + parent::__construct($arguments, $separator, $lineNumber); } /** * @throws SourceException * @throws UnexpectedEOFException * @throws UnexpectedTokenException + * + * @internal since V8.8.0 */ - public static function parse(ParserState $oParserState, bool $bIgnoreCase = false): CSSFunction + public static function parse(ParserState $parserState, bool $ignoreCase = false): CSSFunction { - $sName = self::parseName($oParserState, $bIgnoreCase); - $oParserState->consume('('); - $mArguments = self::parseArguments($oParserState); + $name = self::parseName($parserState, $ignoreCase); + $parserState->consume('('); + $arguments = self::parseArguments($parserState); - $oResult = new CSSFunction($sName, $mArguments, ',', $oParserState->currentLine()); - $oParserState->consume(')'); + $result = new CSSFunction($name, $arguments, ',', $parserState->currentLine()); + $parserState->consume(')'); - return $oResult; + return $result; } /** @@ -55,9 +64,9 @@ public static function parse(ParserState $oParserState, bool $bIgnoreCase = fals * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ - private static function parseName(ParserState $oParserState, bool $bIgnoreCase = false): string + private static function parseName(ParserState $parserState, bool $ignoreCase = false): string { - return $oParserState->parseIdentifier($bIgnoreCase); + return $parserState->parseIdentifier($ignoreCase); } /** @@ -67,46 +76,41 @@ private static function parseName(ParserState $oParserState, bool $bIgnoreCase = * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ - private static function parseArguments(ParserState $oParserState) + private static function parseArguments(ParserState $parserState) { - return Value::parseValue($oParserState, ['=', ' ', ',']); + return Value::parseValue($parserState, ['=', ' ', ',']); } /** - * @return string + * @return non-empty-string */ - public function getName() + public function getName(): string { - return $this->sName; + return $this->name; } /** - * @param string $sName + * @param non-empty-string $name */ - public function setName($sName): void + public function setName(string $name): void { - $this->sName = $sName; + $this->name = $name; } /** - * @return array + * @return array */ - public function getArguments() - { - return $this->aComponents; - } - - public function __toString(): string + public function getArguments(): array { - return $this->render(new OutputFormat()); + return $this->components; } /** - * @return string + * @return non-empty-string */ - public function render(OutputFormat $oOutputFormat) + public function render(OutputFormat $outputFormat): string { - $aArguments = parent::render($oOutputFormat); - return "{$this->sName}({$aArguments})"; + $arguments = parent::render($outputFormat); + return "{$this->name}({$arguments})"; } } diff --git a/src/Value/CSSString.php b/src/Value/CSSString.php index 42e358c93..569311d77 100644 --- a/src/Value/CSSString.php +++ b/src/Value/CSSString.php @@ -1,5 +1,7 @@ |null $lineNumber */ - public function __construct($sString, $iLineNo = 0) + public function __construct(string $string, ?int $lineNumber = null) { - $this->sString = $sString; - parent::__construct($iLineNo); + $this->string = $string; + parent::__construct($lineNumber); } /** * @throws SourceException * @throws UnexpectedEOFException * @throws UnexpectedTokenException + * + * @internal since V8.8.0 */ - public static function parse(ParserState $oParserState): CSSString + public static function parse(ParserState $parserState): CSSString { - $sBegin = $oParserState->peek(); - $sQuote = null; - if ($sBegin === "'") { - $sQuote = "'"; - } elseif ($sBegin === '"') { - $sQuote = '"'; + $begin = $parserState->peek(); + $quote = null; + if ($begin === "'") { + $quote = "'"; + } elseif ($begin === '"') { + $quote = '"'; } - if ($sQuote !== null) { - $oParserState->consume($sQuote); + if ($quote !== null) { + $parserState->consume($quote); } - $sResult = ''; - $sContent = null; - if ($sQuote === null) { + $result = ''; + $content = null; + if ($quote === null) { // Unquoted strings end in whitespace or with braces, brackets, parentheses - while (!\preg_match('/[\\s{}()<>\\[\\]]/isu', $oParserState->peek())) { - $sResult .= $oParserState->parseCharacter(false); + while (preg_match('/[\\s{}()<>\\[\\]]/isu', $parserState->peek()) === 0) { + $result .= $parserState->parseCharacter(false); } } else { - while (!$oParserState->comes($sQuote)) { - $sContent = $oParserState->parseCharacter(false); - if ($sContent === null) { + while (!$parserState->comes($quote)) { + $content = $parserState->parseCharacter(false); + if ($content === null) { throw new SourceException( - "Non-well-formed quoted string {$oParserState->peek(3)}", - $oParserState->currentLine() + "Non-well-formed quoted string {$parserState->peek(3)}", + $parserState->currentLine() ); } - $sResult .= $sContent; + $result .= $content; } - $oParserState->consume($sQuote); + $parserState->consume($quote); } - return new CSSString($sResult, $oParserState->currentLine()); + return new CSSString($result, $parserState->currentLine()); } - /** - * @param string $sString - */ - public function setString($sString): void + public function setString(string $string): void { - $this->sString = $sString; + $this->string = $string; } - /** - * @return string - */ - public function getString() + public function getString(): string { - return $this->sString; + return $this->string; } - public function __toString(): string - { - return $this->render(new OutputFormat()); - } - - public function render(OutputFormat $oOutputFormat): string + /** + * @return non-empty-string + */ + public function render(OutputFormat $outputFormat): string { - $sString = \addslashes($this->sString); - $sString = \str_replace("\n", '\\A', $sString); - return $oOutputFormat->getStringQuotingType() . $sString . $oOutputFormat->getStringQuotingType(); + $string = \addslashes($this->string); + $string = \str_replace("\n", '\\A', $string); + return $outputFormat->getStringQuotingType() . $string . $outputFormat->getStringQuotingType(); } } diff --git a/src/Value/CalcFunction.php b/src/Value/CalcFunction.php index 49a91b8af..dba6e1dd9 100644 --- a/src/Value/CalcFunction.php +++ b/src/Value/CalcFunction.php @@ -1,5 +1,7 @@ parseIdentifier(); - if ($oParserState->peek() != '(') { + $operators = ['+', '-', '*', '/']; + $function = $parserState->parseIdentifier(); + if ($parserState->peek() !== '(') { // Found ; or end of line before an opening bracket - throw new UnexpectedTokenException('(', $oParserState->peek(), 'literal', $oParserState->currentLine()); - } elseif (!\in_array($sFunction, ['calc', '-moz-calc', '-webkit-calc'], true)) { + throw new UnexpectedTokenException('(', $parserState->peek(), 'literal', $parserState->currentLine()); + } elseif ($function !== 'calc') { // Found invalid calc definition. Example calc (... - throw new UnexpectedTokenException('calc', $sFunction, 'literal', $oParserState->currentLine()); + throw new UnexpectedTokenException('calc', $function, 'literal', $parserState->currentLine()); } - $oParserState->consume('('); - $oCalcList = new CalcRuleValueList($oParserState->currentLine()); - $oList = new RuleValueList(',', $oParserState->currentLine()); - $iNestingLevel = 0; - $iLastComponentType = null; - while (!$oParserState->comes(')') || $iNestingLevel > 0) { - if ($oParserState->isEnd() && $iNestingLevel === 0) { + $parserState->consume('('); + $calcRuleValueList = new CalcRuleValueList($parserState->currentLine()); + $list = new RuleValueList(',', $parserState->currentLine()); + $nestingLevel = 0; + $lastComponentType = null; + while (!$parserState->comes(')') || $nestingLevel > 0) { + if ($parserState->isEnd() && $nestingLevel === 0) { break; } - $oParserState->consumeWhiteSpace(); - if ($oParserState->comes('(')) { - $iNestingLevel++; - $oCalcList->addListComponent($oParserState->consume(1)); - $oParserState->consumeWhiteSpace(); + $parserState->consumeWhiteSpace(); + if ($parserState->comes('(')) { + $nestingLevel++; + $calcRuleValueList->addListComponent($parserState->consume(1)); + $parserState->consumeWhiteSpace(); continue; - } elseif ($oParserState->comes(')')) { - $iNestingLevel--; - $oCalcList->addListComponent($oParserState->consume(1)); - $oParserState->consumeWhiteSpace(); + } elseif ($parserState->comes(')')) { + $nestingLevel--; + $calcRuleValueList->addListComponent($parserState->consume(1)); + $parserState->consumeWhiteSpace(); continue; } - if ($iLastComponentType != CalcFunction::T_OPERAND) { - $oVal = Value::parsePrimitiveValue($oParserState); - $oCalcList->addListComponent($oVal); - $iLastComponentType = CalcFunction::T_OPERAND; + if ($lastComponentType !== CalcFunction::T_OPERAND) { + $value = Value::parsePrimitiveValue($parserState); + $calcRuleValueList->addListComponent($value); + $lastComponentType = CalcFunction::T_OPERAND; } else { - if (\in_array($oParserState->peek(), $aOperators, true)) { - if (($oParserState->comes('-') || $oParserState->comes('+'))) { + if (\in_array($parserState->peek(), $operators, true)) { + if (($parserState->comes('-') || $parserState->comes('+'))) { if ( - $oParserState->peek(1, -1) != ' ' - || !($oParserState->comes('- ') - || $oParserState->comes('+ ')) + $parserState->peek(1, -1) !== ' ' + || !($parserState->comes('- ') + || $parserState->comes('+ ')) ) { throw new UnexpectedTokenException( - " {$oParserState->peek()} ", - $oParserState->peek(1, -1) . $oParserState->peek(2), + " {$parserState->peek()} ", + $parserState->peek(1, -1) . $parserState->peek(2), 'literal', - $oParserState->currentLine() + $parserState->currentLine() ); } } - $oCalcList->addListComponent($oParserState->consume(1)); - $iLastComponentType = CalcFunction::T_OPERATOR; + $calcRuleValueList->addListComponent($parserState->consume(1)); + $lastComponentType = CalcFunction::T_OPERATOR; } else { throw new UnexpectedTokenException( \sprintf( 'Next token was expected to be an operand of type %s. Instead "%s" was found.', - \implode(', ', $aOperators), - $oParserState->peek() + \implode(', ', $operators), + $parserState->peek() ), '', 'custom', - $oParserState->currentLine() + $parserState->currentLine() ); } } - $oParserState->consumeWhiteSpace(); + $parserState->consumeWhiteSpace(); } - $oList->addListComponent($oCalcList); - if (!$oParserState->isEnd()) { - $oParserState->consume(')'); + $list->addListComponent($calcRuleValueList); + if (!$parserState->isEnd()) { + $parserState->consume(')'); } - return new CalcFunction($sFunction, $oList, ',', $oParserState->currentLine()); + return new CalcFunction($function, $list, ',', $parserState->currentLine()); } } diff --git a/src/Value/CalcRuleValueList.php b/src/Value/CalcRuleValueList.php index 7dbd26a1b..f904f12c7 100644 --- a/src/Value/CalcRuleValueList.php +++ b/src/Value/CalcRuleValueList.php @@ -1,5 +1,7 @@ |null $lineNumber */ - public function __construct($iLineNo = 0) + public function __construct(?int $lineNumber = null) { - parent::__construct(',', $iLineNo); + parent::__construct(',', $lineNumber); } - /** - * @return string - */ - public function render(OutputFormat $oOutputFormat) + public function render(OutputFormat $outputFormat): string { - return $oOutputFormat->implode(' ', $this->aComponents); + return $outputFormat->getFormatter()->implode(' ', $this->components); } } diff --git a/src/Value/Color.php b/src/Value/Color.php index a084fd35d..63bccab8b 100644 --- a/src/Value/Color.php +++ b/src/Value/Color.php @@ -1,5 +1,7 @@ $aColor - * @param int $iLineNo + * @param array $colorValues + * @param int<1, max>|null $lineNumber */ - public function __construct(array $aColor, $iLineNo = 0) + public function __construct(array $colorValues, ?int $lineNumber = null) { - parent::__construct(\implode('', \array_keys($aColor)), $aColor, ',', $iLineNo); + parent::__construct(\implode('', \array_keys($colorValues)), $colorValues, ',', $lineNumber); } /** * @throws UnexpectedEOFException * @throws UnexpectedTokenException + * + * @internal since V8.8.0 */ - public static function parse(ParserState $oParserState, bool $bIgnoreCase = false): CSSFunction - { - $aColor = []; - if ($oParserState->comes('#')) { - $oParserState->consume('#'); - $sValue = $oParserState->parseIdentifier(false); - if ($oParserState->strlen($sValue) === 3) { - $sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2]; - } elseif ($oParserState->strlen($sValue) === 4) { - $sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2] . $sValue[3] - . $sValue[3]; - } + public static function parse(ParserState $parserState, bool $ignoreCase = false): CSSFunction + { + return $parserState->comes('#') + ? self::parseHexColor($parserState) + : self::parseColorFunction($parserState); + } + + /** + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + private static function parseHexColor(ParserState $parserState): Color + { + $parserState->consume('#'); + $hexValue = $parserState->parseIdentifier(false); + if ($parserState->strlen($hexValue) === 3) { + $hexValue = $hexValue[0] . $hexValue[0] . $hexValue[1] . $hexValue[1] . $hexValue[2] . $hexValue[2]; + } elseif ($parserState->strlen($hexValue) === 4) { + $hexValue = $hexValue[0] . $hexValue[0] . $hexValue[1] . $hexValue[1] . $hexValue[2] . $hexValue[2] + . $hexValue[3] . $hexValue[3]; + } + + if ($parserState->strlen($hexValue) === 8) { + $colorValues = [ + 'r' => new Size(\intval($hexValue[0] . $hexValue[1], 16), null, true, $parserState->currentLine()), + 'g' => new Size(\intval($hexValue[2] . $hexValue[3], 16), null, true, $parserState->currentLine()), + 'b' => new Size(\intval($hexValue[4] . $hexValue[5], 16), null, true, $parserState->currentLine()), + 'a' => new Size( + \round(self::mapRange(\intval($hexValue[6] . $hexValue[7], 16), 0, 255, 0, 1), 2), + null, + true, + $parserState->currentLine() + ), + ]; + } elseif ($parserState->strlen($hexValue) === 6) { + $colorValues = [ + 'r' => new Size(\intval($hexValue[0] . $hexValue[1], 16), null, true, $parserState->currentLine()), + 'g' => new Size(\intval($hexValue[2] . $hexValue[3], 16), null, true, $parserState->currentLine()), + 'b' => new Size(\intval($hexValue[4] . $hexValue[5], 16), null, true, $parserState->currentLine()), + ]; + } else { + throw new UnexpectedTokenException( + 'Invalid hex color value', + $hexValue, + 'custom', + $parserState->currentLine() + ); + } + + return new Color($colorValues, $parserState->currentLine()); + } + + /** + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + private static function parseColorFunction(ParserState $parserState): CSSFunction + { + $colorValues = []; + + $colorMode = $parserState->parseIdentifier(true); + $parserState->consumeWhiteSpace(); + $parserState->consume('('); - if ($oParserState->strlen($sValue) === 8) { - $aColor = [ - 'r' => new Size(\intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()), - 'g' => new Size(\intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()), - 'b' => new Size(\intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine()), - 'a' => new Size( - \round(self::mapRange(\intval($sValue[6] . $sValue[7], 16), 0, 255, 0, 1), 2), - null, - true, - $oParserState->currentLine() - ), - ]; - } elseif ($oParserState->strlen($sValue) === 6) { - $aColor = [ - 'r' => new Size(\intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()), - 'g' => new Size(\intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()), - 'b' => new Size(\intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine()), - ]; + // CSS Color Module Level 4 says that `rgb` and `rgba` are now aliases; likewise `hsl` and `hsla`. + // So, attempt to parse with the `a`, and allow for it not being there. + switch ($colorMode) { + case 'rgb': + $colorModeForParsing = 'rgba'; + $mayHaveOptionalAlpha = true; + break; + case 'hsl': + $colorModeForParsing = 'hsla'; + $mayHaveOptionalAlpha = true; + break; + case 'rgba': + // This is handled identically to the following case. + case 'hsla': + $colorModeForParsing = $colorMode; + $mayHaveOptionalAlpha = true; + break; + default: + $colorModeForParsing = $colorMode; + $mayHaveOptionalAlpha = false; + } + + $containsVar = false; + $containsNone = false; + $isLegacySyntax = false; + $expectedArgumentCount = $parserState->strlen($colorModeForParsing); + for ($argumentIndex = 0; $argumentIndex < $expectedArgumentCount; ++$argumentIndex) { + $parserState->consumeWhiteSpace(); + $valueKey = $colorModeForParsing[$argumentIndex]; + if ($parserState->comes('var')) { + $colorValues[$valueKey] = CSSFunction::parseIdentifierOrFunction($parserState); + $containsVar = true; + } elseif (!$isLegacySyntax && $parserState->comes('none')) { + $colorValues[$valueKey] = $parserState->parseIdentifier(); + $containsNone = true; } else { - throw new UnexpectedTokenException( - 'Invalid hex color value', - $sValue, - 'custom', - $oParserState->currentLine() - ); + $colorValues[$valueKey] = Size::parse($parserState, true); } - } else { - $sColorMode = $oParserState->parseIdentifier(true); - $oParserState->consumeWhiteSpace(); - $oParserState->consume('('); - - $bContainsVar = false; - $iLength = $oParserState->strlen($sColorMode); - for ($i = 0; $i < $iLength; ++$i) { - $oParserState->consumeWhiteSpace(); - if ($oParserState->comes('var')) { - $aColor[$sColorMode[$i]] = CSSFunction::parseIdentifierOrFunction($oParserState); - $bContainsVar = true; - } else { - $aColor[$sColorMode[$i]] = Size::parse($oParserState, true); - } - if ($bContainsVar && $oParserState->comes(')')) { - // With a var argument the function can have fewer arguments - break; - } + // This must be done first, to consume comments as well, so that the `comes` test will work. + $parserState->consumeWhiteSpace(); + + // With a `var` argument, the function can have fewer arguments. + // And as of CSS Color Module Level 4, the alpha argument is optional. + $canCloseNow = + $containsVar + || ($mayHaveOptionalAlpha && $argumentIndex >= $expectedArgumentCount - 2); + if ($canCloseNow && $parserState->comes(')')) { + break; + } - $oParserState->consumeWhiteSpace(); - if ($i < ($iLength - 1)) { - $oParserState->consume(','); + // "Legacy" syntax is comma-delimited, and does not allow the `none` keyword. + // "Modern" syntax is space-delimited, with `/` as alpha delimiter. + // They cannot be mixed. + if ($argumentIndex === 0 && !$containsNone) { + // An immediate closing parenthesis is not valid. + if ($parserState->comes(')')) { + throw new UnexpectedTokenException( + 'Color function with no arguments', + '', + 'custom', + $parserState->currentLine() + ); } + $isLegacySyntax = $parserState->comes(','); } - $oParserState->consume(')'); - if ($bContainsVar) { - return new CSSFunction($sColorMode, \array_values($aColor), ',', $oParserState->currentLine()); + if ($isLegacySyntax && $argumentIndex < ($expectedArgumentCount - 1)) { + $parserState->consume(','); + } + + // In the "modern" syntax, the alpha value must be delimited with `/`. + if (!$isLegacySyntax) { + if ($containsVar) { + // If the `var` substitution encompasses more than one argument, + // the alpha deliminator may come at any time. + if ($parserState->comes('/')) { + $parserState->consume('/'); + } + } elseif (($colorModeForParsing[$argumentIndex + 1] ?? '') === 'a') { + // Alpha value is the next expected argument. + // Since a closing parenthesis was not found, a `/` separator is now required. + $parserState->consume('/'); + } } } - return new Color($aColor, $oParserState->currentLine()); + $parserState->consume(')'); + + return $containsVar + ? new CSSFunction($colorMode, \array_values($colorValues), ',', $parserState->currentLine()) + : new Color($colorValues, $parserState->currentLine()); + } + + private static function mapRange(float $value, float $fromMin, float $fromMax, float $toMin, float $toMax): float + { + $fromRange = $fromMax - $fromMin; + $toRange = $toMax - $toMin; + $multiplier = $toRange / $fromRange; + $newValue = $value - $fromMin; + $newValue *= $multiplier; + + return $newValue + $toMin; } /** - * @param float $fVal - * @param float $fFromMin - * @param float $fFromMax - * @param float $fToMin - * @param float $fToMax - * - * @return float + * @return array */ - private static function mapRange($fVal, $fFromMin, $fFromMax, $fToMin, $fToMax) + public function getColor(): array { - $fFromRange = $fFromMax - $fFromMin; - $fToRange = $fToMax - $fToMin; - $fMultiplier = $fToRange / $fFromRange; - $fNewVal = $fVal - $fFromMin; - $fNewVal *= $fMultiplier; - return $fNewVal + $fToMin; + return $this->components; } /** - * @return array + * @param array $colorValues */ - public function getColor() + public function setColor(array $colorValues): void { - return $this->aComponents; + $this->setName(\implode('', \array_keys($colorValues))); + $this->components = $colorValues; } /** - * @param array $aColor + * @return non-empty-string */ - public function setColor(array $aColor): void + public function getColorDescription(): string { - $this->setName(\implode('', \array_keys($aColor))); - $this->aComponents = $aColor; + return $this->getName(); } /** - * @return string + * @return non-empty-string */ - public function getColorDescription() + public function render(OutputFormat $outputFormat): string { - return $this->getName(); + if ($this->shouldRenderAsHex($outputFormat)) { + return $this->renderAsHex(); + } + + if ($this->shouldRenderInModernSyntax()) { + return $this->renderInModernSyntax($outputFormat); + } + + return parent::render($outputFormat); } - public function __toString(): string + private function shouldRenderAsHex(OutputFormat $outputFormat): bool { - return $this->render(new OutputFormat()); + return + $outputFormat->usesRgbHashNotation() + && $this->getRealName() === 'rgb' + && $this->allComponentsAreNumbers(); } /** - * @return string + * The function name is a concatenation of the array keys of the components, which is passed to the constructor. + * However, this can be changed by calling {@see CSSFunction::setName}, + * so is not reliable in situations where it's necessary to determine the function name based on the components. */ - public function render(OutputFormat $oOutputFormat) - { - // Shorthand RGB color values - if ($oOutputFormat->getRGBHashNotation() && \implode('', \array_keys($this->aComponents)) === 'rgb') { - $sResult = \sprintf( - '%02x%02x%02x', - $this->aComponents['r']->getSize(), - $this->aComponents['g']->getSize(), - $this->aComponents['b']->getSize() - ); - return '#' . (($sResult[0] == $sResult[1]) && ($sResult[2] == $sResult[3]) && ($sResult[4] == $sResult[5]) - ? "$sResult[0]$sResult[2]$sResult[4]" : $sResult); + private function getRealName(): string + { + return \implode('', \array_keys($this->components)); + } + + /** + * Test whether all color components are absolute numbers (CSS type `number`), not percentages or anything else. + * If any component is not an instance of `Size`, the method will also return `false`. + */ + private function allComponentsAreNumbers(): bool + { + foreach ($this->components as $component) { + if (!($component instanceof Size) || $component->getUnit() !== null) { + return false; + } } - return parent::render($oOutputFormat); + + return true; + } + + /** + * Note that this method assumes the following: + * - The `components` array has keys for `r`, `g` and `b`; + * - The values in the array are all instances of `Size`. + * + * Errors will be triggered or thrown if this is not the case. + * + * @return non-empty-string + */ + private function renderAsHex(): string + { + $result = \sprintf( + '%02x%02x%02x', + $this->components['r']->getSize(), + $this->components['g']->getSize(), + $this->components['b']->getSize() + ); + $canUseShortVariant = ($result[0] === $result[1]) && ($result[2] === $result[3]) && ($result[4] === $result[5]); + + return '#' . ($canUseShortVariant ? $result[0] . $result[2] . $result[4] : $result); + } + + /** + * The "legacy" syntax does not allow RGB colors to have a mixture of `percentage`s and `number`s, + * and does not allow `none` as any component value. + * + * The "legacy" and "modern" monikers are part of the formal W3C syntax. + * See the following for more information: + * - {@link + * https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/rgb#formal_syntax + * Description of the formal syntax for `rgb()` on MDN + * }; + * - {@link + * https://www.w3.org/TR/css-color-4/#rgb-functions + * The same in the CSS Color Module Level 4 W3C Candidate Recommendation Draft + * } (as of 13 February 2024, at time of writing). + */ + private function shouldRenderInModernSyntax(): bool + { + if ($this->hasNoneAsComponentValue()) { + return true; + } + + if (!$this->colorFunctionMayHaveMixedValueTypes($this->getRealName())) { + return false; + } + + $hasPercentage = false; + $hasNumber = false; + foreach ($this->components as $key => $value) { + if ($key === 'a') { + // Alpha can have units that don't match those of the RGB components in the "legacy" syntax. + // So it is not necessary to check it. It's also always last, hence `break` rather than `continue`. + break; + } + if (!($value instanceof Size)) { + // Unexpected, unknown, or modified via the API + return false; + } + $unit = $value->getUnit(); + // `switch` only does loose comparison + if ($unit === null) { + $hasNumber = true; + } elseif ($unit === '%') { + $hasPercentage = true; + } else { + // Invalid unit + return false; + } + } + + return $hasPercentage && $hasNumber; + } + + private function hasNoneAsComponentValue(): bool + { + return \in_array('none', $this->components, true); + } + + /** + * Some color functions, such as `rgb`, + * may have a mixture of `percentage`, `number`, or possibly other types in their arguments. + * + * Note that this excludes the alpha component, which is treated separately. + */ + private function colorFunctionMayHaveMixedValueTypes(string $function): bool + { + $functionsThatMayHaveMixedValueTypes = ['rgb', 'rgba']; + + return \in_array($function, $functionsThatMayHaveMixedValueTypes, true); + } + + /** + * @return non-empty-string + */ + private function renderInModernSyntax(OutputFormat $outputFormat): string + { + // Maybe not yet without alpha, but will be... + $componentsWithoutAlpha = $this->components; + \end($componentsWithoutAlpha); + if (\key($componentsWithoutAlpha) === 'a') { + $alpha = $this->components['a']; + unset($componentsWithoutAlpha['a']); + } + + $formatter = $outputFormat->getFormatter(); + $arguments = $formatter->implode(' ', $componentsWithoutAlpha); + if (isset($alpha)) { + $separator = $formatter->spaceBeforeListArgumentSeparator('/') + . '/' . $formatter->spaceAfterListArgumentSeparator('/'); + $arguments = $formatter->implode($separator, [$arguments, $alpha]); + } + + return $this->getName() . '(' . $arguments . ')'; } } diff --git a/src/Value/LineName.php b/src/Value/LineName.php index 5c83b5651..763cc48ea 100644 --- a/src/Value/LineName.php +++ b/src/Value/LineName.php @@ -1,5 +1,7 @@ $aComponents - * @param int $iLineNo + * @param array $components + * @param int<1, max>|null $lineNumber */ - public function __construct(array $aComponents = [], $iLineNo = 0) + public function __construct(array $components = [], ?int $lineNumber = null) { - parent::__construct($aComponents, ' ', $iLineNo); + parent::__construct($components, ' ', $lineNumber); } /** * @throws UnexpectedTokenException * @throws UnexpectedEOFException + * + * @internal since V8.8.0 */ - public static function parse(ParserState $oParserState): LineName + public static function parse(ParserState $parserState): LineName { - $oParserState->consume('['); - $oParserState->consumeWhiteSpace(); - $aNames = []; + $parserState->consume('['); + $parserState->consumeWhiteSpace(); + $names = []; do { - if ($oParserState->getSettings()->bLenientParsing) { + if ($parserState->getSettings()->usesLenientParsing()) { try { - $aNames[] = $oParserState->parseIdentifier(); + $names[] = $parserState->parseIdentifier(); } catch (UnexpectedTokenException $e) { - if (!$oParserState->comes(']')) { + if (!$parserState->comes(']')) { throw $e; } } } else { - $aNames[] = $oParserState->parseIdentifier(); + $names[] = $parserState->parseIdentifier(); } - $oParserState->consumeWhiteSpace(); - } while (!$oParserState->comes(']')); - $oParserState->consume(']'); - return new LineName($aNames, $oParserState->currentLine()); - } - - public function __toString(): string - { - return $this->render(new OutputFormat()); + $parserState->consumeWhiteSpace(); + } while (!$parserState->comes(']')); + $parserState->consume(']'); + return new LineName($names, $parserState->currentLine()); } - public function render(OutputFormat $oOutputFormat): string + /** + * @return non-empty-string + */ + public function render(OutputFormat $outputFormat): string { return '[' . parent::render(OutputFormat::createCompact()) . ']'; } diff --git a/src/Value/PrimitiveValue.php b/src/Value/PrimitiveValue.php index 055a43975..f7f940928 100644 --- a/src/Value/PrimitiveValue.php +++ b/src/Value/PrimitiveValue.php @@ -1,14 +1,7 @@ |null $lineNumber */ - public function __construct($sSeparator = ',', $iLineNo = 0) + public function __construct(string $separator = ',', ?int $lineNumber = null) { - parent::__construct([], $sSeparator, $iLineNo); + parent::__construct([], $separator, $lineNumber); } } diff --git a/src/Value/Size.php b/src/Value/Size.php index bf40f4a07..eac736d79 100644 --- a/src/Value/Size.php +++ b/src/Value/Size.php @@ -1,5 +1,7 @@ + * @var list */ private const ABSOLUTE_SIZE_UNITS = [ - 'px', 'pt', 'pc', - 'cm', 'mm', 'mozmm', 'in', - 'vh', 'dvh', 'svh', 'lvh', - 'vw', 'vmin', 'vmax', 'rem', + 'px', + 'pt', + 'pc', + 'cm', + 'mm', + 'mozmm', + 'in', + 'vh', + 'dvh', + 'svh', + 'lvh', + 'vw', + 'vmin', + 'vmax', + 'rem', ]; /** - * @var array + * @var list */ private const RELATIVE_SIZE_UNITS = ['%', 'em', 'ex', 'ch', 'fr']; /** - * @var array + * @var list */ private const NON_SIZE_UNITS = ['deg', 'grad', 'rad', 's', 'ms', 'turn', 'Hz', 'kHz']; /** - * @var array>|null + * @var array, array>|null */ private static $SIZE_UNITS = null; /** * @var float */ - private $fSize; + private $size; /** * @var string|null */ - private $sUnit; + private $unit; /** * @var bool */ - private $bIsColorComponent; + private $isColorComponent; /** - * @param float|int|string $fSize - * @param string|null $sUnit - * @param bool $bIsColorComponent - * @param int $iLineNo + * @param float|int|string $size + * @param int<1, max>|null $lineNumber */ - public function __construct($fSize, $sUnit = null, $bIsColorComponent = false, $iLineNo = 0) + public function __construct($size, ?string $unit = null, bool $isColorComponent = false, ?int $lineNumber = null) { - parent::__construct($iLineNo); - $this->fSize = (float) $fSize; - $this->sUnit = $sUnit; - $this->bIsColorComponent = $bIsColorComponent; + parent::__construct($lineNumber); + $this->size = (float) $size; + $this->unit = $unit; + $this->isColorComponent = $isColorComponent; } /** - * @param bool $bIsColorComponent - * * @throws UnexpectedEOFException * @throws UnexpectedTokenException + * + * @internal since V8.8.0 */ - public static function parse(ParserState $oParserState, $bIsColorComponent = false): Size + public static function parse(ParserState $parserState, bool $isColorComponent = false): Size { - $sSize = ''; - if ($oParserState->comes('-')) { - $sSize .= $oParserState->consume('-'); + $size = ''; + if ($parserState->comes('-')) { + $size .= $parserState->consume('-'); } - while (\is_numeric($oParserState->peek()) || $oParserState->comes('.') || $oParserState->comes('e', true)) { - if ($oParserState->comes('.')) { - $sSize .= $oParserState->consume('.'); - } elseif ($oParserState->comes('e', true)) { - $sLookahead = $oParserState->peek(1, 1); - if (\is_numeric($sLookahead) || $sLookahead === '+' || $sLookahead === '-') { - $sSize .= $oParserState->consume(2); + while (\is_numeric($parserState->peek()) || $parserState->comes('.') || $parserState->comes('e', true)) { + if ($parserState->comes('.')) { + $size .= $parserState->consume('.'); + } elseif ($parserState->comes('e', true)) { + $lookahead = $parserState->peek(1, 1); + if (\is_numeric($lookahead) || $lookahead === '+' || $lookahead === '-') { + $size .= $parserState->consume(2); } else { break; // Reached the unit part of the number like "em" or "ex" } } else { - $sSize .= $oParserState->consume(1); + $size .= $parserState->consume(1); } } - $sUnit = null; - $aSizeUnits = self::getSizeUnits(); - foreach ($aSizeUnits as $iLength => &$aValues) { - $sKey = \strtolower($oParserState->peek($iLength)); - if (\array_key_exists($sKey, $aValues)) { - if (($sUnit = $aValues[$sKey]) !== null) { - $oParserState->consume($iLength); + $unit = null; + $sizeUnits = self::getSizeUnits(); + foreach ($sizeUnits as $length => &$values) { + $key = \strtolower($parserState->peek($length)); + if (\array_key_exists($key, $values)) { + if (($unit = $values[$key]) !== null) { + $parserState->consume($length); break; } } } - return new Size((float) $sSize, $sUnit, $bIsColorComponent, $oParserState->currentLine()); + return new Size((float) $size, $unit, $isColorComponent, $parserState->currentLine()); } /** - * @return array> + * @return array, array> */ - private static function getSizeUnits() + private static function getSizeUnits(): array { if (!\is_array(self::$SIZE_UNITS)) { self::$SIZE_UNITS = []; - foreach (\array_merge(self::ABSOLUTE_SIZE_UNITS, self::RELATIVE_SIZE_UNITS, self::NON_SIZE_UNITS) as $val) { - $iSize = \strlen($val); - if (!isset(self::$SIZE_UNITS[$iSize])) { - self::$SIZE_UNITS[$iSize] = []; + $sizeUnits = \array_merge(self::ABSOLUTE_SIZE_UNITS, self::RELATIVE_SIZE_UNITS, self::NON_SIZE_UNITS); + foreach ($sizeUnits as $sizeUnit) { + $tokenLength = \strlen($sizeUnit); + if (!isset(self::$SIZE_UNITS[$tokenLength])) { + self::$SIZE_UNITS[$tokenLength] = []; } - self::$SIZE_UNITS[$iSize][\strtolower($val)] = $val; + self::$SIZE_UNITS[$tokenLength][\strtolower($sizeUnit)] = $sizeUnit; } \krsort(self::$SIZE_UNITS, SORT_NUMERIC); @@ -130,54 +145,43 @@ private static function getSizeUnits() return self::$SIZE_UNITS; } - /** - * @param string $sUnit - */ - public function setUnit($sUnit): void + public function setUnit(string $unit): void { - $this->sUnit = $sUnit; + $this->unit = $unit; } - /** - * @return string|null - */ - public function getUnit() + public function getUnit(): ?string { - return $this->sUnit; + return $this->unit; } /** - * @param float|int|string $fSize + * @param float|int|string $size */ - public function setSize($fSize): void + public function setSize($size): void { - $this->fSize = (float) $fSize; + $this->size = (float) $size; } - /** - * @return float - */ - public function getSize() + public function getSize(): float { - return $this->fSize; + return $this->size; } - /** - * @return bool - */ - public function isColorComponent() + public function isColorComponent(): bool { - return $this->bIsColorComponent; + return $this->isColorComponent; } /** * Returns whether the number stored in this Size really represents a size (as in a length of something on screen). * - * @return false if the unit an angle, a duration, a frequency or the number is a component in a Color object. + * Returns `false` if the unit is an angle, a duration, a frequency, or the number is a component in a `Color` + * object. */ public function isSize(): bool { - if (\in_array($this->sUnit, self::NON_SIZE_UNITS, true)) { + if (\in_array($this->unit, self::NON_SIZE_UNITS, true)) { return false; } return !$this->isColorComponent(); @@ -185,27 +189,25 @@ public function isSize(): bool public function isRelative(): bool { - if (\in_array($this->sUnit, self::RELATIVE_SIZE_UNITS, true)) { + if (\in_array($this->unit, self::RELATIVE_SIZE_UNITS, true)) { return true; } - if ($this->sUnit === null && $this->fSize != 0) { + if ($this->unit === null && $this->size !== 0.0) { return true; } return false; } - public function __toString(): string + /** + * @return non-empty-string + */ + public function render(OutputFormat $outputFormat): string { - return $this->render(new OutputFormat()); - } + $locale = \localeconv(); + $decimalPoint = \preg_quote($locale['decimal_point'], '/'); + $size = preg_match('/[\\d\\.]+e[+-]?\\d+/i', (string) $this->size) === 1 + ? preg_replace("/$decimalPoint?0+$/", '', \sprintf('%f', $this->size)) : (string) $this->size; - public function render(OutputFormat $oOutputFormat): string - { - $l = \localeconv(); - $sPoint = \preg_quote($l['decimal_point'], '/'); - $sSize = \preg_match('/[\\d\\.]+e[+-]?\\d+/i', (string) $this->fSize) - ? \preg_replace("/$sPoint?0+$/", '', \sprintf('%f', $this->fSize)) : $this->fSize; - return \preg_replace(["/$sPoint/", '/^(-?)0\\./'], ['.', '$1.'], $sSize) - . ($this->sUnit ?? ''); + return preg_replace(["/$decimalPoint/", '/^(-?)0\\./'], ['.', '$1.'], $size) . ($this->unit ?? ''); } } diff --git a/src/Value/URL.php b/src/Value/URL.php index b56f1288b..f6e7b974a 100644 --- a/src/Value/URL.php +++ b/src/Value/URL.php @@ -1,5 +1,7 @@ |null $lineNumber */ - public function __construct(CSSString $oURL, $iLineNo = 0) + public function __construct(CSSString $url, ?int $lineNumber = null) { - parent::__construct($iLineNo); - $this->oURL = $oURL; + parent::__construct($lineNumber); + $this->url = $url; } /** * @throws SourceException * @throws UnexpectedEOFException * @throws UnexpectedTokenException + * + * @internal since V8.8.0 */ - public static function parse(ParserState $oParserState): URL + public static function parse(ParserState $parserState): URL { - $oAnchor = $oParserState->anchor(); - $sIdentifier = ''; + $anchor = $parserState->anchor(); + $identifier = ''; for ($i = 0; $i < 3; $i++) { - $sChar = $oParserState->parseCharacter(true); - if ($sChar === null) { + $character = $parserState->parseCharacter(true); + if ($character === null) { break; } - $sIdentifier .= $sChar; + $identifier .= $character; } - $bUseUrl = $oParserState->streql($sIdentifier, 'url'); - if ($bUseUrl) { - $oParserState->consumeWhiteSpace(); - $oParserState->consume('('); + $useUrl = $parserState->streql($identifier, 'url'); + if ($useUrl) { + $parserState->consumeWhiteSpace(); + $parserState->consume('('); } else { - $oAnchor->backtrack(); + $anchor->backtrack(); } - $oParserState->consumeWhiteSpace(); - $oResult = new URL(CSSString::parse($oParserState), $oParserState->currentLine()); - if ($bUseUrl) { - $oParserState->consumeWhiteSpace(); - $oParserState->consume(')'); + $parserState->consumeWhiteSpace(); + $result = new URL(CSSString::parse($parserState), $parserState->currentLine()); + if ($useUrl) { + $parserState->consumeWhiteSpace(); + $parserState->consume(')'); } - return $oResult; - } - - public function setURL(CSSString $oURL): void - { - $this->oURL = $oURL; + return $result; } - /** - * @return CSSString - */ - public function getURL() + public function setURL(CSSString $url): void { - return $this->oURL; + $this->url = $url; } - public function __toString(): string + public function getURL(): CSSString { - return $this->render(new OutputFormat()); + return $this->url; } - public function render(OutputFormat $oOutputFormat): string + /** + * @return non-empty-string + */ + public function render(OutputFormat $outputFormat): string { - return "url({$this->oURL->render($oOutputFormat)})"; + return "url({$this->url->render($outputFormat)})"; } } diff --git a/src/Value/Value.php b/src/Value/Value.php index 5fb6e3d09..263c420da 100644 --- a/src/Value/Value.php +++ b/src/Value/Value.php @@ -1,137 +1,137 @@ |null $lineNumber */ - public function __construct($iLineNo = 0) + public function __construct(?int $lineNumber = null) { - $this->iLineNo = $iLineNo; + $this->setPosition($lineNumber); } /** - * @param array $aListDelimiters + * @param array $listDelimiters * * @return Value|string * * @throws UnexpectedTokenException * @throws UnexpectedEOFException + * + * @internal since V8.8.0 */ - public static function parseValue(ParserState $oParserState, array $aListDelimiters = []) + public static function parseValue(ParserState $parserState, array $listDelimiters = []) { - /** @var array $aStack */ - $aStack = []; - $oParserState->consumeWhiteSpace(); + /** @var list $stack */ + $stack = []; + $parserState->consumeWhiteSpace(); //Build a list of delimiters and parsed values while ( - !($oParserState->comes('}') || $oParserState->comes(';') || $oParserState->comes('!') - || $oParserState->comes(')') - || $oParserState->comes('\\') - || $oParserState->isEnd()) + !($parserState->comes('}') || $parserState->comes(';') || $parserState->comes('!') + || $parserState->comes(')') + || $parserState->isEnd()) ) { - if (\count($aStack) > 0) { - $bFoundDelimiter = false; - foreach ($aListDelimiters as $sDelimiter) { - if ($oParserState->comes($sDelimiter)) { - \array_push($aStack, $oParserState->consume($sDelimiter)); - $oParserState->consumeWhiteSpace(); - $bFoundDelimiter = true; + if (\count($stack) > 0) { + $foundDelimiter = false; + foreach ($listDelimiters as $delimiter) { + if ($parserState->comes($delimiter)) { + \array_push($stack, $parserState->consume($delimiter)); + $parserState->consumeWhiteSpace(); + $foundDelimiter = true; break; } } - if (!$bFoundDelimiter) { + if (!$foundDelimiter) { //Whitespace was the list delimiter - \array_push($aStack, ' '); + \array_push($stack, ' '); } } - \array_push($aStack, self::parsePrimitiveValue($oParserState)); - $oParserState->consumeWhiteSpace(); + \array_push($stack, self::parsePrimitiveValue($parserState)); + $parserState->consumeWhiteSpace(); } // Convert the list to list objects - foreach ($aListDelimiters as $sDelimiter) { - $iStackLength = \count($aStack); - if ($iStackLength === 1) { - return $aStack[0]; + foreach ($listDelimiters as $delimiter) { + $stackSize = \count($stack); + if ($stackSize === 1) { + return $stack[0]; } - $aNewStack = []; - for ($iStartPosition = 0; $iStartPosition < $iStackLength; ++$iStartPosition) { - if ($iStartPosition === ($iStackLength - 1) || $sDelimiter !== $aStack[$iStartPosition + 1]) { - $aNewStack[] = $aStack[$iStartPosition]; + $newStack = []; + for ($offset = 0; $offset < $stackSize; ++$offset) { + if ($offset === ($stackSize - 1) || $delimiter !== $stack[$offset + 1]) { + $newStack[] = $stack[$offset]; continue; } - $iLength = 2; //Number of elements to be joined - for ($i = $iStartPosition + 3; $i < $iStackLength; $i += 2, ++$iLength) { - if ($sDelimiter !== $aStack[$i]) { + $length = 2; //Number of elements to be joined + for ($i = $offset + 3; $i < $stackSize; $i += 2, ++$length) { + if ($delimiter !== $stack[$i]) { break; } } - $oList = new RuleValueList($sDelimiter, $oParserState->currentLine()); - for ($i = $iStartPosition; $i - $iStartPosition < $iLength * 2; $i += 2) { - $oList->addListComponent($aStack[$i]); + $list = new RuleValueList($delimiter, $parserState->currentLine()); + for ($i = $offset; $i - $offset < $length * 2; $i += 2) { + $list->addListComponent($stack[$i]); } - $aNewStack[] = $oList; - $iStartPosition += $iLength * 2 - 2; + $newStack[] = $list; + $offset += $length * 2 - 2; } - $aStack = $aNewStack; + $stack = $newStack; } - if (!isset($aStack[0])) { + if (!isset($stack[0])) { throw new UnexpectedTokenException( - " {$oParserState->peek()} ", - $oParserState->peek(1, -1) . $oParserState->peek(2), + " {$parserState->peek()} ", + $parserState->peek(1, -1) . $parserState->peek(2), 'literal', - $oParserState->currentLine() + $parserState->currentLine() ); } - return $aStack[0]; + return $stack[0]; } /** - * @param bool $bIgnoreCase - * * @return CSSFunction|string * * @throws UnexpectedEOFException * @throws UnexpectedTokenException + * + * @internal since V8.8.0 */ - public static function parseIdentifierOrFunction(ParserState $oParserState, $bIgnoreCase = false) + public static function parseIdentifierOrFunction(ParserState $parserState, bool $ignoreCase = false) { - $oAnchor = $oParserState->anchor(); - $mResult = $oParserState->parseIdentifier($bIgnoreCase); + $anchor = $parserState->anchor(); + $result = $parserState->parseIdentifier($ignoreCase); - if ($oParserState->comes('(')) { - $oAnchor->backtrack(); - if ($oParserState->streql('url', $mResult)) { - $mResult = URL::parse($oParserState); - } elseif ( - $oParserState->streql('calc', $mResult) - || $oParserState->streql('-webkit-calc', $mResult) - || $oParserState->streql('-moz-calc', $mResult) - ) { - $mResult = CalcFunction::parse($oParserState); + if ($parserState->comes('(')) { + $anchor->backtrack(); + if ($parserState->streql('url', $result)) { + $result = URL::parse($parserState); + } elseif ($parserState->streql('calc', $result)) { + $result = CalcFunction::parse($parserState); } else { - $mResult = CSSFunction::parse($oParserState, $bIgnoreCase); + $result = CSSFunction::parse($parserState, $ignoreCase); } } - return $mResult; + return $result; } /** @@ -140,78 +140,76 @@ public static function parseIdentifierOrFunction(ParserState $oParserState, $bIg * @throws UnexpectedEOFException * @throws UnexpectedTokenException * @throws SourceException + * + * @internal since V8.8.0 */ - public static function parsePrimitiveValue(ParserState $oParserState) + public static function parsePrimitiveValue(ParserState $parserState) { - $oValue = null; - $oParserState->consumeWhiteSpace(); + $value = null; + $parserState->consumeWhiteSpace(); if ( - \is_numeric($oParserState->peek()) - || ($oParserState->comes('-.') - && \is_numeric($oParserState->peek(1, 2))) - || (($oParserState->comes('-') || $oParserState->comes('.')) && \is_numeric($oParserState->peek(1, 1))) + \is_numeric($parserState->peek()) + || ($parserState->comes('-.') + && \is_numeric($parserState->peek(1, 2))) + || (($parserState->comes('-') || $parserState->comes('.')) && \is_numeric($parserState->peek(1, 1))) ) { - $oValue = Size::parse($oParserState); - } elseif ($oParserState->comes('#') || $oParserState->comes('rgb', true) || $oParserState->comes('hsl', true)) { - $oValue = Color::parse($oParserState); - } elseif ($oParserState->comes("'") || $oParserState->comes('"')) { - $oValue = CSSString::parse($oParserState); - } elseif ($oParserState->comes('progid:') && $oParserState->getSettings()->bLenientParsing) { - $oValue = self::parseMicrosoftFilter($oParserState); - } elseif ($oParserState->comes('[')) { - $oValue = LineName::parse($oParserState); - } elseif ($oParserState->comes('U+')) { - $oValue = self::parseUnicodeRangeValue($oParserState); + $value = Size::parse($parserState); + } elseif ($parserState->comes('#') || $parserState->comes('rgb', true) || $parserState->comes('hsl', true)) { + $value = Color::parse($parserState); + } elseif ($parserState->comes("'") || $parserState->comes('"')) { + $value = CSSString::parse($parserState); + } elseif ($parserState->comes('progid:') && $parserState->getSettings()->usesLenientParsing()) { + $value = self::parseMicrosoftFilter($parserState); + } elseif ($parserState->comes('[')) { + $value = LineName::parse($parserState); + } elseif ($parserState->comes('U+')) { + $value = self::parseUnicodeRangeValue($parserState); } else { - $sNextChar = $oParserState->peek(1); + $nextCharacter = $parserState->peek(1); try { - $oValue = self::parseIdentifierOrFunction($oParserState); + $value = self::parseIdentifierOrFunction($parserState); } catch (UnexpectedTokenException $e) { - if (\in_array($sNextChar, ['+', '-', '*', '/'], true)) { - $oValue = $oParserState->consume(1); + if (\in_array($nextCharacter, ['+', '-', '*', '/'], true)) { + $value = $parserState->consume(1); } else { throw $e; } } } - $oParserState->consumeWhiteSpace(); - return $oValue; + $parserState->consumeWhiteSpace(); + + return $value; } /** * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ - private static function parseMicrosoftFilter(ParserState $oParserState): CSSFunction + private static function parseMicrosoftFilter(ParserState $parserState): CSSFunction { - $sFunction = $oParserState->consumeUntil('(', false, true); - $aArguments = Value::parseValue($oParserState, [',', '=']); - return new CSSFunction($sFunction, $aArguments, ',', $oParserState->currentLine()); + $function = $parserState->consumeUntil('(', false, true); + $arguments = Value::parseValue($parserState, [',', '=']); + return new CSSFunction($function, $arguments, ',', $parserState->currentLine()); } /** * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ - private static function parseUnicodeRangeValue(ParserState $oParserState): string + private static function parseUnicodeRangeValue(ParserState $parserState): string { - $iCodepointMaxLength = 6; // Code points outside BMP can use up to six digits - $sRange = ''; - $oParserState->consume('U+'); + $codepointMaxLength = 6; // Code points outside BMP can use up to six digits + $range = ''; + $parserState->consume('U+'); do { - if ($oParserState->comes('-')) { - $iCodepointMaxLength = 13; // Max length is 2 six digit code points + the dash(-) between them + if ($parserState->comes('-')) { + $codepointMaxLength = 13; // Max length is 2 six-digit code points + the dash(-) between them } - $sRange .= $oParserState->consume(1); - } while (\strlen($sRange) < $iCodepointMaxLength && \preg_match('/[A-Fa-f0-9\\?-]/', $oParserState->peek())); - return "U+{$sRange}"; - } + $range .= $parserState->consume(1); + } while ( + (\strlen($range) < $codepointMaxLength) && (preg_match('/[A-Fa-f0-9\\?-]/', $parserState->peek()) === 1) + ); - /** - * @return int - */ - public function getLineNo() - { - return $this->iLineNo; + return "U+{$range}"; } } diff --git a/src/Value/ValueList.php b/src/Value/ValueList.php index 6aff22770..6f85f895e 100644 --- a/src/Value/ValueList.php +++ b/src/Value/ValueList.php @@ -1,5 +1,7 @@ + * @var array + * + * @internal since 8.8.0 */ - protected $aComponents; + protected $components; /** - * @var string + * @var non-empty-string + * + * @internal since 8.8.0 */ - protected $sSeparator; + protected $separator; /** - * @param array|Value|string $aComponents - * @param string $sSeparator - * @param int $iLineNo + * @param array|Value|string $components + * @param non-empty-string $separator + * @param int<1, max>|null $lineNumber */ - public function __construct($aComponents = [], $sSeparator = ',', $iLineNo = 0) + public function __construct($components = [], $separator = ',', ?int $lineNumber = null) { - parent::__construct($iLineNo); - if (!\is_array($aComponents)) { - $aComponents = [$aComponents]; + parent::__construct($lineNumber); + if (!\is_array($components)) { + $components = [$components]; } - $this->aComponents = $aComponents; - $this->sSeparator = $sSeparator; + $this->components = $components; + $this->separator = $separator; } /** - * @param Value|string $mComponent + * @param Value|string $component */ - public function addListComponent($mComponent): void + public function addListComponent($component): void { - $this->aComponents[] = $mComponent; + $this->components[] = $component; } /** - * @return array + * @return array */ - public function getListComponents() + public function getListComponents(): array { - return $this->aComponents; + return $this->components; } /** - * @param array $aComponents + * @param array $components */ - public function setListComponents(array $aComponents): void + public function setListComponents(array $components): void { - $this->aComponents = $aComponents; + $this->components = $components; } /** - * @return string + * @return non-empty-string */ - public function getListSeparator() + public function getListSeparator(): string { - return $this->sSeparator; + return $this->separator; } /** - * @param string $sSeparator + * @param non-empty-string $separator */ - public function setListSeparator($sSeparator): void + public function setListSeparator(string $separator): void { - $this->sSeparator = $sSeparator; + $this->separator = $separator; } - public function __toString(): string + public function render(OutputFormat $outputFormat): string { - return $this->render(new OutputFormat()); - } + $formatter = $outputFormat->getFormatter(); - /** - * @return string - */ - public function render(OutputFormat $oOutputFormat) - { - return $oOutputFormat->implode( - $oOutputFormat->spaceBeforeListArgumentSeparator($this->sSeparator) . $this->sSeparator - . $oOutputFormat->spaceAfterListArgumentSeparator($this->sSeparator), - $this->aComponents + return $formatter->implode( + $formatter->spaceBeforeListArgumentSeparator($this->separator) . $this->separator + . $formatter->spaceAfterListArgumentSeparator($this->separator), + $this->components ); } } diff --git a/tests/CSSList/AtRuleBlockListTest.php b/tests/CSSList/AtRuleBlockListTest.php index 5398bfba6..9a725b212 100644 --- a/tests/CSSList/AtRuleBlockListTest.php +++ b/tests/CSSList/AtRuleBlockListTest.php @@ -1,16 +1,18 @@ */ - public static function provideSyntacticlyCorrectAtRule(): array + public static function provideSyntacticallyCorrectAtRule(): array { return [ 'media print' => ['@media print { html { background: white; color: black; } }'], 'keyframes' => ['@keyframes mymove { from { top: 0px; } }'], - 'supports' => [' - @supports (display: flex) { - .flex-container > * { - text-shadow: 0 0 2px blue; - float: none; - } - .flex-container { - display: flex; + 'supports' => [ + ' + @supports (display: flex) { + .flex-container > * { + text-shadow: 0 0 2px blue; + float: none; + } + .flex-container { + display: flex; + } } - } - '], + ', + ], ]; } - /** - * @test - */ - public function implementsAtRule(): void - { - $subject = new AtRuleBlockList(''); - - self::assertInstanceOf(AtRuleBlockList::class, $subject); - } - - /** - * @test - */ - public function implementsRenderable(): void - { - $subject = new AtRuleBlockList(''); - - self::assertInstanceOf(Renderable::class, $subject); - } - - /** - * @test - */ - public function implementsCommentable(): void - { - $subject = new AtRuleBlockList(''); - - self::assertInstanceOf(Commentable::class, $subject); - } - /** * @test * @@ -87,6 +61,7 @@ public function parsesRuleNameOfMediaQueries(string $css): void $contents = (new Parser($css))->parse()->getContents(); $atRuleBlockList = $contents[0]; + self::assertInstanceOf(AtRuleBlockList::class, $atRuleBlockList); self::assertSame('media', $atRuleBlockList->atRuleName()); } @@ -100,6 +75,7 @@ public function parsesArgumentsOfMediaQueries(string $css): void $contents = (new Parser($css))->parse()->getContents(); $atRuleBlockList = $contents[0]; + self::assertInstanceOf(AtRuleBlockList::class, $atRuleBlockList); self::assertSame('(min-width: 768px)', $atRuleBlockList->atRuleArgs()); } @@ -107,9 +83,9 @@ public function parsesArgumentsOfMediaQueries(string $css): void * @test * * @dataProvider provideMinWidthMediaRule - * @dataProvider provideSyntacticlyCorrectAtRule + * @dataProvider provideSyntacticallyCorrectAtRule */ - public function parsesSyntacticlyCorrectAtRuleInStrictMode(string $css): void + public function parsesSyntacticallyCorrectAtRuleInStrictMode(string $css): void { $contents = (new Parser($css, Settings::create()->beStrict()))->parse()->getContents(); diff --git a/tests/CSSList/DocumentTest.php b/tests/CSSList/DocumentTest.php deleted file mode 100644 index e700edbaf..000000000 --- a/tests/CSSList/DocumentTest.php +++ /dev/null @@ -1,143 +0,0 @@ -subject = new Document(); - } - - /** - * @test - */ - public function implementsRenderable(): void - { - self::assertInstanceOf(Renderable::class, $this->subject); - } - - /** - * @test - */ - public function implementsCommentable(): void - { - self::assertInstanceOf(Commentable::class, $this->subject); - } - - /** - * @test - */ - public function getContentsInitiallyReturnsEmptyArray(): void - { - self::assertSame([], $this->subject->getContents()); - } - - /** - * @return array>> - */ - public static function contentsDataProvider(): array - { - return [ - 'empty array' => [[]], - '1 item' => [[new DeclarationBlock()]], - '2 items' => [[new DeclarationBlock(), new DeclarationBlock()]], - ]; - } - - /** - * @test - * - * @param array $contents - * - * @dataProvider contentsDataProvider - */ - public function setContentsSetsContents(array $contents): void - { - $this->subject->setContents($contents); - - self::assertSame($contents, $this->subject->getContents()); - } - - /** - * @test - */ - public function setContentsReplacesContentsSetInPreviousCall(): void - { - $contents2 = [new DeclarationBlock()]; - - $this->subject->setContents([new DeclarationBlock()]); - $this->subject->setContents($contents2); - - self::assertSame($contents2, $this->subject->getContents()); - } - - /** - * @test - */ - public function insertContentBeforeInsertsContentBeforeSibbling(): void - { - $bogusOne = new DeclarationBlock(); - $bogusOne->setSelectors('.bogus-one'); - $bogusTwo = new DeclarationBlock(); - $bogusTwo->setSelectors('.bogus-two'); - - $item = new DeclarationBlock(); - $item->setSelectors('.item'); - - $sibling = new DeclarationBlock(); - $sibling->setSelectors('.sibling'); - - $this->subject->setContents([$bogusOne, $sibling, $bogusTwo]); - - self::assertCount(3, $this->subject->getContents()); - - $this->subject->insertBefore($item, $sibling); - - self::assertCount(4, $this->subject->getContents()); - self::assertSame([$bogusOne, $item, $sibling, $bogusTwo], $this->subject->getContents()); - } - - /** - * @test - */ - public function insertContentBeforeAppendsIfSibblingNotFound(): void - { - $bogusOne = new DeclarationBlock(); - $bogusOne->setSelectors('.bogus-one'); - $bogusTwo = new DeclarationBlock(); - $bogusTwo->setSelectors('.bogus-two'); - - $item = new DeclarationBlock(); - $item->setSelectors('.item'); - - $sibling = new DeclarationBlock(); - $sibling->setSelectors('.sibling'); - - $orphan = new DeclarationBlock(); - $orphan->setSelectors('.forever-alone'); - - $this->subject->setContents([$bogusOne, $sibling, $bogusTwo]); - - self::assertCount(3, $this->subject->getContents()); - - $this->subject->insertBefore($item, $orphan); - - self::assertCount(4, $this->subject->getContents()); - self::assertSame([$bogusOne, $sibling, $bogusTwo, $item], $this->subject->getContents()); - } -} diff --git a/tests/CSSList/KeyFrameTest.php b/tests/CSSList/KeyFrameTest.php deleted file mode 100644 index a6c7edd15..000000000 --- a/tests/CSSList/KeyFrameTest.php +++ /dev/null @@ -1,49 +0,0 @@ -subject = new KeyFrame(); - } - - /** - * @test - */ - public function implementsAtRule(): void - { - self::assertInstanceOf(AtRule::class, $this->subject); - } - - /** - * @test - */ - public function implementsRenderable(): void - { - self::assertInstanceOf(Renderable::class, $this->subject); - } - - /** - * @test - */ - public function implementsCommentable(): void - { - self::assertInstanceOf(Commentable::class, $this->subject); - } -} diff --git a/tests/Comment/CommentTest.php b/tests/Comment/CommentTest.php index 79374afbe..52c3de550 100644 --- a/tests/Comment/CommentTest.php +++ b/tests/Comment/CommentTest.php @@ -1,118 +1,24 @@ getComment()); - } - - /** - * @test - */ - public function getCommentInitiallyReturnsCommentPassedToConstructor(): void - { - $comment = 'There is no spoon.'; - $subject = new Comment($comment); - - self::assertSame($comment, $subject->getComment()); - } - - /** - * @test - */ - public function setCommentSetsComments(): void - { - $comment = 'There is no spoon.'; - $subject = new Comment(); - - $subject->setComment($comment); - - self::assertSame($comment, $subject->getComment()); - } - - /** - * @test - */ - public function getLineNoOnEmptyInstanceReturnsReturnsZero(): void - { - $subject = new Comment(); - - self::assertSame(0, $subject->getLineNo()); - } - - /** - * @test - */ - public function getLineNoInitiallyReturnsLineNumberPassedToConstructor(): void - { - $lineNumber = 42; - $subject = new Comment('', $lineNumber); - - self::assertSame($lineNumber, $subject->getLineNo()); - } - - /** - * @test - */ - public function toStringRendersCommentEnclosedInCommentDelimiters(): void - { - $comment = 'There is no spoon.'; - $subject = new Comment(); - - $subject->setComment($comment); - - self::assertSame('/*' . $comment . '*/', (string) $subject); - } - - /** - * @test - */ - public function renderRendersCommentEnclosedInCommentDelimiters(): void - { - $comment = 'There is no spoon.'; - $subject = new Comment(); - - $subject->setComment($comment); - - self::assertSame('/*' . $comment . '*/', $subject->render(new OutputFormat())); - } - /** * @test */ public function keepCommentsInOutput(): void { - $oCss = TestsParserTest::parsedStructureForFile('comments'); + $cssDocument = TestsParserTest::parsedStructureForFile('comments'); self::assertSame('/** Number 11 **/ /** @@ -137,15 +43,15 @@ public function keepCommentsInOutput(): void position: absolute; } } -', $oCss->render(OutputFormat::createPretty())); +', $cssDocument->render(OutputFormat::createPretty())); self::assertSame( '/** Number 11 **//**' . "\n" . ' * Comments' . "\n" . ' *//* Hell */@import url("some/url.css") screen;' . '/* Number 4 *//* Number 5 */.foo,#bar{' - . '/* Number 6 */background-color:#000;}@media screen{' - . '/** Number 10 **/#foo.bar{/** Number 10b **/position:absolute;}}', - $oCss->render(OutputFormat::createCompact()->setRenderComments(true)) + . '/* Number 6 */background-color:#000}@media screen{' + . '/** Number 10 **/#foo.bar{/** Number 10b **/position:absolute}}', + $cssDocument->render(OutputFormat::createCompact()->setRenderComments(true)) ); } @@ -154,7 +60,7 @@ public function keepCommentsInOutput(): void */ public function stripCommentsFromOutput(): void { - $oCss = TestsParserTest::parsedStructureForFile('comments'); + $css = TestsParserTest::parsedStructureForFile('comments'); self::assertSame(' @import url("some/url.css") screen; @@ -167,12 +73,12 @@ public function stripCommentsFromOutput(): void position: absolute; } } -', $oCss->render(OutputFormat::createPretty()->setRenderComments(false))); +', $css->render(OutputFormat::createPretty()->setRenderComments(false))); self::assertSame( '@import url("some/url.css") screen;' - . '.foo,#bar{background-color:#000;}' - . '@media screen{#foo.bar{position:absolute;}}', - $oCss->render(OutputFormat::createCompact()) + . '.foo,#bar{background-color:#000}' + . '@media screen{#foo.bar{position:absolute}}', + $css->render(OutputFormat::createCompact()) ); } } diff --git a/tests/Functional/CSSList/DocumentTest.php b/tests/Functional/CSSList/DocumentTest.php new file mode 100644 index 000000000..71334f7f4 --- /dev/null +++ b/tests/Functional/CSSList/DocumentTest.php @@ -0,0 +1,137 @@ +render()); + } + + /** + * @test + */ + public function renderWithVirginOutputFormatCanRenderEmptyDocument(): void + { + $subject = new Document(); + + self::assertSame('', $subject->render(new OutputFormat())); + } + + /** + * @test + */ + public function renderWithDefaultOutputFormatCanRenderEmptyDocument(): void + { + $subject = new Document(); + + self::assertSame('', $subject->render(OutputFormat::create())); + } + + /** + * @test + */ + public function renderWithCompactOutputFormatCanRenderEmptyDocument(): void + { + $subject = new Document(); + + self::assertSame('', $subject->render(OutputFormat::createCompact())); + } + + /** + * @test + */ + public function renderWithPrettyOutputFormatCanRenderEmptyDocument(): void + { + $subject = new Document(); + + self::assertSame('', $subject->render(OutputFormat::createPretty())); + } + + /** + * Builds a subject with one `@charset` rule and one `@media` rule. + */ + private function buildSubjectWithAtRules(): Document + { + $subject = new Document(); + $charset = new Charset(new CSSString('UTF-8')); + $subject->append($charset); + $mediaQuery = new AtRuleBlockList('media', 'screen'); + $subject->append($mediaQuery); + + return $subject; + } + + /** + * @test + */ + public function renderWithoutOutputFormatCanRenderAtRules(): void + { + $subject = $this->buildSubjectWithAtRules(); + + $expected = '@charset "UTF-8";' . "\n" . '@media screen {}'; + self::assertSame($expected, $subject->render()); + } + + /** + * @test + */ + public function renderWithVirginOutputFormatCanRenderAtRules(): void + { + $subject = $this->buildSubjectWithAtRules(); + + $expected = '@charset "UTF-8";' . "\n" . '@media screen {}'; + self::assertSame($expected, $subject->render(new OutputFormat())); + } + + /** + * @test + */ + public function renderWithDefaultOutputFormatCanRenderAtRules(): void + { + $subject = $this->buildSubjectWithAtRules(); + + $expected = '@charset "UTF-8";' . "\n" . '@media screen {}'; + self::assertSame($expected, $subject->render(OutputFormat::create())); + } + + /** + * @test + */ + public function renderWithCompactOutputFormatCanRenderAtRules(): void + { + $subject = $this->buildSubjectWithAtRules(); + + $expected = '@charset "UTF-8";@media screen{}'; + self::assertSame($expected, $subject->render(OutputFormat::createCompact())); + } + + /** + * @test + */ + public function renderWithPrettyOutputFormatCanRenderAtRules(): void + { + $subject = $this->buildSubjectWithAtRules(); + + $expected = "\n" . '@charset "UTF-8";' . "\n\n" . '@media screen {}' . "\n"; + self::assertSame($expected, $subject->render(OutputFormat::createPretty())); + } +} diff --git a/tests/Functional/Comment/CommentTest.php b/tests/Functional/Comment/CommentTest.php new file mode 100644 index 000000000..bbfa06a75 --- /dev/null +++ b/tests/Functional/Comment/CommentTest.php @@ -0,0 +1,67 @@ +setComment($comment); + + self::assertSame('/*' . $comment . '*/', $subject->render(new OutputFormat())); + } + + /** + * @test + */ + public function renderWithDefaultOutputFormatRendersCommentEnclosedInCommentDelimiters(): void + { + $comment = 'There is no spoon.'; + $subject = new Comment(); + + $subject->setComment($comment); + + self::assertSame('/*' . $comment . '*/', $subject->render(OutputFormat::create())); + } + + /** + * @test + */ + public function renderWithCompactOutputFormatRendersCommentEnclosedInCommentDelimiters(): void + { + $comment = 'There is no spoon.'; + $subject = new Comment(); + + $subject->setComment($comment); + + self::assertSame('/*' . $comment . '*/', $subject->render(OutputFormat::createCompact())); + } + + /** + * @test + */ + public function renderWithPrettyOutputFormatRendersCommentEnclosedInCommentDelimiters(): void + { + $comment = 'There is no spoon.'; + $subject = new Comment(); + + $subject->setComment($comment); + + self::assertSame('/*' . $comment . '*/', $subject->render(OutputFormat::createPretty())); + } +} diff --git a/tests/Functional/ParserTest.php b/tests/Functional/ParserTest.php new file mode 100644 index 000000000..982bb3a23 --- /dev/null +++ b/tests/Functional/ParserTest.php @@ -0,0 +1,39 @@ +parse(); + + self::assertInstanceOf(Document::class, $result); + } + + /** + * @test + */ + public function parseWithOneRuleSetReturnsDocument(): void + { + $parser = new Parser('.thing { }'); + + $result = $parser->parse(); + + self::assertInstanceOf(Document::class, $result); + } +} diff --git a/tests/Functional/Property/SelectorTest.php b/tests/Functional/Property/SelectorTest.php new file mode 100644 index 000000000..397dbc722 --- /dev/null +++ b/tests/Functional/Property/SelectorTest.php @@ -0,0 +1,59 @@ +render(new OutputFormat())); + } + + /** + * @test + */ + public function renderWithDefaultOutputFormatRendersSelectorPassedToConstructor(): void + { + $pattern = 'a'; + $subject = new Selector($pattern); + + self::assertSame($pattern, $subject->render(OutputFormat::create())); + } + + /** + * @test + */ + public function renderWithCompactOutputFormatRendersSelectorPassedToConstructor(): void + { + $pattern = 'a'; + $subject = new Selector($pattern); + + self::assertSame($pattern, $subject->render(OutputFormat::createCompact())); + } + + /** + * @test + */ + public function renderWithPrettyOutputFormatRendersSelectorPassedToConstructor(): void + { + $pattern = 'a'; + $subject = new Selector($pattern); + + self::assertSame($pattern, $subject->render(OutputFormat::createPretty())); + } +} diff --git a/tests/Functional/RuleSet/DeclarationBlockTest.php b/tests/Functional/RuleSet/DeclarationBlockTest.php new file mode 100644 index 000000000..fe6b9e51c --- /dev/null +++ b/tests/Functional/RuleSet/DeclarationBlockTest.php @@ -0,0 +1,69 @@ + + */ + public static function provideInvalidDeclarationBlock(): array + { + return [ + 'no selector' => ['{ color: red; }'], + 'invalid selector' => ['/ { color: red; }'], + 'no opening brace' => ['body color: red; }'], + ]; + } + + /** + * @test + * + * @dataProvider provideInvalidDeclarationBlock + */ + public function parseReturnsNullForInvalidDeclarationBlock(string $invalidDeclarationBlock): void + { + $parserState = new ParserState($invalidDeclarationBlock, Settings::create()); + + $result = DeclarationBlock::parse($parserState); + + self::assertNull($result); + } + + /** + * @test + */ + public function rendersRulesInOrderProvided(): void + { + $declarationBlock = new DeclarationBlock(); + $declarationBlock->setSelectors([new Selector('.test')]); + + $rule1 = new Rule('background-color'); + $rule1->setValue('transparent'); + $declarationBlock->addRule($rule1); + + $rule2 = new Rule('background'); + $rule2->setValue('#222'); + $declarationBlock->addRule($rule2); + + $rule3 = new Rule('background-color'); + $rule3->setValue('#fff'); + $declarationBlock->addRule($rule3); + + $expectedRendering = 'background-color: transparent;background: #222;background-color: #fff'; + self::assertStringContainsString($expectedRendering, $declarationBlock->render(new OutputFormat())); + } +} diff --git a/tests/Functional/RuleSet/RuleSetTest.php b/tests/Functional/RuleSet/RuleSetTest.php new file mode 100644 index 000000000..b0d976055 --- /dev/null +++ b/tests/Functional/RuleSet/RuleSetTest.php @@ -0,0 +1,119 @@ +subject = new RuleSet(); + } + + /** + * @return array, 1: string}> + */ + public static function providePropertyNamesAndValuesAndExpectedCss(): array + { + return [ + 'no properties' => [[], ''], + 'one property' => [ + [['name' => 'color', 'value' => 'green']], + 'color: green;', + ], + 'two different properties' => [ + [ + ['name' => 'color', 'value' => 'green'], + ['name' => 'display', 'value' => 'block'], + ], + 'color: green;display: block;', + ], + 'two of the same property' => [ + [ + ['name' => 'color', 'value' => '#40A040'], + ['name' => 'color', 'value' => 'rgba(0, 128, 0, 0.25)'], + ], + 'color: #40A040;color: rgba(0, 128, 0, 0.25);', + ], + ]; + } + + /** + * @test + * + * @param list $propertyNamesAndValuesToSet + * + * @dataProvider providePropertyNamesAndValuesAndExpectedCss + */ + public function renderReturnsCssForRulesSet(array $propertyNamesAndValuesToSet, string $expectedCss): void + { + $this->setRulesFromPropertyNamesAndValues($propertyNamesAndValuesToSet); + + $result = $this->subject->render(OutputFormat::create()); + + self::assertSame($expectedCss, $result); + } + + /** + * @test + */ + public function renderWithCompactOutputFormatReturnsCssWithoutWhitespaceOrTrailingSemicolon(): void + { + $this->setRulesFromPropertyNamesAndValues([ + ['name' => 'color', 'value' => 'green'], + ['name' => 'display', 'value' => 'block'], + ]); + + $result = $this->subject->render(OutputFormat::createCompact()); + + self::assertSame('color:green;display:block', $result); + } + + /** + * @test + */ + public function renderWithPrettyOutputFormatReturnsCssWithNewlinesAroundIndentedDeclarations(): void + { + $this->setRulesFromPropertyNamesAndValues([ + ['name' => 'color', 'value' => 'green'], + ['name' => 'display', 'value' => 'block'], + ]); + + $result = $this->subject->render(OutputFormat::createPretty()); + + self::assertSame("\n\tcolor: green;\n\tdisplay: block;\n", $result); + } + + /** + * @param list $propertyNamesAndValues + */ + private function setRulesFromPropertyNamesAndValues(array $propertyNamesAndValues): void + { + $rulesToSet = \array_map( + /** + * @param array{name: string, value: string} $nameAndValue + */ + static function (array $nameAndValue): Rule { + $rule = new Rule($nameAndValue['name']); + $rule->setValue($nameAndValue['value']); + return $rule; + }, + $propertyNamesAndValues + ); + $this->subject->setRules($rulesToSet); + } +} diff --git a/tests/Functional/Value/ValueTest.php b/tests/Functional/Value/ValueTest.php new file mode 100644 index 000000000..4d65ee2a8 --- /dev/null +++ b/tests/Functional/Value/ValueTest.php @@ -0,0 +1,45 @@ + + */ + private const DEFAULT_DELIMITERS = [',', ' ', '/']; + + /** + * @test + */ + public function parsesFirstArgumentInMaxFunction(): void + { + $parsedValue = Value::parseValue( + new ParserState('max(300px, 400px);', Settings::create()), + self::DEFAULT_DELIMITERS + ); + + self::assertInstanceOf(CSSFunction::class, $parsedValue); + $size = $parsedValue->getArguments()[0]; + self::assertInstanceOf(Size::class, $size); + self::assertSame(300.0, $size->getSize()); + self::assertSame('px', $size->getUnit()); + self::assertFalse($size->isColorComponent()); + } +} diff --git a/tests/FunctionalDeprecated/.gitkeep b/tests/FunctionalDeprecated/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/tests/OutputFormatTest.php b/tests/OutputFormatTest.php index 37fc14f8b..3a8deb30e 100644 --- a/tests/OutputFormatTest.php +++ b/tests/OutputFormatTest.php @@ -1,5 +1,7 @@ oParser = new Parser(self::TEST_CSS); - $this->oDocument = $this->oParser->parse(); + $this->parser = new Parser(self::TEST_CSS); + $this->document = $this->parser->parse(); } /** @@ -57,7 +59,7 @@ public function plain(): void self::assertSame( '.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} @media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', - $this->oDocument->render() + $this->document->render() ); } @@ -67,9 +69,9 @@ public function plain(): void public function compact(): void { self::assertSame( - '.main,.test{font:italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background:white;}' - . '@media screen{.main{background-size:100% 100%;font-size:1.3em;background-color:#fff;}}', - $this->oDocument->render(OutputFormat::createCompact()) + '.main,.test{font:italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background:white}' + . '@media screen{.main{background-size:100% 100%;font-size:1.3em;background-color:#fff}}', + $this->document->render(OutputFormat::createCompact()) ); } @@ -78,7 +80,7 @@ public function compact(): void */ public function pretty(): void { - self::assertSame(self::TEST_CSS, $this->oDocument->render(OutputFormat::createPretty())); + self::assertSame(self::TEST_CSS, $this->document->render(OutputFormat::createPretty())); } /** @@ -90,7 +92,7 @@ public function spaceAfterListArgumentSeparator(): void '.main, .test {font: italic normal bold 16px/ 1.2 ' . '"Helvetica", Verdana, sans-serif;background: white;}' . "\n@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}", - $this->oDocument->render(OutputFormat::create()->setSpaceAfterListArgumentSeparator(' ')) + $this->document->render(OutputFormat::create()->setSpaceAfterListArgumentSeparator(' ')) ); } @@ -102,12 +104,15 @@ public function spaceAfterListArgumentSeparatorComplex(): void self::assertSame( '.main, .test {font: italic normal bold 16px/1.2 "Helvetica", Verdana, sans-serif;background: white;}' . "\n@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}", - $this->oDocument->render(OutputFormat::create()->setSpaceAfterListArgumentSeparator([ - 'default' => ' ', - ',' => "\t", - '/' => '', - ' ' => '', - ])) + $this->document->render( + OutputFormat::create() + ->setSpaceAfterListArgumentSeparator(' ') + ->setSpaceAfterListArgumentSeparators([ + ',' => "\t", + '/' => '', + ' ' => '', + ]) + ) ); } @@ -120,7 +125,7 @@ public function spaceAfterSelectorSeparator(): void '.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} @media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', - $this->oDocument->render(OutputFormat::create()->setSpaceAfterSelectorSeparator("\n")) + $this->document->render(OutputFormat::create()->setSpaceAfterSelectorSeparator("\n")) ); } @@ -132,7 +137,7 @@ public function stringQuotingType(): void self::assertSame( '.main, .test {font: italic normal bold 16px/1.2 \'Helvetica\',Verdana,sans-serif;background: white;} @media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', - $this->oDocument->render(OutputFormat::create()->setStringQuotingType("'")) + $this->document->render(OutputFormat::create()->setStringQuotingType("'")) ); } @@ -144,7 +149,7 @@ public function rGBHashNotation(): void self::assertSame( '.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} @media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: rgb(255,255,255);}}', - $this->oDocument->render(OutputFormat::create()->setRGBHashNotation(false)) + $this->document->render(OutputFormat::create()->setRGBHashNotation(false)) ); } @@ -156,7 +161,7 @@ public function semicolonAfterLastRule(): void self::assertSame( '.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white} @media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff}}', - $this->oDocument->render(OutputFormat::create()->setSemicolonAfterLastRule(false)) + $this->document->render(OutputFormat::create()->setSemicolonAfterLastRule(false)) ); } @@ -168,7 +173,7 @@ public function spaceAfterRuleName(): void self::assertSame( '.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} @media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', - $this->oDocument->render(OutputFormat::create()->setSpaceAfterRuleName("\t")) + $this->document->render(OutputFormat::create()->setSpaceAfterRuleName("\t")) ); } @@ -177,6 +182,11 @@ public function spaceAfterRuleName(): void */ public function spaceRules(): void { + $outputFormat = OutputFormat::create() + ->setSpaceBeforeRules("\n") + ->setSpaceBetweenRules("\n") + ->setSpaceAfterRules("\n"); + self::assertSame('.main, .test { font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif; background: white; @@ -185,7 +195,7 @@ public function spaceRules(): void background-size: 100% 100%; font-size: 1.3em; background-color: #fff; - }}', $this->oDocument->render(OutputFormat::create()->set('Space*Rules', "\n"))); + }}', $this->document->render($outputFormat)); } /** @@ -193,12 +203,17 @@ public function spaceRules(): void */ public function spaceBlocks(): void { + $outputFormat = OutputFormat::create() + ->setSpaceBeforeBlocks("\n") + ->setSpaceBetweenBlocks("\n") + ->setSpaceAfterBlocks("\n"); + self::assertSame(' .main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} @media screen { .main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;} } -', $this->oDocument->render(OutputFormat::create()->set('Space*Blocks', "\n"))); +', $this->document->render($outputFormat)); } /** @@ -206,6 +221,14 @@ public function spaceBlocks(): void */ public function spaceBoth(): void { + $outputFormat = OutputFormat::create() + ->setSpaceBeforeRules("\n") + ->setSpaceBetweenRules("\n") + ->setSpaceAfterRules("\n") + ->setSpaceBeforeBlocks("\n") + ->setSpaceBetweenBlocks("\n") + ->setSpaceAfterBlocks("\n"); + self::assertSame(' .main, .test { font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif; @@ -218,7 +241,7 @@ public function spaceBoth(): void background-color: #fff; } } -', $this->oDocument->render(OutputFormat::create()->set('Space*Rules', "\n")->set('Space*Blocks', "\n"))); +', $this->document->render($outputFormat)); } /** @@ -226,10 +249,13 @@ public function spaceBoth(): void */ public function spaceBetweenBlocks(): void { + $outputFormat = OutputFormat::create() + ->setSpaceBetweenBlocks(''); + self::assertSame( '.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;}' . '@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', - $this->oDocument->render(OutputFormat::create()->setSpaceBetweenBlocks('')) + $this->document->render($outputFormat) ); } @@ -238,6 +264,15 @@ public function spaceBetweenBlocks(): void */ public function indentation(): void { + $outputFormat = OutputFormat::create() + ->setSpaceBeforeRules("\n") + ->setSpaceBetweenRules("\n") + ->setSpaceAfterRules("\n") + ->setSpaceBeforeBlocks("\n") + ->setSpaceBetweenBlocks("\n") + ->setSpaceAfterBlocks("\n") + ->setIndentation(''); + self::assertSame(' .main, .test { font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif; @@ -250,10 +285,7 @@ public function indentation(): void background-color: #fff; } } -', $this->oDocument->render(OutputFormat::create() - ->set('Space*Rules', "\n") - ->set('Space*Blocks', "\n") - ->setIndentation(''))); +', $this->document->render($outputFormat)); } /** @@ -261,10 +293,13 @@ public function indentation(): void */ public function spaceBeforeBraces(): void { + $outputFormat = OutputFormat::create() + ->setSpaceBeforeOpeningBrace(''); + self::assertSame( '.main, .test{font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} @media screen{.main{background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', - $this->oDocument->render(OutputFormat::create()->setSpaceBeforeOpeningBrace('')) + $this->document->render($outputFormat) ); } @@ -275,16 +310,18 @@ public function ignoreExceptionsOff(): void { $this->expectException(OutputException::class); - $aBlocks = $this->oDocument->getAllDeclarationBlocks(); - $oFirstBlock = $aBlocks[0]; - $oFirstBlock->removeSelector('.main'); + $outputFormat = OutputFormat::create()->setIgnoreExceptions(false); + + $declarationBlocks = $this->document->getAllDeclarationBlocks(); + $firstDeclarationBlock = $declarationBlocks[0]; + $firstDeclarationBlock->removeSelector('.main'); self::assertSame( '.test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;} @media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', - $this->oDocument->render(OutputFormat::create()->setIgnoreExceptions(false)) + $this->document->render($outputFormat) ); - $oFirstBlock->removeSelector('.test'); - $this->oDocument->render(OutputFormat::create()->setIgnoreExceptions(false)); + $firstDeclarationBlock->removeSelector('.test'); + $this->document->render($outputFormat); } /** @@ -292,13 +329,15 @@ public function ignoreExceptionsOff(): void */ public function ignoreExceptionsOn(): void { - $aBlocks = $this->oDocument->getAllDeclarationBlocks(); - $oFirstBlock = $aBlocks[0]; - $oFirstBlock->removeSelector('.main'); - $oFirstBlock->removeSelector('.test'); + $outputFormat = OutputFormat::create()->setIgnoreExceptions(true); + + $declarationBlocks = $this->document->getAllDeclarationBlocks(); + $firstDeclarationBlock = $declarationBlocks[0]; + $firstDeclarationBlock->removeSelector('.main'); + $firstDeclarationBlock->removeSelector('.test'); self::assertSame( '@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', - $this->oDocument->render(OutputFormat::create()->setIgnoreExceptions(true)) + $this->document->render($outputFormat) ); } } diff --git a/tests/ParserTest.php b/tests/ParserTest.php index 2c1c72877..b80280a77 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -1,8 +1,12 @@ getContents(); self::assertCount(1, $cssList); - self::assertInstanceOf(RuleSet::class, $cssList[0]); + self::assertInstanceOf(DeclarationBlock::class, $cssList[0]); } /** @@ -59,30 +60,27 @@ public function parseForOneRuleSetReturnsDocumentWithOneRuleSet(): void */ public function files(): void { - $sDirectory = __DIR__ . '/fixtures'; - if ($rHandle = \opendir($sDirectory)) { - /* This is the correct way to loop over the directory. */ - while (false !== ($sFileName = \readdir($rHandle))) { - if (\strpos($sFileName, '.') === 0) { - continue; - } - if (\strrpos($sFileName, '.css') !== \strlen($sFileName) - \strlen('.css')) { - continue; - } - if (\strpos($sFileName, '-') === 0) { - // Either a file which SHOULD fail (at least in strict mode) - // or a future test of a as-of-now missing feature - continue; - } - $oParser = new Parser(\file_get_contents($sDirectory . '/' . $sFileName)); - try { - self::assertNotEquals('', $oParser->parse()->render()); - } catch (\Exception $e) { - self::fail($e); - } + $directory = __DIR__ . '/fixtures'; + $directoryHandle = opendir($directory); + + /* This is the correct way to loop over the directory. */ + while (false !== ($filename = \readdir($directoryHandle))) { + if (\strpos($filename, '.') === 0) { + continue; + } + if (\strrpos($filename, '.css') !== \strlen($filename) - \strlen('.css')) { + continue; + } + if (\strpos($filename, '-') === 0) { + // Either a file which SHOULD fail (at least in strict mode) + // or a future test of an as-of-now missing feature + continue; } - \closedir($rHandle); + $parser = new Parser(file_get_contents($directory . '/' . $filename)); + self::assertNotSame('', $parser->parse()->render()); } + + \closedir($directoryHandle); } /** @@ -92,66 +90,69 @@ public function files(): void */ public function colorParsing(): void { - $oDoc = self::parsedStructureForFile('colortest'); - foreach ($oDoc->getAllRuleSets() as $oRuleSet) { - if (!$oRuleSet instanceof DeclarationBlock) { - continue; - } - $sSelector = $oRuleSet->getSelectors(); - $sSelector = $sSelector[0]->getSelector(); - if ($sSelector === '#mine') { - $aColorRule = $oRuleSet->getRules('color'); - $oColor = $aColorRule[0]->getValue(); - self::assertSame('red', $oColor); - $aColorRule = $oRuleSet->getRules('background-'); - $oColor = $aColorRule[0]->getValue(); + $document = self::parsedStructureForFile('colortest'); + foreach ($document->getAllDeclarationBlocks() as $declarationBlock) { + $selectors = $declarationBlock->getSelectors(); + $selector = $selectors[0]->getSelector(); + if ($selector === '#mine') { + $colorRules = $declarationBlock->getRules('color'); + $colorRuleValue = $colorRules[0]->getValue(); + self::assertSame('red', $colorRuleValue); + $colorRules = $declarationBlock->getRules('background-'); + $colorRuleValue = $colorRules[0]->getValue(); + self::assertInstanceOf(Color::class, $colorRuleValue); self::assertEquals([ - 'r' => new Size(35.0, null, true, $oColor->getLineNo()), - 'g' => new Size(35.0, null, true, $oColor->getLineNo()), - 'b' => new Size(35.0, null, true, $oColor->getLineNo()), - ], $oColor->getColor()); - $aColorRule = $oRuleSet->getRules('border-color'); - $oColor = $aColorRule[0]->getValue(); + 'r' => new Size(35.0, null, true, $colorRuleValue->getLineNumber()), + 'g' => new Size(35.0, null, true, $colorRuleValue->getLineNumber()), + 'b' => new Size(35.0, null, true, $colorRuleValue->getLineNumber()), + ], $colorRuleValue->getColor()); + $colorRules = $declarationBlock->getRules('border-color'); + $colorRuleValue = $colorRules[0]->getValue(); + self::assertInstanceOf(Color::class, $colorRuleValue); self::assertEquals([ - 'r' => new Size(10.0, null, true, $oColor->getLineNo()), - 'g' => new Size(100.0, null, true, $oColor->getLineNo()), - 'b' => new Size(230.0, null, true, $oColor->getLineNo()), - ], $oColor->getColor()); - $oColor = $aColorRule[1]->getValue(); + 'r' => new Size(10.0, null, true, $colorRuleValue->getLineNumber()), + 'g' => new Size(100.0, null, true, $colorRuleValue->getLineNumber()), + 'b' => new Size(230.0, null, true, $colorRuleValue->getLineNumber()), + ], $colorRuleValue->getColor()); + $colorRuleValue = $colorRules[1]->getValue(); + self::assertInstanceOf(Color::class, $colorRuleValue); self::assertEquals([ - 'r' => new Size(10.0, null, true, $oColor->getLineNo()), - 'g' => new Size(100.0, null, true, $oColor->getLineNo()), - 'b' => new Size(231.0, null, true, $oColor->getLineNo()), - 'a' => new Size('0000.3', null, true, $oColor->getLineNo()), - ], $oColor->getColor()); - $aColorRule = $oRuleSet->getRules('outline-color'); - $oColor = $aColorRule[0]->getValue(); + 'r' => new Size(10.0, null, true, $colorRuleValue->getLineNumber()), + 'g' => new Size(100.0, null, true, $colorRuleValue->getLineNumber()), + 'b' => new Size(231.0, null, true, $colorRuleValue->getLineNumber()), + 'a' => new Size('0000.3', null, true, $colorRuleValue->getLineNumber()), + ], $colorRuleValue->getColor()); + $colorRules = $declarationBlock->getRules('outline-color'); + $colorRuleValue = $colorRules[0]->getValue(); + self::assertInstanceOf(Color::class, $colorRuleValue); self::assertEquals([ - 'r' => new Size(34.0, null, true, $oColor->getLineNo()), - 'g' => new Size(34.0, null, true, $oColor->getLineNo()), - 'b' => new Size(34.0, null, true, $oColor->getLineNo()), - ], $oColor->getColor()); - } elseif ($sSelector === '#yours') { - $aColorRule = $oRuleSet->getRules('background-color'); - $oColor = $aColorRule[0]->getValue(); + 'r' => new Size(34.0, null, true, $colorRuleValue->getLineNumber()), + 'g' => new Size(34.0, null, true, $colorRuleValue->getLineNumber()), + 'b' => new Size(34.0, null, true, $colorRuleValue->getLineNumber()), + ], $colorRuleValue->getColor()); + } elseif ($selector === '#yours') { + $colorRules = $declarationBlock->getRules('background-color'); + $colorRuleValue = $colorRules[0]->getValue(); + self::assertInstanceOf(Color::class, $colorRuleValue); self::assertEquals([ - 'h' => new Size(220.0, null, true, $oColor->getLineNo()), - 's' => new Size(10.0, '%', true, $oColor->getLineNo()), - 'l' => new Size(220.0, '%', true, $oColor->getLineNo()), - ], $oColor->getColor()); - $oColor = $aColorRule[1]->getValue(); + 'h' => new Size(220.0, null, true, $colorRuleValue->getLineNumber()), + 's' => new Size(10.0, '%', true, $colorRuleValue->getLineNumber()), + 'l' => new Size(220.0, '%', true, $colorRuleValue->getLineNumber()), + ], $colorRuleValue->getColor()); + $colorRuleValue = $colorRules[1]->getValue(); + self::assertInstanceOf(Color::class, $colorRuleValue); self::assertEquals([ - 'h' => new Size(220.0, null, true, $oColor->getLineNo()), - 's' => new Size(10.0, '%', true, $oColor->getLineNo()), - 'l' => new Size(220.0, '%', true, $oColor->getLineNo()), - 'a' => new Size(0000.3, null, true, $oColor->getLineNo()), - ], $oColor->getColor()); - $aColorRule = $oRuleSet->getRules('outline-color'); - self::assertEmpty($aColorRule); + 'h' => new Size(220.0, null, true, $colorRuleValue->getLineNumber()), + 's' => new Size(10.0, '%', true, $colorRuleValue->getLineNumber()), + 'l' => new Size(220.0, '%', true, $colorRuleValue->getLineNumber()), + 'a' => new Size(0000.3, null, true, $colorRuleValue->getLineNumber()), + ], $colorRuleValue->getColor()); + $colorRules = $declarationBlock->getRules('outline-color'); + self::assertEmpty($colorRules); } } - foreach ($oDoc->getAllValues('color') as $sColor) { - self::assertSame('red', $sColor); + foreach ($document->getAllValues(null, 'color') as $colorValue) { + self::assertSame('red', $colorValue); } self::assertSame( '#mine {color: red;border-color: #0a64e6;border-color: rgba(10,100,231,.3);outline-color: #222;' @@ -165,7 +166,7 @@ public function colorParsing(): void . "\n" . '#variables-alpha {background-color: rgba(var(--some-rgb),.1);' . 'background-color: rgba(var(--some-rg),255,.1);background-color: hsla(var(--some-hsl),.1);}', - $oDoc->render() + $document->render() ); } @@ -174,47 +175,47 @@ public function colorParsing(): void */ public function unicodeParsing(): void { - $oDoc = self::parsedStructureForFile('unicode'); - foreach ($oDoc->getAllDeclarationBlocks() as $oRuleSet) { - $sSelector = $oRuleSet->getSelectors(); - $sSelector = $sSelector[0]->getSelector(); - if (\substr($sSelector, 0, \strlen('.test-')) !== '.test-') { + $document = self::parsedStructureForFile('unicode'); + foreach ($document->getAllDeclarationBlocks() as $declarationBlock) { + $selectors = $declarationBlock->getSelectors(); + $selector = $selectors[0]->getSelector(); + if (\substr($selector, 0, \strlen('.test-')) !== '.test-') { continue; } - $aContentRules = $oRuleSet->getRules('content'); - $sString = $aContentRules[0]->getValue()->__toString(); - if ($sSelector == '.test-1') { - self::assertSame('" "', $sString); + $contentRules = $declarationBlock->getRules('content'); + $firstContentRuleAsString = $contentRules[0]->getValue()->render(OutputFormat::create()); + if ($selector === '.test-1') { + self::assertSame('" "', $firstContentRuleAsString); } - if ($sSelector == '.test-2') { - self::assertSame('"é"', $sString); + if ($selector === '.test-2') { + self::assertSame('"é"', $firstContentRuleAsString); } - if ($sSelector == '.test-3') { - self::assertSame('" "', $sString); + if ($selector === '.test-3') { + self::assertSame('" "', $firstContentRuleAsString); } - if ($sSelector == '.test-4') { - self::assertSame('"𝄞"', $sString); + if ($selector === '.test-4') { + self::assertSame('"𝄞"', $firstContentRuleAsString); } - if ($sSelector == '.test-5') { - self::assertSame('"水"', $sString); + if ($selector === '.test-5') { + self::assertSame('"水"', $firstContentRuleAsString); } - if ($sSelector == '.test-6') { - self::assertSame('"¥"', $sString); + if ($selector === '.test-6') { + self::assertSame('"¥"', $firstContentRuleAsString); } - if ($sSelector == '.test-7') { - self::assertSame('"\\A"', $sString); + if ($selector === '.test-7') { + self::assertSame('"\\A"', $firstContentRuleAsString); } - if ($sSelector == '.test-8') { - self::assertSame('"\\"\\""', $sString); + if ($selector === '.test-8') { + self::assertSame('"\\"\\""', $firstContentRuleAsString); } - if ($sSelector == '.test-9') { - self::assertSame('"\\"\\\'"', $sString); + if ($selector === '.test-9') { + self::assertSame('"\\"\\\'"', $firstContentRuleAsString); } - if ($sSelector == '.test-10') { - self::assertSame('"\\\'\\\\"', $sString); + if ($selector === '.test-10') { + self::assertSame('"\\\'\\\\"', $firstContentRuleAsString); } - if ($sSelector == '.test-11') { - self::assertSame('"test"', $sString); + if ($selector === '.test-11') { + self::assertSame('"test"', $firstContentRuleAsString); } } } @@ -224,9 +225,9 @@ public function unicodeParsing(): void */ public function unicodeRangeParsing(): void { - $oDoc = self::parsedStructureForFile('unicode-range'); - $sExpected = '@font-face {unicode-range: U+0100-024F,U+0259,U+1E??-2EFF,U+202F;}'; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile('unicode-range'); + $expected = '@font-face {unicode-range: U+0100-024F,U+0259,U+1E??-2EFF,U+202F;}'; + self::assertSame($expected, $document->render()); } /** @@ -234,51 +235,27 @@ public function unicodeRangeParsing(): void */ public function specificity(): void { - $oDoc = self::parsedStructureForFile('specificity'); - $oDeclarationBlock = $oDoc->getAllDeclarationBlocks(); - $oDeclarationBlock = $oDeclarationBlock[0]; - $aSelectors = $oDeclarationBlock->getSelectors(); - foreach ($aSelectors as $oSelector) { - switch ($oSelector->getSelector()) { - case '#test .help': - self::assertSame(110, $oSelector->getSpecificity()); - break; - case '#file': - self::assertSame(100, $oSelector->getSpecificity()); - break; - case '.help:hover': - self::assertSame(20, $oSelector->getSpecificity()); - break; - case 'ol li::before': - self::assertSame(3, $oSelector->getSpecificity()); - break; - case 'li.green': - self::assertSame(11, $oSelector->getSpecificity()); - break; - default: - self::fail('specificity: untested selector ' . $oSelector->getSelector()); - } - } - self::assertEquals([new Selector('#test .help', true)], $oDoc->getSelectorsBySpecificity('> 100')); + $document = self::parsedStructureForFile('specificity'); + self::assertEquals([new Selector('#test .help')], $document->getSelectorsBySpecificity('> 100')); self::assertEquals( - [new Selector('#test .help', true), new Selector('#file', true)], - $oDoc->getSelectorsBySpecificity('>= 100') + [new Selector('#test .help'), new Selector('#file')], + $document->getSelectorsBySpecificity('>= 100') ); - self::assertEquals([new Selector('#file', true)], $oDoc->getSelectorsBySpecificity('=== 100')); - self::assertEquals([new Selector('#file', true)], $oDoc->getSelectorsBySpecificity('== 100')); + self::assertEquals([new Selector('#file')], $document->getSelectorsBySpecificity('=== 100')); + self::assertEquals([new Selector('#file')], $document->getSelectorsBySpecificity('== 100')); self::assertEquals([ - new Selector('#file', true), - new Selector('.help:hover', true), - new Selector('li.green', true), - new Selector('ol li::before', true), - ], $oDoc->getSelectorsBySpecificity('<= 100')); + new Selector('#file'), + new Selector('.help:hover'), + new Selector('li.green'), + new Selector('ol li::before'), + ], $document->getSelectorsBySpecificity('<= 100')); self::assertEquals([ - new Selector('.help:hover', true), - new Selector('li.green', true), - new Selector('ol li::before', true), - ], $oDoc->getSelectorsBySpecificity('< 100')); - self::assertEquals([new Selector('li.green', true)], $oDoc->getSelectorsBySpecificity('11')); - self::assertEquals([new Selector('ol li::before', true)], $oDoc->getSelectorsBySpecificity(3)); + new Selector('.help:hover'), + new Selector('li.green'), + new Selector('ol li::before'), + ], $document->getSelectorsBySpecificity('< 100')); + self::assertEquals([new Selector('li.green')], $document->getSelectorsBySpecificity('11')); + self::assertEquals([new Selector('ol li::before')], $document->getSelectorsBySpecificity('3')); } /** @@ -286,7 +263,7 @@ public function specificity(): void */ public function manipulation(): void { - $oDoc = self::parsedStructureForFile('atrules'); + $document = self::parsedStructureForFile('atrules'); self::assertSame( '@charset "utf-8";' . "\n" @@ -318,12 +295,12 @@ public function manipulation(): void . '@media screen and (orientation: landscape) {@-ms-viewport {width: 1024px;height: 768px;}}' . "\n" . '@region-style #intro {p {color: blue;}}', - $oDoc->render() + $document->render() ); - foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) { - foreach ($oBlock->getSelectors() as $oSelector) { + foreach ($document->getAllDeclarationBlocks() as $declarationBlock) { + foreach ($declarationBlock->getSelectors() as $selector) { //Loop over all selector parts (the comma-separated strings in a selector) and prepend the id - $oSelector->setSelector('#my_id ' . $oSelector->getSelector()); + $selector->setSelector('#my_id ' . $selector->getSelector()); } } self::assertSame( @@ -357,33 +334,33 @@ public function manipulation(): void . '@media screen and (orientation: landscape) {@-ms-viewport {width: 1024px;height: 768px;}}' . "\n" . '@region-style #intro {#my_id p {color: blue;}}', - $oDoc->render(OutputFormat::create()->setRenderComments(false)) + $document->render(OutputFormat::create()->setRenderComments(false)) ); - $oDoc = self::parsedStructureForFile('values'); + $document = self::parsedStructureForFile('values'); self::assertSame( '#header {margin: 10px 2em 1cm 2%;font-family: Verdana,Helvetica,"Gill Sans",sans-serif;' . 'font-size: 10px;color: red !important;background-color: green;' . 'background-color: rgba(0,128,0,.7);frequency: 30Hz;transform: rotate(1turn);} body {color: green;font: 75% "Lucida Grande","Trebuchet MS",Verdana,sans-serif;}', - $oDoc->render() + $document->render() ); - foreach ($oDoc->getAllRuleSets() as $oRuleSet) { - $oRuleSet->removeRule('font-'); + foreach ($document->getAllRuleSets() as $ruleSet) { + $ruleSet->removeMatchingRules('font-'); } self::assertSame( '#header {margin: 10px 2em 1cm 2%;color: red !important;background-color: green;' . 'background-color: rgba(0,128,0,.7);frequency: 30Hz;transform: rotate(1turn);} body {color: green;}', - $oDoc->render() + $document->render() ); - foreach ($oDoc->getAllRuleSets() as $oRuleSet) { - $oRuleSet->removeRule('background-'); + foreach ($document->getAllRuleSets() as $ruleSet) { + $ruleSet->removeMatchingRules('background-'); } self::assertSame( '#header {margin: 10px 2em 1cm 2%;color: red !important;frequency: 30Hz;transform: rotate(1turn);} body {color: green;}', - $oDoc->render() + $document->render() ); } @@ -392,22 +369,22 @@ public function manipulation(): void */ public function ruleGetters(): void { - $oDoc = self::parsedStructureForFile('values'); - $aBlocks = $oDoc->getAllDeclarationBlocks(); - $oHeaderBlock = $aBlocks[0]; - $oBodyBlock = $aBlocks[1]; - $aHeaderRules = $oHeaderBlock->getRules('background-'); - self::assertCount(2, $aHeaderRules); - self::assertSame('background-color', $aHeaderRules[0]->getRule()); - self::assertSame('background-color', $aHeaderRules[1]->getRule()); - $aHeaderRules = $oHeaderBlock->getRulesAssoc('background-'); - self::assertCount(1, $aHeaderRules); - self::assertTrue($aHeaderRules['background-color']->getValue() instanceof Color); - self::assertSame('rgba', $aHeaderRules['background-color']->getValue()->getColorDescription()); - $oHeaderBlock->removeRule($aHeaderRules['background-color']); - $aHeaderRules = $oHeaderBlock->getRules('background-'); - self::assertCount(1, $aHeaderRules); - self::assertSame('green', $aHeaderRules[0]->getValue()); + $document = self::parsedStructureForFile('values'); + $declarationBlocks = $document->getAllDeclarationBlocks(); + $headerBlock = $declarationBlocks[0]; + $bodyBlock = $declarationBlocks[1]; + $backgroundHeaderRules = $headerBlock->getRules('background-'); + self::assertCount(2, $backgroundHeaderRules); + self::assertSame('background-color', $backgroundHeaderRules[0]->getRule()); + self::assertSame('background-color', $backgroundHeaderRules[1]->getRule()); + $backgroundHeaderRules = $headerBlock->getRulesAssoc('background-'); + self::assertCount(1, $backgroundHeaderRules); + self::assertInstanceOf(Color::class, $backgroundHeaderRules['background-color']->getValue()); + self::assertSame('rgba', $backgroundHeaderRules['background-color']->getValue()->getColorDescription()); + $headerBlock->removeRule($backgroundHeaderRules['background-color']); + $backgroundHeaderRules = $headerBlock->getRules('background-'); + self::assertCount(1, $backgroundHeaderRules); + self::assertSame('green', $backgroundHeaderRules[0]->getValue()); } /** @@ -415,39 +392,43 @@ public function ruleGetters(): void */ public function slashedValues(): void { - $oDoc = self::parsedStructureForFile('slashed'); + $document = self::parsedStructureForFile('slashed'); self::assertSame( '.test {font: 12px/1.5 Verdana,Arial,sans-serif;border-radius: 5px 10px 5px 10px/10px 5px 10px 5px;}', - $oDoc->render() + $document->render() ); - foreach ($oDoc->getAllValues(null) as $mValue) { - if ($mValue instanceof Size && $mValue->isSize() && !$mValue->isRelative()) { - $mValue->setSize($mValue->getSize() * 3); + foreach ($document->getAllValues(null) as $value) { + if ($value instanceof Size && $value->isSize() && !$value->isRelative()) { + $value->setSize($value->getSize() * 3); } } - foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) { - $oRule = $oBlock->getRules('font'); - $oRule = $oRule[0]; - $oSpaceList = $oRule->getValue(); - self::assertSame(' ', $oSpaceList->getListSeparator()); - $oSlashList = $oSpaceList->getListComponents(); - $oCommaList = $oSlashList[1]; - $oSlashList = $oSlashList[0]; - self::assertSame(',', $oCommaList->getListSeparator()); - self::assertSame('/', $oSlashList->getListSeparator()); - $oRule = $oBlock->getRules('border-radius'); - $oRule = $oRule[0]; - $oSlashList = $oRule->getValue(); - self::assertSame('/', $oSlashList->getListSeparator()); - $oSpaceList1 = $oSlashList->getListComponents(); - $oSpaceList2 = $oSpaceList1[1]; - $oSpaceList1 = $oSpaceList1[0]; - self::assertSame(' ', $oSpaceList1->getListSeparator()); - self::assertSame(' ', $oSpaceList2->getListSeparator()); + foreach ($document->getAllDeclarationBlocks() as $declarationBlock) { + $fontRules = $declarationBlock->getRules('font'); + $fontRule = $fontRules[0]; + $fontRuleValue = $fontRule->getValue(); + self::assertSame(' ', $fontRuleValue->getListSeparator()); + $fontRuleValueComponents = $fontRuleValue->getListComponents(); + $commaList = $fontRuleValueComponents[1]; + self::assertInstanceOf(ValueList::class, $commaList); + $slashList = $fontRuleValueComponents[0]; + self::assertInstanceOf(ValueList::class, $slashList); + self::assertSame(',', $commaList->getListSeparator()); + self::assertSame('/', $slashList->getListSeparator()); + $borderRadiusRules = $declarationBlock->getRules('border-radius'); + $borderRadiusRule = $borderRadiusRules[0]; + $slashList = $borderRadiusRule->getValue(); + self::assertSame('/', $slashList->getListSeparator()); + $slashListComponents = $slashList->getListComponents(); + $secondSlashListComponent = $slashListComponents[1]; + self::assertInstanceOf(ValueList::class, $secondSlashListComponent); + $firstSlashListComponent = $slashListComponents[0]; + self::assertInstanceOf(ValueList::class, $firstSlashListComponent); + self::assertSame(' ', $firstSlashListComponent->getListSeparator()); + self::assertSame(' ', $secondSlashListComponent->getListSeparator()); } self::assertSame( '.test {font: 36px/1.5 Verdana,Arial,sans-serif;border-radius: 15px 30px 15px 30px/30px 15px 30px 15px;}', - $oDoc->render() + $document->render() ); } @@ -456,8 +437,8 @@ public function slashedValues(): void */ public function functionSyntax(): void { - $oDoc = self::parsedStructureForFile('functions'); - $sExpected = 'div.main {background-image: linear-gradient(#000,#fff);}' + $document = self::parsedStructureForFile('functions'); + $expected = 'div.main {background-image: linear-gradient(#000,#fff);}' . "\n" . '.collapser::before, .collapser::-moz-before, .collapser::-webkit-before {content: "»";font-size: 1.2em;' . 'margin-right: .2em;-moz-transition-property: -moz-transform;-moz-transition-duration: .2s;' @@ -470,64 +451,23 @@ public function functionSyntax(): void . '-moz-transition-duration: .3s;}' . "\n" . '.collapser.expanded + * {height: auto;}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); - foreach ($oDoc->getAllValues(null, true) as $mValue) { - if ($mValue instanceof Size && $mValue->isSize()) { - $mValue->setSize($mValue->getSize() * 3); + foreach ($document->getAllValues(null, null, true) as $value) { + if ($value instanceof Size && $value->isSize()) { + $value->setSize($value->getSize() * 3); } } - $sExpected = \str_replace(['1.2em', '.2em', '60%'], ['3.6em', '.6em', '180%'], $sExpected); - self::assertSame($sExpected, $oDoc->render()); + $expected = \str_replace(['1.2em', '.2em', '60%'], ['3.6em', '.6em', '180%'], $expected); + self::assertSame($expected, $document->render()); - foreach ($oDoc->getAllValues(null, true) as $mValue) { - if ($mValue instanceof Size && !$mValue->isRelative() && !$mValue->isColorComponent()) { - $mValue->setSize($mValue->getSize() * 2); + foreach ($document->getAllValues(null, null, true) as $value) { + if ($value instanceof Size && !$value->isRelative() && !$value->isColorComponent()) { + $value->setSize($value->getSize() * 2); } } - $sExpected = \str_replace(['.2s', '.3s', '90deg'], ['.4s', '.6s', '180deg'], $sExpected); - self::assertSame($sExpected, $oDoc->render()); - } - - /** - * @test - */ - public function expandShorthands(): void - { - $oDoc = self::parsedStructureForFile('expand-shorthands'); - $sExpected = 'body {font: italic 500 14px/1.618 "Trebuchet MS",Georgia,serif;border: 2px solid #f0f;' - . 'background: #ccc url("/images/foo.png") no-repeat left top;margin: 1em !important;' - . 'padding: 2px 6px 3px;}'; - self::assertSame($sExpected, $oDoc->render()); - $oDoc->expandShorthands(); - $sExpected = 'body {margin-top: 1em !important;margin-right: 1em !important;margin-bottom: 1em !important;' - . 'margin-left: 1em !important;padding-top: 2px;padding-right: 6px;padding-bottom: 3px;' - . 'padding-left: 6px;border-top-color: #f0f;border-right-color: #f0f;border-bottom-color: #f0f;' - . 'border-left-color: #f0f;border-top-style: solid;border-right-style: solid;' - . 'border-bottom-style: solid;border-left-style: solid;border-top-width: 2px;' - . 'border-right-width: 2px;border-bottom-width: 2px;border-left-width: 2px;font-style: italic;' - . 'font-variant: normal;font-weight: 500;font-size: 14px;line-height: 1.618;' - . 'font-family: "Trebuchet MS",Georgia,serif;background-color: #ccc;' - . 'background-image: url("/images/foo.png");background-repeat: no-repeat;background-attachment: scroll;' - . 'background-position: left top;}'; - self::assertSame($sExpected, $oDoc->render()); - } - - /** - * @test - */ - public function createShorthands(): void - { - $oDoc = self::parsedStructureForFile('create-shorthands'); - $sExpected = 'body {font-size: 2em;font-family: Helvetica,Arial,sans-serif;font-weight: bold;' - . 'border-width: 2px;border-color: #999;border-style: dotted;background-color: #fff;' - . 'background-image: url("foobar.png");background-repeat: repeat-y;margin-top: 2px;margin-right: 3px;' - . 'margin-bottom: 4px;margin-left: 5px;}'; - self::assertSame($sExpected, $oDoc->render()); - $oDoc->createShorthands(); - $sExpected = 'body {background: #fff url("foobar.png") repeat-y;margin: 2px 5px 4px 3px;' - . 'border: 2px dotted #999;font: bold 2em Helvetica,Arial,sans-serif;}'; - self::assertSame($sExpected, $oDoc->render()); + $expected = \str_replace(['.2s', '.3s', '90deg'], ['.4s', '.6s', '180deg'], $expected); + self::assertSame($expected, $document->render()); } /** @@ -535,14 +475,14 @@ public function createShorthands(): void */ public function namespaces(): void { - $oDoc = self::parsedStructureForFile('namespaces'); - $sExpected = '@namespace toto "http://toto.example.org"; + $document = self::parsedStructureForFile('namespaces'); + $expected = '@namespace toto "http://toto.example.org"; @namespace "http://example.com/foo"; @namespace foo url("http://www.example.com/"); @namespace foo url("http://www.example.com/"); foo|test {gaga: 1;} |test {gaga: 2;}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); } /** @@ -550,9 +490,9 @@ public function namespaces(): void */ public function innerColors(): void { - $oDoc = self::parsedStructureForFile('inner-color'); - $sExpected = 'test {background: -webkit-gradient(linear,0 0,0 bottom,from(#006cad),to(hsl(202,100%,49%)));}'; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile('inner-color'); + $expected = 'test {background: -webkit-gradient(linear,0 0,0 bottom,from(#006cad),to(hsl(202,100%,49%)));}'; + self::assertSame($expected, $document->render()); } /** @@ -560,9 +500,9 @@ public function innerColors(): void */ public function prefixedGradient(): void { - $oDoc = self::parsedStructureForFile('webkit'); - $sExpected = '.test {background: -webkit-linear-gradient(top right,white,black);}'; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile('webkit'); + $expected = '.test {background: -webkit-linear-gradient(top right,white,black);}'; + self::assertSame($expected, $document->render()); } /** @@ -570,36 +510,36 @@ public function prefixedGradient(): void */ public function listValueRemoval(): void { - $oDoc = self::parsedStructureForFile('atrules'); - foreach ($oDoc->getContents() as $oItem) { - if ($oItem instanceof AtRule) { - $oDoc->remove($oItem); + $document = self::parsedStructureForFile('atrules'); + foreach ($document->getContents() as $contentItem) { + if ($contentItem instanceof AtRule) { + $document->remove($contentItem); continue; } } - self::assertSame('html, body {font-size: -.6em;}', $oDoc->render()); + self::assertSame('html, body {font-size: -.6em;}', $document->render()); - $oDoc = self::parsedStructureForFile('nested'); - foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) { - $oDoc->removeDeclarationBlockBySelector($oBlock, false); + $document = self::parsedStructureForFile('nested'); + foreach ($document->getAllDeclarationBlocks() as $declarationBlock) { + $document->removeDeclarationBlockBySelector($declarationBlock, false); break; } self::assertSame( 'html {some-other: -test(val1);} @media screen {html {some: -test(val2);}} #unrelated {other: yes;}', - $oDoc->render() + $document->render() ); - $oDoc = self::parsedStructureForFile('nested'); - foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) { - $oDoc->removeDeclarationBlockBySelector($oBlock, true); + $document = self::parsedStructureForFile('nested'); + foreach ($document->getAllDeclarationBlocks() as $declarationBlock) { + $document->removeDeclarationBlockBySelector($declarationBlock, true); break; } self::assertSame( '@media screen {html {some: -test(val2);}} #unrelated {other: yes;}', - $oDoc->render() + $document->render() ); } @@ -610,18 +550,18 @@ public function selectorRemoval(): void { $this->expectException(OutputException::class); - $oDoc = self::parsedStructureForFile('1readme'); - $aBlocks = $oDoc->getAllDeclarationBlocks(); - $oBlock1 = $aBlocks[0]; - self::assertTrue($oBlock1->removeSelector('html')); - $sExpected = '@charset "utf-8"; + $document = self::parsedStructureForFile('1readme'); + $declarationsBlocks = $document->getAllDeclarationBlocks(); + $declarationBlock = $declarationsBlocks[0]; + self::assertTrue($declarationBlock->removeSelector('html')); + $expected = '@charset "utf-8"; @font-face {font-family: "CrassRoots";src: url("../media/cr.ttf");} body {font-size: 1.6em;}'; - self::assertSame($sExpected, $oDoc->render()); - self::assertFalse($oBlock1->removeSelector('html')); - self::assertTrue($oBlock1->removeSelector('body')); + self::assertSame($expected, $document->render()); + self::assertFalse($declarationBlock->removeSelector('html')); + self::assertTrue($declarationBlock->removeSelector('body')); // This tries to output a declaration block without a selector and throws. - $oDoc->render(); + $document->render(); } /** @@ -629,13 +569,13 @@ public function selectorRemoval(): void */ public function comments(): void { - $oDoc = self::parsedStructureForFile('comments'); - $sExpected = <<render()); + self::assertSame($expected, $document->render()); } /** @@ -643,10 +583,10 @@ public function comments(): void */ public function urlInFile(): void { - $oDoc = self::parsedStructureForFile('url', Settings::create()->withMultibyteSupport(true)); - $sExpected = 'body {background: #fff url("https://somesite.com/images/someimage.gif") repeat top center;} + $document = self::parsedStructureForFile('url', Settings::create()->withMultibyteSupport(true)); + $expected = 'body {background: #fff url("https://somesite.com/images/someimage.gif") repeat top center;} body {background-url: url("https://somesite.com/images/someimage.gif");}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); } /** @@ -654,10 +594,10 @@ public function urlInFile(): void */ public function hexAlphaInFile(): void { - $oDoc = self::parsedStructureForFile('hex-alpha', Settings::create()->withMultibyteSupport(true)); - $sExpected = 'div {background: rgba(17,34,51,.27);} + $document = self::parsedStructureForFile('hex-alpha', Settings::create()->withMultibyteSupport(true)); + $expected = 'div {background: rgba(17,34,51,.27);} div {background: rgba(17,34,51,.27);}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); } /** @@ -665,12 +605,12 @@ public function hexAlphaInFile(): void */ public function calcInFile(): void { - $oDoc = self::parsedStructureForFile('calc', Settings::create()->withMultibyteSupport(true)); - $sExpected = 'div {width: calc(100% / 4);} + $document = self::parsedStructureForFile('calc', Settings::create()->withMultibyteSupport(true)); + $expected = 'div {width: calc(100% / 4);} div {margin-top: calc(-120% - 4px);} -div {height: -webkit-calc(9 / 16 * 100%) !important;width: -moz-calc(( 50px - 50% ) * 2);} +div {height: calc(9 / 16 * 100%) !important;width: calc(( 50px - 50% ) * 2);} div {width: calc(50% - ( ( 4% ) * .5 ));}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); } /** @@ -678,9 +618,9 @@ public function calcInFile(): void */ public function calcNestedInFile(): void { - $oDoc = self::parsedStructureForFile('calc-nested', Settings::create()->withMultibyteSupport(true)); - $sExpected = '.test {font-size: calc(( 3 * 4px ) + -2px);top: calc(200px - calc(20 * 3px));}'; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile('calc-nested', Settings::create()->withMultibyteSupport(true)); + $expected = '.test {font-size: calc(( 3 * 4px ) + -2px);top: calc(200px - calc(20 * 3px));}'; + self::assertSame($expected, $document->render()); } /** @@ -688,13 +628,13 @@ public function calcNestedInFile(): void */ public function invalidCalcInFile(): void { - $oDoc = self::parsedStructureForFile('calc-invalid', Settings::create()->withMultibyteSupport(true)); - $sExpected = 'div {} + $document = self::parsedStructureForFile('calc-invalid', Settings::create()->withMultibyteSupport(true)); + $expected = 'div {} div {} div {} div {height: -moz-calc;} div {height: calc;}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); } /** @@ -703,28 +643,28 @@ public function invalidCalcInFile(): void public function invalidCalc(): void { $parser = new Parser('div { height: calc(100px'); - $oDoc = $parser->parse(); - self::assertSame('div {height: calc(100px);}', $oDoc->render()); + $document = $parser->parse(); + self::assertSame('div {height: calc(100px);}', $document->render()); $parser = new Parser('div { height: calc(100px)'); - $oDoc = $parser->parse(); - self::assertSame('div {height: calc(100px);}', $oDoc->render()); + $document = $parser->parse(); + self::assertSame('div {height: calc(100px);}', $document->render()); $parser = new Parser('div { height: calc(100px);'); - $oDoc = $parser->parse(); - self::assertSame('div {height: calc(100px);}', $oDoc->render()); + $document = $parser->parse(); + self::assertSame('div {height: calc(100px);}', $document->render()); $parser = new Parser('div { height: calc(100px}'); - $oDoc = $parser->parse(); - self::assertSame('div {}', $oDoc->render()); + $document = $parser->parse(); + self::assertSame('div {}', $document->render()); $parser = new Parser('div { height: calc(100px;'); - $oDoc = $parser->parse(); - self::assertSame('div {}', $oDoc->render()); + $document = $parser->parse(); + self::assertSame('div {}', $document->render()); $parser = new Parser('div { height: calc(100px;}'); - $oDoc = $parser->parse(); - self::assertSame('div {}', $oDoc->render()); + $document = $parser->parse(); + self::assertSame('div {}', $document->render()); } /** @@ -732,10 +672,10 @@ public function invalidCalc(): void */ public function gridLineNameInFile(): void { - $oDoc = self::parsedStructureForFile('grid-linename', Settings::create()->withMultibyteSupport(true)); - $sExpected = "div {grid-template-columns: [linename] 100px;}\n" + $document = self::parsedStructureForFile('grid-linename', Settings::create()->withMultibyteSupport(true)); + $expected = "div {grid-template-columns: [linename] 100px;}\n" . 'span {grid-template-columns: [linename1 linename2] 100px;}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); } /** @@ -743,9 +683,9 @@ public function gridLineNameInFile(): void */ public function emptyGridLineNameLenientInFile(): void { - $oDoc = self::parsedStructureForFile('empty-grid-linename'); - $sExpected = '.test {grid-template-columns: [] 100px;}'; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile('empty-grid-linename'); + $expected = '.test {grid-template-columns: [] 100px;}'; + self::assertSame($expected, $document->render()); } /** @@ -753,9 +693,12 @@ public function emptyGridLineNameLenientInFile(): void */ public function invalidGridLineNameInFile(): void { - $oDoc = self::parsedStructureForFile('invalid-grid-linename', Settings::create()->withMultibyteSupport(true)); - $sExpected = 'div {}'; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile( + 'invalid-grid-linename', + Settings::create()->withMultibyteSupport(true) + ); + $expected = 'div {}'; + self::assertSame($expected, $document->render()); } /** @@ -763,9 +706,9 @@ public function invalidGridLineNameInFile(): void */ public function unmatchedBracesInFile(): void { - $oDoc = self::parsedStructureForFile('unmatched_braces', Settings::create()->withMultibyteSupport(true)); - $sExpected = 'button, input, checkbox, textarea {outline: 0;margin: 0;}'; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile('unmatched_braces', Settings::create()->withMultibyteSupport(true)); + $expected = 'button, input, checkbox, textarea {outline: 0;margin: 0;}'; + self::assertSame($expected, $document->render()); } /** @@ -773,20 +716,20 @@ public function unmatchedBracesInFile(): void */ public function invalidSelectorsInFile(): void { - $oDoc = self::parsedStructureForFile('invalid-selectors', Settings::create()->withMultibyteSupport(true)); - $sExpected = '@keyframes mymove {from {top: 0px;}} + $document = self::parsedStructureForFile('invalid-selectors', Settings::create()->withMultibyteSupport(true)); + $expected = '@keyframes mymove {from {top: 0px;}} #test {color: white;background: green;} #test {display: block;background: white;color: black;}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); - $oDoc = self::parsedStructureForFile('invalid-selectors-2', Settings::create()->withMultibyteSupport(true)); - $sExpected = '@media only screen and (max-width: 1215px) {.breadcrumb {padding-left: 10px;} + $document = self::parsedStructureForFile('invalid-selectors-2', Settings::create()->withMultibyteSupport(true)); + $expected = '@media only screen and (max-width: 1215px) {.breadcrumb {padding-left: 10px;} .super-menu > li:first-of-type {border-left-width: 0;} .super-menu > li:last-of-type {border-right-width: 0;} html[dir="rtl"] .super-menu > li:first-of-type {border-left-width: 1px;border-right-width: 0;} html[dir="rtl"] .super-menu > li:last-of-type {border-left-width: 0;}} body {background-color: red;}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); } /** @@ -794,19 +737,19 @@ public function invalidSelectorsInFile(): void */ public function selectorEscapesInFile(): void { - $oDoc = self::parsedStructureForFile('selector-escapes', Settings::create()->withMultibyteSupport(true)); - $sExpected = '#\\# {color: red;} + $document = self::parsedStructureForFile('selector-escapes', Settings::create()->withMultibyteSupport(true)); + $expected = '#\\# {color: red;} .col-sm-1\\/5 {width: 20%;}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); - $oDoc = self::parsedStructureForFile('invalid-selectors-2', Settings::create()->withMultibyteSupport(true)); - $sExpected = '@media only screen and (max-width: 1215px) {.breadcrumb {padding-left: 10px;} + $document = self::parsedStructureForFile('invalid-selectors-2', Settings::create()->withMultibyteSupport(true)); + $expected = '@media only screen and (max-width: 1215px) {.breadcrumb {padding-left: 10px;} .super-menu > li:first-of-type {border-left-width: 0;} .super-menu > li:last-of-type {border-right-width: 0;} html[dir="rtl"] .super-menu > li:first-of-type {border-left-width: 1px;border-right-width: 0;} html[dir="rtl"] .super-menu > li:last-of-type {border-left-width: 0;}} body {background-color: red;}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); } /** @@ -814,10 +757,10 @@ public function selectorEscapesInFile(): void */ public function identifierEscapesInFile(): void { - $oDoc = self::parsedStructureForFile('identifier-escapes', Settings::create()->withMultibyteSupport(true)); - $sExpected = 'div {font: 14px Font Awesome\\ 5 Pro;font: 14px Font Awesome\\} 5 Pro;' + $document = self::parsedStructureForFile('identifier-escapes', Settings::create()->withMultibyteSupport(true)); + $expected = 'div {font: 14px Font Awesome\\ 5 Pro;font: 14px Font Awesome\\} 5 Pro;' . 'font: 14px Font Awesome\\; 5 Pro;f\\;ont: 14px Font Awesome\\; 5 Pro;}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); } /** @@ -825,13 +768,13 @@ public function identifierEscapesInFile(): void */ public function selectorIgnoresInFile(): void { - $oDoc = self::parsedStructureForFile('selector-ignores', Settings::create()->withMultibyteSupport(true)); - $sExpected = '.some[selectors-may=\'contain-a-{\'] {}' + $document = self::parsedStructureForFile('selector-ignores', Settings::create()->withMultibyteSupport(true)); + $expected = '.some[selectors-may=\'contain-a-{\'] {}' . "\n" . '.this-selector .valid {width: 100px;}' . "\n" . '@media only screen and (min-width: 200px) {.test {prop: val;}}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); } /** @@ -839,16 +782,16 @@ public function selectorIgnoresInFile(): void */ public function keyframeSelectors(): void { - $oDoc = self::parsedStructureForFile( + $document = self::parsedStructureForFile( 'keyframe-selector-validation', Settings::create()->withMultibyteSupport(true) ); - $sExpected = '@-webkit-keyframes zoom {0% {-webkit-transform: scale(1,1);}' + $expected = '@-webkit-keyframes zoom {0% {-webkit-transform: scale(1,1);}' . "\n\t" . '50% {-webkit-transform: scale(1.2,1.2);}' . "\n\t" . '100% {-webkit-transform: scale(1,1);}}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); } /** @@ -876,11 +819,11 @@ public function calcFailure(): void */ public function urlInFileMbOff(): void { - $oDoc = self::parsedStructureForFile('url', Settings::create()->withMultibyteSupport(false)); - $sExpected = 'body {background: #fff url("https://somesite.com/images/someimage.gif") repeat top center;}' + $document = self::parsedStructureForFile('url', Settings::create()->withMultibyteSupport(false)); + $expected = 'body {background: #fff url("https://somesite.com/images/someimage.gif") repeat top center;}' . "\n" . 'body {background-url: url("https://somesite.com/images/someimage.gif");}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); } /** @@ -888,9 +831,9 @@ public function urlInFileMbOff(): void */ public function emptyFile(): void { - $oDoc = self::parsedStructureForFile('-empty', Settings::create()->withMultibyteSupport(true)); - $sExpected = ''; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile('-empty', Settings::create()->withMultibyteSupport(true)); + $expected = ''; + self::assertSame($expected, $document->render()); } /** @@ -898,9 +841,9 @@ public function emptyFile(): void */ public function emptyFileMbOff(): void { - $oDoc = self::parsedStructureForFile('-empty', Settings::create()->withMultibyteSupport(false)); - $sExpected = ''; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile('-empty', Settings::create()->withMultibyteSupport(false)); + $expected = ''; + self::assertSame($expected, $document->render()); } /** @@ -908,9 +851,9 @@ public function emptyFileMbOff(): void */ public function charsetLenient1(): void { - $oDoc = self::parsedStructureForFile('-charset-after-rule', Settings::create()->withLenientParsing(true)); - $sExpected = '#id {prop: var(--val);}'; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile('-charset-after-rule', Settings::create()->withLenientParsing(true)); + $expected = '#id {prop: var(--val);}'; + self::assertSame($expected, $document->render()); } /** @@ -918,9 +861,9 @@ public function charsetLenient1(): void */ public function charsetLenient2(): void { - $oDoc = self::parsedStructureForFile('-charset-in-block', Settings::create()->withLenientParsing(true)); - $sExpected = '@media print {}'; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile('-charset-in-block', Settings::create()->withLenientParsing(true)); + $expected = '@media print {}'; + self::assertSame($expected, $document->render()); } /** @@ -928,9 +871,9 @@ public function charsetLenient2(): void */ public function trailingWhitespace(): void { - $oDoc = self::parsedStructureForFile('trailing-whitespace', Settings::create()->withLenientParsing(false)); - $sExpected = 'div {width: 200px;}'; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile('trailing-whitespace', Settings::create()->withLenientParsing(false)); + $expected = 'div {width: 200px;}'; + self::assertSame($expected, $document->render()); } /** @@ -966,8 +909,6 @@ public function unopenedClosingBracketFailure(): void /** * Ensure that a missing property value raises an exception. * - * @covers \Sabberworm\CSS\Value\Value::parseValue() - * * @test */ public function missingPropertyValueStrict(): void @@ -980,16 +921,14 @@ public function missingPropertyValueStrict(): void /** * Ensure that a missing property value is ignored when in lenient parsing mode. * - * @covers \Sabberworm\CSS\Value\Value::parseValue() - * * @test */ public function missingPropertyValueLenient(): void { $parsed = self::parsedStructureForFile('missing-property-value', Settings::create()->withLenientParsing(true)); - $rulesets = $parsed->getAllRuleSets(); - self::assertCount(1, $rulesets); - $block = $rulesets[0]; + $declarationBlocks = $parsed->getAllDeclarationBlocks(); + self::assertCount(1, $declarationBlocks); + $block = $declarationBlocks[0]; self::assertInstanceOf(DeclarationBlock::class, $block); self::assertEquals([new Selector('div')], $block->getSelectors()); $rules = $block->getRules(); @@ -1002,14 +941,14 @@ public function missingPropertyValueLenient(): void /** * Parses structure for file. * - * @param string $sFileName - * @param Settings|null $oSettings + * @param string $filename + * @param Settings|null $settings */ - public static function parsedStructureForFile($sFileName, $oSettings = null): Document + public static function parsedStructureForFile($filename, $settings = null): Document { - $sFile = __DIR__ . "/fixtures/$sFileName.css"; - $oParser = new Parser(\file_get_contents($sFile), $oSettings); - return $oParser->parse(); + $filename = __DIR__ . "/fixtures/$filename.css"; + $parser = new Parser(file_get_contents($filename), $settings); + return $parser->parse(); } /** @@ -1019,9 +958,9 @@ public static function parsedStructureForFile($sFileName, $oSettings = null): Do */ public function lineNumbersParsing(): void { - $oDoc = self::parsedStructureForFile('line-numbers'); + $document = self::parsedStructureForFile('line-numbers'); // array key is the expected line number - $aExpected = [ + $expected = [ 1 => [Charset::class], 3 => [CSSNamespace::class], 5 => [AtRuleSet::class], @@ -1032,42 +971,45 @@ public function lineNumbersParsing(): void 25 => [DeclarationBlock::class], ]; - $aActual = []; - foreach ($oDoc->getContents() as $oContent) { - $aActual[$oContent->getLineNo()] = [\get_class($oContent)]; - if ($oContent instanceof KeyFrame) { - foreach ($oContent->getContents() as $block) { - $aActual[$oContent->getLineNo()][] = $block->getLineNo(); + $actual = []; + foreach ($document->getContents() as $contentItem) { + self::assertInstanceOf(Positionable::class, $contentItem); + $actual[$contentItem->getLineNumber()] = [\get_class($contentItem)]; + if ($contentItem instanceof KeyFrame) { + foreach ($contentItem->getContents() as $block) { + self::assertInstanceOf(Positionable::class, $block); + $actual[$contentItem->getLineNumber()][] = $block->getLineNumber(); } } } - $aUrlExpected = [7, 26]; // expected line numbers - $aUrlActual = []; - foreach ($oDoc->getAllValues() as $oValue) { - if ($oValue instanceof URL) { - $aUrlActual[] = $oValue->getLineNo(); + $expectedLineNumbers = [7, 26]; + $actualLineNumbers = []; + foreach ($document->getAllValues() as $value) { + if ($value instanceof URL) { + $actualLineNumbers[] = $value->getLineNumber(); } } // Checking for the multiline color rule lines 27-31 - $aExpectedColorLines = [28, 29, 30]; - $aDeclBlocks = $oDoc->getAllDeclarationBlocks(); + $expectedColorLineNumbers = [28, 29, 30]; + $declarationBlocks = $document->getAllDeclarationBlocks(); // Choose the 2nd one - $oDeclBlock = $aDeclBlocks[1]; - $aRules = $oDeclBlock->getRules(); + $secondDeclarationBlock = $declarationBlocks[1]; + $rules = $secondDeclarationBlock->getRules(); // Choose the 2nd one - $oColor = $aRules[1]->getValue(); - self::assertSame(27, $aRules[1]->getLineNo()); + $valueOfSecondRule = $rules[1]->getValue(); + self::assertInstanceOf(Color::class, $valueOfSecondRule); + self::assertSame(27, $rules[1]->getLineNumber()); - $aActualColorLines = []; - foreach ($oColor->getColor() as $oSize) { - $aActualColorLines[] = $oSize->getLineNo(); + $actualColorLineNumbers = []; + foreach ($valueOfSecondRule->getColor() as $size) { + $actualColorLineNumbers[] = $size->getLineNumber(); } - self::assertSame($aExpectedColorLines, $aActualColorLines); - self::assertSame($aUrlExpected, $aUrlActual); - self::assertSame($aExpected, $aActual); + self::assertSame($expectedColorLineNumbers, $actualColorLineNumbers); + self::assertSame($expectedLineNumbers, $actualLineNumbers); + self::assertSame($expected, $actual); } /** @@ -1077,37 +1019,15 @@ public function unexpectedTokenExceptionLineNo(): void { $this->expectException(UnexpectedTokenException::class); - $oParser = new Parser("\ntest: 1;", Settings::create()->beStrict()); + $parser = new Parser("\ntest: 1;", Settings::create()->beStrict()); try { - $oParser->parse(); + $parser->parse(); } catch (UnexpectedTokenException $e) { - self::assertSame(2, $e->getLineNo()); + self::assertSame(2, $e->getLineNumber()); throw $e; } } - /** - * @test - */ - public function ieHacksStrictParsing(): void - { - $this->expectException(UnexpectedTokenException::class); - - // We can't strictly parse IE hacks. - self::parsedStructureForFile('ie-hacks', Settings::create()->beStrict()); - } - - /** - * @test - */ - public function ieHacksParsing(): void - { - $oDoc = self::parsedStructureForFile('ie-hacks', Settings::create()->withLenientParsing(true)); - $sExpected = 'p {padding-right: .75rem \\9;background-image: none \\9;color: red \\9\\0;' - . 'background-color: red \\9\\0;background-color: red \\9\\0 !important;content: "red \\0";content: "red઼";}'; - self::assertSame($sExpected, $oDoc->render()); - } - /** * @depends files * @@ -1115,17 +1035,19 @@ public function ieHacksParsing(): void */ public function commentExtracting(): void { - $oDoc = self::parsedStructureForFile('comments'); - $aNodes = $oDoc->getContents(); + $document = self::parsedStructureForFile('comments'); + $nodes = $document->getContents(); // Import property. - $importComments = $aNodes[0]->getComments(); + self::assertInstanceOf(Commentable::class, $nodes[0]); + $importComments = $nodes[0]->getComments(); self::assertCount(2, $importComments); self::assertSame("*\n * Comments\n ", $importComments[0]->getComment()); self::assertSame(' Hell ', $importComments[1]->getComment()); // Declaration block. - $fooBarBlock = $aNodes[1]; + $fooBarBlock = $nodes[1]; + self::assertInstanceOf(Commentable::class, $fooBarBlock); $fooBarBlockComments = $fooBarBlock->getComments(); // TODO Support comments in selectors. // $this->assertCount(2, $fooBarBlockComments); @@ -1133,6 +1055,7 @@ public function commentExtracting(): void // $this->assertSame("* Number 5 *", $fooBarBlockComments[1]->getComment()); // Declaration rules. + self::assertInstanceOf(DeclarationBlock::class, $fooBarBlock); $fooBarRules = $fooBarBlock->getRules(); $fooBarRule = $fooBarRules[0]; $fooBarRuleComments = $fooBarRule->getComments(); @@ -1140,16 +1063,20 @@ public function commentExtracting(): void self::assertSame(' Number 6 ', $fooBarRuleComments[0]->getComment()); // Media property. - $mediaComments = $aNodes[2]->getComments(); + self::assertInstanceOf(Commentable::class, $nodes[2]); + $mediaComments = $nodes[2]->getComments(); self::assertCount(0, $mediaComments); // Media children. - $mediaRules = $aNodes[2]->getContents(); + self::assertInstanceOf(CSSList::class, $nodes[2]); + $mediaRules = $nodes[2]->getContents(); + self::assertInstanceOf(Commentable::class, $mediaRules[0]); $fooBarComments = $mediaRules[0]->getComments(); self::assertCount(1, $fooBarComments); self::assertSame('* Number 10 *', $fooBarComments[0]->getComment()); // Media -> declaration -> rule. + self::assertInstanceOf(DeclarationBlock::class, $mediaRules[0]); $fooBarRules = $mediaRules[0]->getRules(); $fooBarChildComments = $fooBarRules[0]->getComments(); self::assertCount(1, $fooBarChildComments); @@ -1159,25 +1086,85 @@ public function commentExtracting(): void /** * @test */ - public function flatCommentExtracting(): void + public function flatCommentExtractingOneComment(): void { $parser = new Parser('div {/*Find Me!*/left:10px; text-align:left;}'); - $doc = $parser->parse(); - $contents = $doc->getContents(); + $document = $parser->parse(); + + $contents = $document->getContents(); + self::assertInstanceOf(DeclarationBlock::class, $contents[0]); $divRules = $contents[0]->getRules(); $comments = $divRules[0]->getComments(); + self::assertCount(1, $comments); self::assertSame('Find Me!', $comments[0]->getComment()); } + /** + * @test + */ + public function flatCommentExtractingTwoConjoinedCommentsForOneRule(): void + { + $parser = new Parser('div {/*Find Me!*//*Find Me Too!*/left:10px; text-align:left;}'); + $document = $parser->parse(); + + $contents = $document->getContents(); + self::assertInstanceOf(DeclarationBlock::class, $contents[0]); + $divRules = $contents[0]->getRules(); + $comments = $divRules[0]->getComments(); + + self::assertCount(2, $comments); + self::assertSame('Find Me!', $comments[0]->getComment()); + self::assertSame('Find Me Too!', $comments[1]->getComment()); + } + + /** + * @test + */ + public function flatCommentExtractingTwoSpaceSeparatedCommentsForOneRule(): void + { + $parser = new Parser('div { /*Find Me!*/ /*Find Me Too!*/ left:10px; text-align:left;}'); + $document = $parser->parse(); + + $contents = $document->getContents(); + self::assertInstanceOf(DeclarationBlock::class, $contents[0]); + $divRules = $contents[0]->getRules(); + $comments = $divRules[0]->getComments(); + + self::assertCount(2, $comments); + self::assertSame('Find Me!', $comments[0]->getComment()); + self::assertSame('Find Me Too!', $comments[1]->getComment()); + } + + /** + * @test + */ + public function flatCommentExtractingCommentsForTwoRules(): void + { + $parser = new Parser('div {/*Find Me!*/left:10px; /*Find Me Too!*/text-align:left;}'); + $document = $parser->parse(); + + $contents = $document->getContents(); + self::assertInstanceOf(DeclarationBlock::class, $contents[0]); + $divRules = $contents[0]->getRules(); + $rule1Comments = $divRules[0]->getComments(); + $rule2Comments = $divRules[1]->getComments(); + + self::assertCount(1, $rule1Comments); + self::assertCount(1, $rule2Comments); + self::assertSame('Find Me!', $rule1Comments[0]->getComment()); + self::assertSame('Find Me Too!', $rule2Comments[0]->getComment()); + } + /** * @test */ public function topLevelCommentExtracting(): void { $parser = new Parser('/*Find Me!*/div {left:10px; text-align:left;}'); - $doc = $parser->parse(); - $contents = $doc->getContents(); + $document = $parser->parse(); + $contents = $document->getContents(); + self::assertInstanceOf(Commentable::class, $contents[0]); $comments = $contents[0]->getComments(); self::assertCount(1, $comments); self::assertSame('Find Me!', $comments[0]->getComment()); @@ -1190,7 +1177,7 @@ public function microsoftFilterStrictParsing(): void { $this->expectException(UnexpectedTokenException::class); - $oDoc = self::parsedStructureForFile('ms-filter', Settings::create()->beStrict()); + $document = self::parsedStructureForFile('ms-filter', Settings::create()->beStrict()); } /** @@ -1198,10 +1185,10 @@ public function microsoftFilterStrictParsing(): void */ public function microsoftFilterParsing(): void { - $oDoc = self::parsedStructureForFile('ms-filter'); - $sExpected = '.test {filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#80000000",' + $document = self::parsedStructureForFile('ms-filter'); + $expected = '.test {filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#80000000",' . 'endColorstr="#00000000",GradientType=1);}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); } /** @@ -1209,9 +1196,9 @@ public function microsoftFilterParsing(): void */ public function largeSizeValuesInFile(): void { - $oDoc = self::parsedStructureForFile('large-z-index', Settings::create()->withMultibyteSupport(false)); - $sExpected = '.overlay {z-index: 10000000000000000000000;}'; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile('large-z-index', Settings::create()->withMultibyteSupport(false)); + $expected = '.overlay {z-index: 10000000000000000000000;}'; + self::assertSame($expected, $document->render()); } /** @@ -1219,14 +1206,14 @@ public function largeSizeValuesInFile(): void */ public function scientificNotationSizeValuesInFile(): void { - $oDoc = $this->parsedStructureForFile( + $document = self::parsedStructureForFile( 'scientific-notation-numbers', Settings::create()->withMultibyteSupport(false) ); - $sExpected = '' + $expected = '' . 'body {background-color: rgba(62,174,151,3041820656523200167936);' . 'z-index: .030418206565232;font-size: 1em;top: 192.3478px;}'; - self::assertSame($sExpected, $oDoc->render()); + self::assertSame($expected, $document->render()); } /** @@ -1234,19 +1221,20 @@ public function scientificNotationSizeValuesInFile(): void */ public function lonelyImport(): void { - $oDoc = self::parsedStructureForFile('lonely-import'); - $sExpected = '@import url("example.css") only screen and (max-width: 600px);'; - self::assertSame($sExpected, $oDoc->render()); + $document = self::parsedStructureForFile('lonely-import'); + $expected = '@import url("example.css") only screen and (max-width: 600px);'; + self::assertSame($expected, $document->render()); } public function escapedSpecialCaseTokens(): void { - $oDoc = $this->parsedStructureForFile('escaped-tokens'); - $contents = $oDoc->getContents(); + $document = self::parsedStructureForFile('escaped-tokens'); + $contents = $document->getContents(); + self::assertInstanceOf(RuleSet::class, $contents[0]); $rules = $contents[0]->getRules(); $urlRule = $rules[0]; $calcRule = $rules[1]; - self::assertTrue(\is_a($urlRule->getValue(), '\\Sabberworm\\CSS\\Value\\URL')); - self::assertTrue(\is_a($calcRule->getValue(), '\\Sabberworm\\CSS\\Value\\CalcFunction')); + self::assertInstanceOf(URL::class, $urlRule->getValue()); + self::assertInstanceOf(CalcFunction::class, $calcRule->getValue()); } } diff --git a/tests/RuleSet/DeclarationBlockTest.php b/tests/RuleSet/DeclarationBlockTest.php index e94f1c54e..63411a646 100644 --- a/tests/RuleSet/DeclarationBlockTest.php +++ b/tests/RuleSet/DeclarationBlockTest.php @@ -1,10 +1,15 @@ parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->expandBorderShorthand(); - } - self::assertSame(\trim((string) $oDoc), $sExpected); - } + $css = '.wrapper { left: 10px; text-align: left; }'; + $parser = new Parser($css); + $document = $parser->parse(); + $rule = new Rule('right'); + $rule->setValue('-10px'); + $contents = $document->getContents(); + $wrapper = $contents[0]; - /** - * @return array> - */ - public static function expandBorderShorthandProvider(): array - { - return [ - ['body{ border: 2px solid #000 }', 'body {border-width: 2px;border-style: solid;border-color: #000;}'], - ['body{ border: none }', 'body {border-style: none;}'], - ['body{ border: 2px }', 'body {border-width: 2px;}'], - ['body{ border: #f00 }', 'body {border-color: #f00;}'], - ['body{ border: 1em solid }', 'body {border-width: 1em;border-style: solid;}'], - ['body{ margin: 1em; }', 'body {margin: 1em;}'], - ]; - } - - /** - * @dataProvider expandFontShorthandProvider - * - * @test - */ - public function expandFontShorthand(string $sCss, string $sExpected): void - { - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->expandFontShorthand(); - } - self::assertSame(\trim((string) $oDoc), $sExpected); - } + self::assertInstanceOf(DeclarationBlock::class, $wrapper); + self::assertCount(2, $wrapper->getRules()); + $wrapper->setRules([$rule]); - /** - * @return array> - */ - public static function expandFontShorthandProvider(): array - { - return [ - [ - 'body{ margin: 1em; }', - 'body {margin: 1em;}', - ], - [ - 'body {font: 12px serif;}', - 'body {font-style: normal;font-variant: normal;font-weight: normal;font-size: 12px;' - . 'line-height: normal;font-family: serif;}', - ], - [ - 'body {font: italic 12px serif;}', - 'body {font-style: italic;font-variant: normal;font-weight: normal;font-size: 12px;' - . 'line-height: normal;font-family: serif;}', - ], - [ - 'body {font: italic bold 12px serif;}', - 'body {font-style: italic;font-variant: normal;font-weight: bold;font-size: 12px;' - . 'line-height: normal;font-family: serif;}', - ], - [ - 'body {font: italic bold 12px/1.6 serif;}', - 'body {font-style: italic;font-variant: normal;font-weight: bold;font-size: 12px;' - . 'line-height: 1.6;font-family: serif;}', - ], - [ - 'body {font: italic small-caps bold 12px/1.6 serif;}', - 'body {font-style: italic;font-variant: small-caps;font-weight: bold;font-size: 12px;' - . 'line-height: 1.6;font-family: serif;}', - ], - ]; + $rules = $wrapper->getRules(); + self::assertCount(1, $rules); + self::assertSame('right', $rules[0]->getRule()); + self::assertSame('-10px', $rules[0]->getValue()); } /** - * @dataProvider expandBackgroundShorthandProvider - * * @test */ - public function expandBackgroundShorthand(string $sCss, string $sExpected): void + public function ruleInsertion(): void { - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->expandBackgroundShorthand(); - } - self::assertSame(\trim((string) $oDoc), $sExpected); - } + $css = '.wrapper { left: 10px; text-align: left; }'; + $parser = new Parser($css); + $document = $parser->parse(); + $contents = $document->getContents(); + $wrapper = $contents[0]; - /** - * @return array> - */ - public static function expandBackgroundShorthandProvider(): array - { - return [ - ['body {border: 1px;}', 'body {border: 1px;}'], - [ - 'body {background: #f00;}', - 'body {background-color: #f00;background-image: none;background-repeat: repeat;' - . 'background-attachment: scroll;background-position: 0% 0%;}', - ], - [ - 'body {background: #f00 url("foobar.png");}', - 'body {background-color: #f00;background-image: url("foobar.png");background-repeat: repeat;' - . 'background-attachment: scroll;background-position: 0% 0%;}', - ], - [ - 'body {background: #f00 url("foobar.png") no-repeat;}', - 'body {background-color: #f00;background-image: url("foobar.png");background-repeat: no-repeat;' - . 'background-attachment: scroll;background-position: 0% 0%;}', - ], - [ - 'body {background: #f00 url("foobar.png") no-repeat center;}', - 'body {background-color: #f00;background-image: url("foobar.png");background-repeat: no-repeat;' - . 'background-attachment: scroll;background-position: center center;}', - ], - [ - 'body {background: #f00 url("foobar.png") no-repeat top left;}', - 'body {background-color: #f00;background-image: url("foobar.png");background-repeat: no-repeat;' - . 'background-attachment: scroll;background-position: top left;}', - ], - ]; - } + self::assertInstanceOf(DeclarationBlock::class, $wrapper); - /** - * @dataProvider expandDimensionsShorthandProvider - * - * @test - */ - public function expandDimensionsShorthand(string $sCss, string $sExpected): void - { - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->expandDimensionsShorthand(); - } - self::assertSame(\trim((string) $oDoc), $sExpected); - } + $leftRules = $wrapper->getRules('left'); + self::assertCount(1, $leftRules); + $firstLeftRule = $leftRules[0]; - /** - * @return array> - */ - public static function expandDimensionsShorthandProvider(): array - { - return [ - ['body {border: 1px;}', 'body {border: 1px;}'], - ['body {margin-top: 1px;}', 'body {margin-top: 1px;}'], - ['body {margin: 1em;}', 'body {margin-top: 1em;margin-right: 1em;margin-bottom: 1em;margin-left: 1em;}'], - [ - 'body {margin: 1em 2em;}', - 'body {margin-top: 1em;margin-right: 2em;margin-bottom: 1em;margin-left: 2em;}', - ], - [ - 'body {margin: 1em 2em 3em;}', - 'body {margin-top: 1em;margin-right: 2em;margin-bottom: 3em;margin-left: 2em;}', - ], - ]; - } + $textRules = $wrapper->getRules('text-'); + self::assertCount(1, $textRules); + $firstTextRule = $textRules[0]; - /** - * @dataProvider createBorderShorthandProvider - * - * @test - */ - public function createBorderShorthand(string $sCss, string $sExpected): void - { - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->createBorderShorthand(); - } - self::assertSame(\trim((string) $oDoc), $sExpected); - } + $leftPrefixRule = new Rule('left'); + $leftPrefixRule->setValue(new Size(16, 'em')); - /** - * @return array> - */ - public static function createBorderShorthandProvider(): array - { - return [ - ['body {border-width: 2px;border-style: solid;border-color: #000;}', 'body {border: 2px solid #000;}'], - ['body {border-style: none;}', 'body {border: none;}'], - ['body {border-width: 1em;border-style: solid;}', 'body {border: 1em solid;}'], - ['body {margin: 1em;}', 'body {margin: 1em;}'], - ]; - } + $textAlignRule = new Rule('text-align'); + $textAlignRule->setValue(new Size(1)); - /** - * @dataProvider createFontShorthandProvider - * - * @test - */ - public function createFontShorthand(string $sCss, string $sExpected): void - { - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->createFontShorthand(); - } - self::assertSame(\trim((string) $oDoc), $sExpected); - } + $borderBottomRule = new Rule('border-bottom-width'); + $borderBottomRule->setValue(new Size(1, 'px')); - /** - * @return array> - */ - public static function createFontShorthandProvider(): array - { - return [ - ['body {font-size: 12px; font-family: serif}', 'body {font: 12px serif;}'], - ['body {font-size: 12px; font-family: serif; font-style: italic;}', 'body {font: italic 12px serif;}'], - [ - 'body {font-size: 12px; font-family: serif; font-style: italic; font-weight: bold;}', - 'body {font: italic bold 12px serif;}', - ], - [ - 'body {font-size: 12px; font-family: serif; font-style: italic; font-weight: bold; line-height: 1.6;}', - 'body {font: italic bold 12px/1.6 serif;}', - ], - [ - 'body {font-size: 12px; font-family: serif; font-style: italic; font-weight: bold; ' - . 'line-height: 1.6; font-variant: small-caps;}', - 'body {font: italic small-caps bold 12px/1.6 serif;}', - ], - ['body {margin: 1em;}', 'body {margin: 1em;}'], - ]; - } + $wrapper->addRule($borderBottomRule); + $wrapper->addRule($leftPrefixRule, $firstLeftRule); + $wrapper->addRule($textAlignRule, $firstTextRule); - /** - * @dataProvider createDimensionsShorthandProvider - * - * @test - */ - public function createDimensionsShorthand(string $sCss, string $sExpected): void - { - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->createDimensionsShorthand(); - } - self::assertSame(\trim((string) $oDoc), $sExpected); - } + $rules = $wrapper->getRules(); - /** - * @return array> - */ - public static function createDimensionsShorthandProvider(): array - { - return [ - ['body {border: 1px;}', 'body {border: 1px;}'], - ['body {margin-top: 1px;}', 'body {margin-top: 1px;}'], - ['body {margin-top: 1em; margin-right: 1em; margin-bottom: 1em; margin-left: 1em;}', 'body {margin: 1em;}'], - [ - 'body {margin-top: 1em; margin-right: 2em; margin-bottom: 1em; margin-left: 2em;}', - 'body {margin: 1em 2em;}', - ], - [ - 'body {margin-top: 1em; margin-right: 2em; margin-bottom: 3em; margin-left: 2em;}', - 'body {margin: 1em 2em 3em;}', - ], - ]; - } + self::assertSame($leftPrefixRule, $rules[0]); + self::assertSame($firstLeftRule, $rules[1]); + self::assertSame($textAlignRule, $rules[2]); + self::assertSame($firstTextRule, $rules[3]); + self::assertSame($borderBottomRule, $rules[4]); - /** - * @dataProvider createBackgroundShorthandProvider - * - * @test - */ - public function createBackgroundShorthand(string $sCss, string $sExpected): void - { - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) { - $oDeclaration->createBackgroundShorthand(); - } - self::assertSame(\trim((string) $oDoc), $sExpected); + self::assertSame( + '.wrapper {left: 16em;left: 10px;text-align: 1;text-align: left;border-bottom-width: 1px;}', + $document->render() + ); } /** - * @return array> + * @return array */ - public static function createBackgroundShorthandProvider(): array + public static function declarationBlocksWithCommentsProvider(): array { return [ - ['body {border: 1px;}', 'body {border: 1px;}'], - ['body {background-color: #f00;}', 'body {background: #f00;}'], - [ - 'body {background-color: #f00;background-image: url(foobar.png);}', - 'body {background: #f00 url("foobar.png");}', - ], - [ - 'body {background-color: #f00;background-image: url(foobar.png);background-repeat: no-repeat;}', - 'body {background: #f00 url("foobar.png") no-repeat;}', - ], - [ - 'body {background-color: #f00;background-image: url(foobar.png);background-repeat: no-repeat;}', - 'body {background: #f00 url("foobar.png") no-repeat;}', - ], - [ - 'body {background-color: #f00;background-image: url(foobar.png);background-repeat: no-repeat;' - . 'background-position: center;}', - 'body {background: #f00 url("foobar.png") no-repeat center;}', - ], - [ - 'body {background-color: #f00;background-image: url(foobar.png);background-repeat: no-repeat;' - . 'background-position: top left;}', - 'body {background: #f00 url("foobar.png") no-repeat top left;}', - ], + 'CSS comments with one asterisk' => ['p {color: #000;/* black */}', 'p {color: #000;}'], + 'CSS comments with two asterisks' => ['p {color: #000;/** black */}', 'p {color: #000;}'], ]; } /** * @test + * @dataProvider declarationBlocksWithCommentsProvider */ - public function overrideRules(): void - { - $sCss = '.wrapper { left: 10px; text-align: left; }'; - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - $oRule = new Rule('right'); - $oRule->setValue('-10px'); - $aContents = $oDoc->getContents(); - $oWrapper = $aContents[0]; + public function canRemoveCommentsFromRulesUsingLenientParsing( + string $cssWithComments, + string $cssWithoutComments + ): void { + $parserSettings = ParserSettings::create()->withLenientParsing(true); + $document = (new Parser($cssWithComments, $parserSettings))->parse(); - self::assertCount(2, $oWrapper->getRules()); - $aContents[0]->setRules([$oRule]); + $outputFormat = (new OutputFormat())->setRenderComments(false); + $renderedDocument = $document->render($outputFormat); - $aRules = $oWrapper->getRules(); - self::assertCount(1, $aRules); - self::assertSame('right', $aRules[0]->getRule()); - self::assertSame('-10px', $aRules[0]->getValue()); + self::assertSame($cssWithoutComments, $renderedDocument); } /** * @test + * @dataProvider declarationBlocksWithCommentsProvider */ - public function ruleInsertion(): void - { - $sCss = '.wrapper { left: 10px; text-align: left; }'; - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - $aContents = $oDoc->getContents(); - $oWrapper = $aContents[0]; - - $oFirst = $oWrapper->getRules('left'); - self::assertCount(1, $oFirst); - $oFirst = $oFirst[0]; - - $oSecond = $oWrapper->getRules('text-'); - self::assertCount(1, $oSecond); - $oSecond = $oSecond[0]; + public function canRemoveCommentsFromRulesUsingStrictParsing( + string $cssWithComments, + string $cssWithoutComments + ): void { + $parserSettings = ParserSettings::create()->withLenientParsing(false); + $document = (new Parser($cssWithComments, $parserSettings))->parse(); - $oBefore = new Rule('left'); - $oBefore->setValue(new Size(16, 'em')); + $outputFormat = (new OutputFormat())->setRenderComments(false); + $renderedDocument = $document->render($outputFormat); - $oMiddle = new Rule('text-align'); - $oMiddle->setValue(new Size(1)); - - $oAfter = new Rule('border-bottom-width'); - $oAfter->setValue(new Size(1, 'px')); - - $oWrapper->addRule($oAfter); - $oWrapper->addRule($oBefore, $oFirst); - $oWrapper->addRule($oMiddle, $oSecond); - - $aRules = $oWrapper->getRules(); - - self::assertSame($oBefore, $aRules[0]); - self::assertSame($oFirst, $aRules[1]); - self::assertSame($oMiddle, $aRules[2]); - self::assertSame($oSecond, $aRules[3]); - self::assertSame($oAfter, $aRules[4]); - - self::assertSame( - '.wrapper {left: 16em;left: 10px;text-align: 1;text-align: left;border-bottom-width: 1px;}', - $oDoc->render() - ); - } - - /** - * @test - * - * TODO: The order is different on PHP 5.6 than on PHP >= 7.0. - */ - public function orderOfElementsMatchingOriginalOrderAfterExpandingShorthands(): void - { - $sCss = '.rule{padding:5px;padding-top: 20px}'; - $oParser = new Parser($sCss); - $oDoc = $oParser->parse(); - $aDocs = $oDoc->getAllDeclarationBlocks(); - - self::assertCount(1, $aDocs); - - $oDeclaration = \array_pop($aDocs); - $oDeclaration->expandShorthands(); - - self::assertEquals( - [ - 'padding-top' => 'padding-top: 20px;', - 'padding-right' => 'padding-right: 5px;', - 'padding-bottom' => 'padding-bottom: 5px;', - 'padding-left' => 'padding-left: 5px;', - ], - \array_map('strval', $oDeclaration->getRulesAssoc()) - ); + self::assertSame($cssWithoutComments, $renderedDocument); } } diff --git a/tests/RuleSet/LenientParsingTest.php b/tests/RuleSet/LenientParsingTest.php index 3d670f74a..ae249701d 100644 --- a/tests/RuleSet/LenientParsingTest.php +++ b/tests/RuleSet/LenientParsingTest.php @@ -1,5 +1,7 @@ expectException(UnexpectedTokenException::class); - $sFile = __DIR__ . '/../fixtures/-fault-tolerance.css'; - $oParser = new Parser(\file_get_contents($sFile), Settings::create()->beStrict()); - $oParser->parse(); + $pathToFile = __DIR__ . '/../fixtures/-fault-tolerance.css'; + $parser = new Parser(file_get_contents($pathToFile), Settings::create()->beStrict()); + $parser->parse(); } /** @@ -39,13 +34,13 @@ public function faultToleranceOff(): void */ public function faultToleranceOn(): void { - $sFile = __DIR__ . '/../fixtures/-fault-tolerance.css'; - $oParser = new Parser(\file_get_contents($sFile), Settings::create()->withLenientParsing(true)); - $oResult = $oParser->parse(); + $pathToFile = __DIR__ . '/../fixtures/-fault-tolerance.css'; + $parser = new Parser(file_get_contents($pathToFile), Settings::create()->withLenientParsing(true)); + $result = $parser->parse(); self::assertSame( '.test1 {}' . "\n" . '.test2 {hello: 2.2;hello: 2000000000000.2;}' . "\n" . '#test {}' . "\n" . '#test2 {help: none;}', - $oResult->render() + $result->render() ); } @@ -56,9 +51,9 @@ public function endToken(): void { $this->expectException(UnexpectedTokenException::class); - $sFile = __DIR__ . '/../fixtures/-end-token.css'; - $oParser = new Parser(\file_get_contents($sFile), Settings::create()->beStrict()); - $oParser->parse(); + $pathToFile = __DIR__ . '/../fixtures/-end-token.css'; + $parser = new Parser(file_get_contents($pathToFile), Settings::create()->beStrict()); + $parser->parse(); } /** @@ -68,9 +63,9 @@ public function endToken2(): void { $this->expectException(UnexpectedTokenException::class); - $sFile = __DIR__ . '/../fixtures/-end-token-2.css'; - $oParser = new Parser(\file_get_contents($sFile), Settings::create()->beStrict()); - $oParser->parse(); + $pathToFile = __DIR__ . '/../fixtures/-end-token-2.css'; + $parser = new Parser(file_get_contents($pathToFile), Settings::create()->beStrict()); + $parser->parse(); } /** @@ -78,10 +73,10 @@ public function endToken2(): void */ public function endTokenPositive(): void { - $sFile = __DIR__ . '/../fixtures/-end-token.css'; - $oParser = new Parser(\file_get_contents($sFile), Settings::create()->withLenientParsing(true)); - $oResult = $oParser->parse(); - self::assertSame('', $oResult->render()); + $pathToFile = __DIR__ . '/../fixtures/-end-token.css'; + $parser = new Parser(file_get_contents($pathToFile), Settings::create()->withLenientParsing(true)); + $result = $parser->parse(); + self::assertSame('', $result->render()); } /** @@ -89,12 +84,12 @@ public function endTokenPositive(): void */ public function endToken2Positive(): void { - $sFile = __DIR__ . '/../fixtures/-end-token-2.css'; - $oParser = new Parser(\file_get_contents($sFile), Settings::create()->withLenientParsing(true)); - $oResult = $oParser->parse(); + $pathToFile = __DIR__ . '/../fixtures/-end-token-2.css'; + $parser = new Parser(file_get_contents($pathToFile), Settings::create()->withLenientParsing(true)); + $result = $parser->parse(); self::assertSame( '#home .bg-layout {background-image: url("/bundles/main/img/bg1.png?5");}', - $oResult->render() + $result->render() ); } @@ -104,13 +99,13 @@ public function endToken2Positive(): void public function localeTrap(): void { \setlocale(LC_ALL, 'pt_PT', 'no'); - $sFile = __DIR__ . '/../fixtures/-fault-tolerance.css'; - $oParser = new Parser(\file_get_contents($sFile), Settings::create()->withLenientParsing(true)); - $oResult = $oParser->parse(); + $pathToFile = __DIR__ . '/../fixtures/-fault-tolerance.css'; + $parser = new Parser(file_get_contents($pathToFile), Settings::create()->withLenientParsing(true)); + $result = $parser->parse(); self::assertSame( '.test1 {}' . "\n" . '.test2 {hello: 2.2;hello: 2000000000000.2;}' . "\n" . '#test {}' . "\n" . '#test2 {help: none;}', - $oResult->render() + $result->render() ); } @@ -119,9 +114,9 @@ public function localeTrap(): void */ public function caseInsensitivity(): void { - $sFile = __DIR__ . '/../fixtures/case-insensitivity.css'; - $oParser = new Parser(\file_get_contents($sFile)); - $oResult = $oParser->parse(); + $pathToFile = __DIR__ . '/../fixtures/case-insensitivity.css'; + $parser = new Parser(file_get_contents($pathToFile)); + $result = $parser->parse(); self::assertSame( '@charset "utf-8";' . "\n" @@ -129,7 +124,7 @@ public function caseInsensitivity(): void . "\n@media screen {}" . "\n#myid {case: insensitive !important;frequency: 30Hz;font-size: 1em;color: #ff0;" . 'color: hsl(40,40%,30%);font-family: Arial;}', - $oResult->render() + $result->render() ); } @@ -138,9 +133,9 @@ public function caseInsensitivity(): void */ public function cssWithInvalidColorStillGetsParsedAsDocument(): void { - $sFile = __DIR__ . '/../fixtures/invalid-color.css'; - $oParser = new Parser(\file_get_contents($sFile), Settings::create()->withLenientParsing(true)); - $result = $oParser->parse(); + $pathToFile = __DIR__ . '/../fixtures/invalid-color.css'; + $parser = new Parser(file_get_contents($pathToFile), Settings::create()->withLenientParsing(true)); + $result = $parser->parse(); self::assertInstanceOf(Document::class, $result); } @@ -152,8 +147,8 @@ public function invalidColorStrict(): void { $this->expectException(UnexpectedTokenException::class); - $sFile = __DIR__ . '/../fixtures/invalid-color.css'; - $oParser = new Parser(\file_get_contents($sFile), Settings::create()->beStrict()); - $oParser->parse(); + $pathToFile = __DIR__ . '/../fixtures/invalid-color.css'; + $parser = new Parser(file_get_contents($pathToFile), Settings::create()->beStrict()); + $parser->parse(); } } diff --git a/tests/Unit/CSSList/AtRuleBlockListTest.php b/tests/Unit/CSSList/AtRuleBlockListTest.php new file mode 100644 index 000000000..3b61e437e --- /dev/null +++ b/tests/Unit/CSSList/AtRuleBlockListTest.php @@ -0,0 +1,147 @@ +atRuleName()); + } + + /** + * @test + */ + public function atRuleArgsByDefaultReturnsEmptyString(): void + { + $subject = new AtRuleBlockList('supports'); + + self::assertSame('', $subject->atRuleArgs()); + } + + /** + * @test + */ + public function atRuleArgsReturnsArgumentsProvidedToConstructor(): void + { + $arguments = 'bar'; + + $subject = new AtRuleBlockList('', $arguments); + + self::assertSame($arguments, $subject->atRuleArgs()); + } + + /** + * @test + */ + public function getLineNumberByDefaultReturnsNull(): void + { + $subject = new AtRuleBlockList(''); + + self::assertNull($subject->getLineNumber()); + } + + /** + * @test + */ + public function getLineNumberReturnsLineNumberProvidedToConstructor(): void + { + $lineNumber = 42; + $subject = new AtRuleBlockList('', '', $lineNumber); + + self::assertSame($lineNumber, $subject->getLineNumber()); + } + + /** + * @test + */ + public function isRootListAlwaysReturnsFalse(): void + { + $subject = new AtRuleBlockList('supports'); + + self::assertFalse($subject->isRootList()); + } +} diff --git a/tests/Unit/CSSList/CSSBlockListTest.php b/tests/Unit/CSSList/CSSBlockListTest.php new file mode 100644 index 000000000..ce7e54477 --- /dev/null +++ b/tests/Unit/CSSList/CSSBlockListTest.php @@ -0,0 +1,474 @@ +getAllDeclarationBlocks()); + } + + /** + * @test + */ + public function getAllDeclarationBlocksReturnsOneDeclarationBlockDirectlySetAsContent(): void + { + $subject = new ConcreteCSSBlockList(); + + $declarationBlock = new DeclarationBlock(); + $subject->setContents([$declarationBlock]); + + $result = $subject->getAllDeclarationBlocks(); + + self::assertSame([$declarationBlock], $result); + } + + /** + * @test + */ + public function getAllDeclarationBlocksReturnsMultipleDeclarationBlocksDirectlySetAsContents(): void + { + $subject = new ConcreteCSSBlockList(); + + $declarationBlock1 = new DeclarationBlock(); + $declarationBlock2 = new DeclarationBlock(); + $subject->setContents([$declarationBlock1, $declarationBlock2]); + + $result = $subject->getAllDeclarationBlocks(); + + self::assertSame([$declarationBlock1, $declarationBlock2], $result); + } + + /** + * @test + */ + public function getAllDeclarationBlocksReturnsDeclarationBlocksWithinAtRuleBlockList(): void + { + $subject = new ConcreteCSSBlockList(); + + $declarationBlock = new DeclarationBlock(); + $atRuleBlockList = new AtRuleBlockList('media'); + $atRuleBlockList->setContents([$declarationBlock]); + $subject->setContents([$atRuleBlockList]); + + $result = $subject->getAllDeclarationBlocks(); + + self::assertSame([$declarationBlock], $result); + } + + /** + * @test + */ + public function getAllDeclarationBlocksIgnoresImport(): void + { + $subject = new ConcreteCSSBlockList(); + + $import = new Import(new URL(new CSSString('https://www.example.com/')), ''); + $subject->setContents([$import]); + + $result = $subject->getAllDeclarationBlocks(); + + self::assertSame([], $result); + } + + /** + * @test + */ + public function getAllDeclarationBlocksIgnoresCharset(): void + { + $subject = new ConcreteCSSBlockList(); + + $charset = new Charset(new CSSString('UTF-8')); + $subject->setContents([$charset]); + + $result = $subject->getAllDeclarationBlocks(); + + self::assertSame([], $result); + } + + /** + * @test + */ + public function getAllRuleSetsWhenNoContentSetReturnsEmptyArray(): void + { + $subject = new ConcreteCSSBlockList(); + + self::assertSame([], $subject->getAllRuleSets()); + } + + /** + * @test + */ + public function getAllRuleSetsReturnsRuleSetFromOneDeclarationBlockDirectlySetAsContent(): void + { + $subject = new ConcreteCSSBlockList(); + + $declarationBlock = new DeclarationBlock(); + $subject->setContents([$declarationBlock]); + + $result = $subject->getAllRuleSets(); + + self::assertSame([$declarationBlock->getRuleSet()], $result); + } + + /** + * @test + */ + public function getAllRuleSetsReturnsOneAtRuleSetDirectlySetAsContent(): void + { + $subject = new ConcreteCSSBlockList(); + + $atRuleSet = new AtRuleSet('media'); + $subject->setContents([$atRuleSet]); + + $result = $subject->getAllRuleSets(); + + self::assertSame([$atRuleSet], $result); + } + + /** + * @test + */ + public function getAllRuleSetsReturnsRuleSetsFromMultipleDeclarationBlocksDirectlySetAsContents(): void + { + $subject = new ConcreteCSSBlockList(); + + $declarationBlock1 = new DeclarationBlock(); + $declarationBlock2 = new DeclarationBlock(); + $subject->setContents([$declarationBlock1, $declarationBlock2]); + + $result = $subject->getAllRuleSets(); + + self::assertSame([$declarationBlock1->getRuleSet(), $declarationBlock2->getRuleSet()], $result); + } + + /** + * @test + */ + public function getAllRuleSetsReturnsMultipleAtRuleSetsDirectlySetAsContents(): void + { + $subject = new ConcreteCSSBlockList(); + + $atRuleSet1 = new AtRuleSet('media'); + $atRuleSet2 = new AtRuleSet('media'); + $subject->setContents([$atRuleSet1, $atRuleSet2]); + + $result = $subject->getAllRuleSets(); + + self::assertSame([$atRuleSet1, $atRuleSet2], $result); + } + + /** + * @test + */ + public function getAllRuleSetsReturnsRuleSetsFromDeclarationBlocksWithinAtRuleBlockList(): void + { + $subject = new ConcreteCSSBlockList(); + + $declarationBlock = new DeclarationBlock(); + $atRuleBlockList = new AtRuleBlockList('media'); + $atRuleBlockList->setContents([$declarationBlock]); + $subject->setContents([$atRuleBlockList]); + + $result = $subject->getAllRuleSets(); + + self::assertSame([$declarationBlock->getRuleSet()], $result); + } + + /** + * @test + */ + public function getAllRuleSetsReturnsAtRuleSetsWithinAtRuleBlockList(): void + { + $subject = new ConcreteCSSBlockList(); + + $atRule = new AtRuleSet('media'); + $atRuleBlockList = new AtRuleBlockList('media'); + $atRuleBlockList->setContents([$atRule]); + $subject->setContents([$atRuleBlockList]); + + $result = $subject->getAllRuleSets(); + + self::assertSame([$atRule], $result); + } + + /** + * @test + */ + public function getAllRuleSetsIgnoresImport(): void + { + $subject = new ConcreteCSSBlockList(); + + $import = new Import(new URL(new CSSString('https://www.example.com/')), ''); + $subject->setContents([$import]); + + $result = $subject->getAllRuleSets(); + + self::assertSame([], $result); + } + + /** + * @test + */ + public function getAllRuleSetsIgnoresCharset(): void + { + $subject = new ConcreteCSSBlockList(); + + $charset = new Charset(new CSSString('UTF-8')); + $subject->setContents([$charset]); + + $result = $subject->getAllRuleSets(); + + self::assertSame([], $result); + } + + /** + * @test + */ + public function getAllValuesWhenNoContentSetReturnsEmptyArray(): void + { + $subject = new ConcreteCSSBlockList(); + + self::assertSame([], $subject->getAllValues()); + } + + /** + * @test + */ + public function getAllValuesReturnsOneValueDirectlySetAsContent(): void + { + $subject = new ConcreteCSSBlockList(); + + $value = new CSSString('Superfont'); + + $declarationBlock = new DeclarationBlock(); + $rule = new Rule('font-family'); + $rule->setValue($value); + $declarationBlock->addRule($rule); + $subject->setContents([$declarationBlock]); + + $result = $subject->getAllValues(); + + self::assertSame([$value], $result); + } + + /** + * @test + */ + public function getAllValuesReturnsMultipleValuesDirectlySetAsContentInOneDeclarationBlock(): void + { + $subject = new ConcreteCSSBlockList(); + + $value1 = new CSSString('Superfont'); + $value2 = new CSSString('aquamarine'); + + $declarationBlock = new DeclarationBlock(); + $rule1 = new Rule('font-family'); + $rule1->setValue($value1); + $declarationBlock->addRule($rule1); + $rule2 = new Rule('color'); + $rule2->setValue($value2); + $declarationBlock->addRule($rule2); + $subject->setContents([$declarationBlock]); + + $result = $subject->getAllValues(); + + self::assertSame([$value1, $value2], $result); + } + + /** + * @test + */ + public function getAllValuesReturnsMultipleValuesDirectlySetAsContentInMultipleDeclarationBlocks(): void + { + $subject = new ConcreteCSSBlockList(); + + $value1 = new CSSString('Superfont'); + $value2 = new CSSString('aquamarine'); + + $declarationBlock1 = new DeclarationBlock(); + $rule1 = new Rule('font-family'); + $rule1->setValue($value1); + $declarationBlock1->addRule($rule1); + $declarationBlock2 = new DeclarationBlock(); + $rule2 = new Rule('color'); + $rule2->setValue($value2); + $declarationBlock2->addRule($rule2); + $subject->setContents([$declarationBlock1, $declarationBlock2]); + + $result = $subject->getAllValues(); + + self::assertSame([$value1, $value2], $result); + } + + /** + * @test + */ + public function getAllValuesReturnsValuesWithinAtRuleBlockList(): void + { + $subject = new ConcreteCSSBlockList(); + + $value = new CSSString('Superfont'); + + $declarationBlock = new DeclarationBlock(); + $rule = new Rule('font-family'); + $rule->setValue($value); + $declarationBlock->addRule($rule); + $atRuleBlockList = new AtRuleBlockList('media'); + $atRuleBlockList->setContents([$declarationBlock]); + $subject->setContents([$atRuleBlockList]); + + $result = $subject->getAllValues(); + + self::assertSame([$value], $result); + } + + /** + * @test + */ + public function getAllValuesWithElementProvidedReturnsOnlyValuesWithinThatElement(): void + { + $subject = new ConcreteCSSBlockList(); + + $value1 = new CSSString('Superfont'); + $value2 = new CSSString('aquamarine'); + + $declarationBlock1 = new DeclarationBlock(); + $rule1 = new Rule('font-family'); + $rule1->setValue($value1); + $declarationBlock1->addRule($rule1); + $declarationBlock2 = new DeclarationBlock(); + $rule2 = new Rule('color'); + $rule2->setValue($value2); + $declarationBlock2->addRule($rule2); + $subject->setContents([$declarationBlock1, $declarationBlock2]); + + $result = $subject->getAllValues($declarationBlock1); + + self::assertSame([$value1], $result); + } + + /** + * @test + */ + public function getAllValuesWithSearchStringProvidedReturnsOnlyValuesFromMatchingRules(): void + { + $subject = new ConcreteCSSBlockList(); + + $value1 = new CSSString('Superfont'); + $value2 = new CSSString('aquamarine'); + + $declarationBlock = new DeclarationBlock(); + $rule1 = new Rule('font-family'); + $rule1->setValue($value1); + $declarationBlock->addRule($rule1); + $rule2 = new Rule('color'); + $rule2->setValue($value2); + $declarationBlock->addRule($rule2); + $subject->setContents([$declarationBlock]); + + $result = $subject->getAllValues(null, 'font-'); + + self::assertSame([$value1], $result); + } + + /** + * @test + */ + public function getAllValuesByDefaultDoesNotReturnValuesInFunctionArguments(): void + { + $subject = new ConcreteCSSBlockList(); + + $value1 = new Size(10, 'px'); + $value2 = new Size(2, '%'); + + $declarationBlock = new DeclarationBlock(); + $rule = new Rule('margin'); + $rule->setValue(new CSSFunction('max', [$value1, $value2])); + $declarationBlock->addRule($rule); + $subject->setContents([$declarationBlock]); + + $result = $subject->getAllValues(); + + self::assertSame([], $result); + } + + /** + * @test + */ + public function getAllValuesWithSearchInFunctionArgumentsReturnsValuesInFunctionArguments(): void + { + $subject = new ConcreteCSSBlockList(); + + $value1 = new Size(10, 'px'); + $value2 = new Size(2, '%'); + + $declarationBlock = new DeclarationBlock(); + $rule = new Rule('margin'); + $rule->setValue(new CSSFunction('max', [$value1, $value2])); + $declarationBlock->addRule($rule); + $subject->setContents([$declarationBlock]); + + $result = $subject->getAllValues(null, null, true); + + self::assertSame([$value1, $value2], $result); + } +} diff --git a/tests/Unit/CSSList/CSSListTest.php b/tests/Unit/CSSList/CSSListTest.php new file mode 100644 index 000000000..ada176e9a --- /dev/null +++ b/tests/Unit/CSSList/CSSListTest.php @@ -0,0 +1,329 @@ +getLineNumber()); + } + + /** + * @test + */ + public function getLineNumberReturnsLineNumberProvidedToConstructor(): void + { + $lineNumber = 42; + $subject = new ConcreteCSSList($lineNumber); + + self::assertSame($lineNumber, $subject->getLineNumber()); + } + + /** + * @test + */ + public function getContentsInitiallyReturnsEmptyArray(): void + { + $subject = new ConcreteCSSList(); + + self::assertSame([], $subject->getContents()); + } + + /** + * @return array}> + */ + public static function contentsDataProvider(): array + { + return [ + 'empty array' => [[]], + '1 item' => [[new DeclarationBlock()]], + '2 items' => [[new DeclarationBlock(), new DeclarationBlock()]], + ]; + } + + /** + * @test + * + * @param list $contents + * + * @dataProvider contentsDataProvider + */ + public function setContentsSetsContents(array $contents): void + { + $subject = new ConcreteCSSList(); + + $subject->setContents($contents); + + self::assertSame($contents, $subject->getContents()); + } + + /** + * @test + */ + public function setContentsReplacesContentsSetInPreviousCall(): void + { + $subject = new ConcreteCSSList(); + + $contents2 = [new DeclarationBlock()]; + + $subject->setContents([new DeclarationBlock()]); + $subject->setContents($contents2); + + self::assertSame($contents2, $subject->getContents()); + } + + /** + * @test + */ + public function insertBeforeInsertsContentBeforeSibling(): void + { + $subject = new ConcreteCSSList(); + + $bogusOne = new DeclarationBlock(); + $bogusOne->setSelectors('.bogus-one'); + $bogusTwo = new DeclarationBlock(); + $bogusTwo->setSelectors('.bogus-two'); + + $item = new DeclarationBlock(); + $item->setSelectors('.item'); + + $sibling = new DeclarationBlock(); + $sibling->setSelectors('.sibling'); + + $subject->setContents([$bogusOne, $sibling, $bogusTwo]); + + self::assertCount(3, $subject->getContents()); + + $subject->insertBefore($item, $sibling); + + self::assertCount(4, $subject->getContents()); + self::assertSame([$bogusOne, $item, $sibling, $bogusTwo], $subject->getContents()); + } + + /** + * @test + */ + public function insertBeforeAppendsIfSiblingNotFound(): void + { + $subject = new ConcreteCSSList(); + + $bogusOne = new DeclarationBlock(); + $bogusOne->setSelectors('.bogus-one'); + $bogusTwo = new DeclarationBlock(); + $bogusTwo->setSelectors('.bogus-two'); + + $item = new DeclarationBlock(); + $item->setSelectors('.item'); + + $sibling = new DeclarationBlock(); + $sibling->setSelectors('.sibling'); + + $orphan = new DeclarationBlock(); + $orphan->setSelectors('.forever-alone'); + + $subject->setContents([$bogusOne, $sibling, $bogusTwo]); + + self::assertCount(3, $subject->getContents()); + + $subject->insertBefore($item, $orphan); + + self::assertCount(4, $subject->getContents()); + self::assertSame([$bogusOne, $sibling, $bogusTwo, $item], $subject->getContents()); + } + + /** + * @test + */ + public function removeDeclarationBlockBySelectorRemovesDeclarationBlockProvided(): void + { + $subject = new ConcreteCSSList(); + $declarationBlock = new DeclarationBlock(); + $declarationBlock->setSelectors(['html', 'body']); + $subject->setContents([$declarationBlock]); + self::assertNotSame([], $subject->getContents()); // make sure contents are set + + $subject->removeDeclarationBlockBySelector($declarationBlock); + + self::assertSame([], $subject->getContents()); + } + + /** + * @test + */ + public function removeDeclarationBlockBySelectorRemovesDeclarationBlockWithSelectorsProvidedFromItself(): void + { + $subject = new ConcreteCSSList(); + $declarationBlock = new DeclarationBlock(); + $declarationBlock->setSelectors(['html', 'body']); + $subject->setContents([$declarationBlock]); + self::assertNotSame([], $subject->getContents()); // make sure contents are set + + $subject->removeDeclarationBlockBySelector($declarationBlock->getSelectors()); + + self::assertSame([], $subject->getContents()); + } + + /** + * @test + */ + public function removeDeclarationBlockBySelectorRemovesDeclarationBlockWithOutsourcedSelectorsProvided(): void + { + $subject = new ConcreteCSSList(); + $declarationBlock = new DeclarationBlock(); + $declarationBlock->setSelectors(['html', 'body']); + $subject->setContents([$declarationBlock]); + self::assertNotSame([], $subject->getContents()); // make sure contents are set + + $subject->removeDeclarationBlockBySelector([new Selector('html'), new Selector('body')]); + + self::assertSame([], $subject->getContents()); + } + + /** + * @test + */ + public function removeDeclarationBlockBySelectorRemovesDeclarationBlockWithStringSelectorsProvided(): void + { + $subject = new ConcreteCSSList(); + $declarationBlock = new DeclarationBlock(); + $declarationBlock->setSelectors(['html', 'body']); + $subject->setContents([$declarationBlock]); + self::assertNotSame([], $subject->getContents()); // make sure contents are set + + $subject->removeDeclarationBlockBySelector(['html', 'body']); + + self::assertSame([], $subject->getContents()); + } + + /** + * @test + */ + public function removeDeclarationBlockBySelectorRemovesDeclarationBlockProvidedAndAnotherWithSameSelectors(): void + { + $subject = new ConcreteCSSList(); + $declarationBlock1 = new DeclarationBlock(); + $declarationBlock1->setSelectors(['html', 'body']); + $declarationBlock2 = new DeclarationBlock(); + $declarationBlock2->setSelectors(['html', 'body']); + $subject->setContents([$declarationBlock1, $declarationBlock2]); + self::assertNotSame([], $subject->getContents()); // make sure contents are set + + $subject->removeDeclarationBlockBySelector($declarationBlock1, true); + + self::assertSame([], $subject->getContents()); + } + + /** + * @test + */ + public function removeDeclarationBlockBySelectorRemovesBlockWithSelectorsFromItselfAndAnotherMatching(): void + { + $subject = new ConcreteCSSList(); + $declarationBlock1 = new DeclarationBlock(); + $declarationBlock1->setSelectors(['html', 'body']); + $declarationBlock2 = new DeclarationBlock(); + $declarationBlock2->setSelectors(['html', 'body']); + $subject->setContents([$declarationBlock1, $declarationBlock2]); + self::assertNotSame([], $subject->getContents()); // make sure contents are set + + $subject->removeDeclarationBlockBySelector($declarationBlock1->getSelectors(), true); + + self::assertSame([], $subject->getContents()); + } + + /** + * @test + */ + public function removeDeclarationBlockBySelectorRemovesMultipleBlocksWithOutsourcedSelectors(): void + { + $subject = new ConcreteCSSList(); + $declarationBlock1 = new DeclarationBlock(); + $declarationBlock1->setSelectors(['html', 'body']); + $declarationBlock2 = new DeclarationBlock(); + $declarationBlock2->setSelectors(['html', 'body']); + $subject->setContents([$declarationBlock1, $declarationBlock2]); + self::assertNotSame([], $subject->getContents()); // make sure contents are set + + $subject->removeDeclarationBlockBySelector([new Selector('html'), new Selector('body')], true); + + self::assertSame([], $subject->getContents()); + } + + /** + * @test + */ + public function removeDeclarationBlockBySelectorRemovesMultipleBlocksWithStringSelectorsProvided(): void + { + $subject = new ConcreteCSSList(); + $declarationBlock1 = new DeclarationBlock(); + $declarationBlock1->setSelectors(['html', 'body']); + $declarationBlock2 = new DeclarationBlock(); + $declarationBlock2->setSelectors(['html', 'body']); + $subject->setContents([$declarationBlock1, $declarationBlock2]); + self::assertNotSame([], $subject->getContents()); // make sure contents are set + + $subject->removeDeclarationBlockBySelector(['html', 'body'], true); + + self::assertSame([], $subject->getContents()); + } +} diff --git a/tests/Unit/CSSList/DocumentTest.php b/tests/Unit/CSSList/DocumentTest.php new file mode 100644 index 000000000..77e8aa81c --- /dev/null +++ b/tests/Unit/CSSList/DocumentTest.php @@ -0,0 +1,66 @@ +isRootList()); + } +} diff --git a/tests/Unit/CSSList/Fixtures/ConcreteCSSBlockList.php b/tests/Unit/CSSList/Fixtures/ConcreteCSSBlockList.php new file mode 100644 index 000000000..956c7036a --- /dev/null +++ b/tests/Unit/CSSList/Fixtures/ConcreteCSSBlockList.php @@ -0,0 +1,21 @@ +getLineNumber()); + } + + /** + * @test + */ + public function getLineNumberReturnsLineNumberProvidedToConstructor(): void + { + $lineNumber = 42; + $subject = new KeyFrame($lineNumber); + + self::assertSame($lineNumber, $subject->getLineNumber()); + } + + /** + * @test + */ + public function getAnimationNameByDefaultReturnsNone(): void + { + $subject = new KeyFrame(); + + self::assertSame('none', $subject->getAnimationName()); + } + + /** + * @test + */ + public function getVendorKeyFrameByDefaultReturnsKeyframes(): void + { + $subject = new KeyFrame(); + + self::assertSame('keyframes', $subject->getVendorKeyFrame()); + } +} diff --git a/tests/Unit/Comment/CommentContainerTest.php b/tests/Unit/Comment/CommentContainerTest.php new file mode 100644 index 000000000..d0e844a45 --- /dev/null +++ b/tests/Unit/Comment/CommentContainerTest.php @@ -0,0 +1,236 @@ +subject = new ConcreteCommentContainer(); + } + + /** + * @test + */ + public function getCommentsInitiallyReturnsEmptyArray(): void + { + self::assertSame([], $this->subject->getComments()); + } + + /** + * @return array}> + */ + public function provideCommentArray(): array + { + return [ + 'no comment' => [[]], + 'one comment' => [[new Comment('Is this really a spoon?')]], + 'two comments' => [ + [ + new Comment('I’m a teapot.'), + new Comment('I’m a cafetière.'), + ], + ], + ]; + } + + /** + * @test + * + * @param list $comments + * + * @dataProvider provideCommentArray + */ + public function addCommentsOnVirginContainerAddsCommentsProvided(array $comments): void + { + $this->subject->addComments($comments); + + self::assertSame($comments, $this->subject->getComments()); + } + + /** + * @test + * + * @param list $comments + * + * @dataProvider provideCommentArray + */ + public function addCommentsWithEmptyArrayKeepsOriginalCommentsUnchanged(array $comments): void + { + $this->subject->setComments($comments); + + $this->subject->addComments([]); + + self::assertSame($comments, $this->subject->getComments()); + } + + /** + * @return array}> + */ + public function provideAlternativeCommentArray(): array + { + return [ + 'no comment' => [[]], + 'one comment' => [[new Comment('Can I eat it with my hands?')]], + 'two comments' => [ + [ + new Comment('I’m a beer barrel.'), + new Comment('I’m a vineyard.'), + ], + ], + ]; + } + + /** + * @return array}> + */ + public function provideAlternativeNonemptyCommentArray(): array + { + $data = $this->provideAlternativeCommentArray(); + + unset($data['no comment']); + + return $data; + } + + /** + * This provider crosses two comment arrays (0, 1 or 2 comments) with different comments, + * so that all combinations can be tested. + * + * @return DataProvider, 1: list}> + */ + public function provideTwoDistinctCommentArrays(): DataProvider + { + return DataProvider::cross($this->provideCommentArray(), $this->provideAlternativeCommentArray()); + } + + /** + * @return DataProvider, 1: non-empty-list}> + */ + public function provideTwoDistinctCommentArraysWithSecondNonempty(): DataProvider + { + return DataProvider::cross($this->provideCommentArray(), $this->provideAlternativeNonemptyCommentArray()); + } + + private static function createContainsConstraint(Comment $comment): TraversableContains + { + return new TraversableContains($comment); + } + + /** + * @param non-empty-list $comments + * + * @return non-empty-list + */ + private static function createContainsConstraints(array $comments): array + { + return \array_map([self::class, 'createContainsConstraint'], $comments); + } + + /** + * @test + * + * @param list $commentsToAdd + * @param non-empty-list $originalComments + * + * @dataProvider provideTwoDistinctCommentArraysWithSecondNonempty + */ + public function addCommentsKeepsOriginalComments(array $commentsToAdd, array $originalComments): void + { + $this->subject->setComments($originalComments); + + $this->subject->addComments($commentsToAdd); + + self::assertThat( + $this->subject->getComments(), + LogicalAnd::fromConstraints(...self::createContainsConstraints($originalComments)) + ); + } + + /** + * @test + * + * @param list $originalComments + * @param non-empty-list $commentsToAdd + * + * @dataProvider provideTwoDistinctCommentArraysWithSecondNonempty + */ + public function addCommentsAfterCommentsSetAddsCommentsProvided(array $originalComments, array $commentsToAdd): void + { + $this->subject->setComments($originalComments); + + $this->subject->addComments($commentsToAdd); + + self::assertThat( + $this->subject->getComments(), + LogicalAnd::fromConstraints(...self::createContainsConstraints($commentsToAdd)) + ); + } + + /** + * @test + * + * @param non-empty-list $comments + * + * @dataProvider provideAlternativeNonemptyCommentArray + */ + public function addCommentsAppends(array $comments): void + { + $firstComment = new Comment('I must be first!'); + $this->subject->setComments([$firstComment]); + + $this->subject->addComments($comments); + + $result = $this->subject->getComments(); + self::assertNotEmpty($result); + self::assertSame($firstComment, $result[0]); + } + + /** + * @test + * + * @param list $comments + * + * @dataProvider provideCommentArray + */ + public function setCommentsOnVirginContainerSetsCommentsProvided(array $comments): void + { + $this->subject->setComments($comments); + + self::assertSame($comments, $this->subject->getComments()); + } + + /** + * @test + * + * @param list $originalComments + * @param list $commentsToSet + * + * @dataProvider provideTwoDistinctCommentArrays + */ + public function setCommentsReplacesWithCommentsProvided(array $originalComments, array $commentsToSet): void + { + $this->subject->setComments($originalComments); + + $this->subject->setComments($commentsToSet); + + self::assertSame($commentsToSet, $this->subject->getComments()); + } +} diff --git a/tests/Unit/Comment/CommentTest.php b/tests/Unit/Comment/CommentTest.php new file mode 100644 index 000000000..69572e903 --- /dev/null +++ b/tests/Unit/Comment/CommentTest.php @@ -0,0 +1,80 @@ +getComment()); + } + + /** + * @test + */ + public function getCommentInitiallyReturnsCommentPassedToConstructor(): void + { + $comment = 'There is no spoon.'; + $subject = new Comment($comment); + + self::assertSame($comment, $subject->getComment()); + } + + /** + * @test + */ + public function setCommentSetsComments(): void + { + $comment = 'There is no spoon.'; + $subject = new Comment(); + + $subject->setComment($comment); + + self::assertSame($comment, $subject->getComment()); + } + + /** + * @test + */ + public function getLineNumberByDefaultReturnsNull(): void + { + $subject = new Comment(); + + self::assertNull($subject->getLineNumber()); + } + + /** + * @test + */ + public function getLineNumberReturnsLineNumberProvidedToConstructor(): void + { + $lineNumber = 42; + $subject = new Comment('', $lineNumber); + + self::assertSame($lineNumber, $subject->getLineNumber()); + } +} diff --git a/tests/Unit/Comment/Fixtures/ConcreteCommentContainer.php b/tests/Unit/Comment/Fixtures/ConcreteCommentContainer.php new file mode 100644 index 000000000..39f6ec37f --- /dev/null +++ b/tests/Unit/Comment/Fixtures/ConcreteCommentContainer.php @@ -0,0 +1,13 @@ +subject = new OutputFormat(); + } + + /** + * @test + */ + public function getStringQuotingTypeInitiallyReturnsDoubleQuote(): void + { + self::assertSame('"', $this->subject->getStringQuotingType()); + } + + /** + * @test + */ + public function setStringQuotingTypeSetsStringQuotingType(): void + { + $value = "'"; + $this->subject->setStringQuotingType($value); + + self::assertSame($value, $this->subject->getStringQuotingType()); + } + + /** + * @test + */ + public function setStringQuotingTypeProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setStringQuotingType('"')); + } + + /** + * @test + */ + public function usesRgbHashNotationInitiallyReturnsTrue(): void + { + self::assertTrue($this->subject->usesRgbHashNotation()); + } + + /** + * @return array + */ + public static function provideBooleans(): array + { + return [ + 'true' => [true], + 'false' => [false], + ]; + } + + /** + * @test + * + * @dataProvider provideBooleans + */ + public function setRGBHashNotationSetsRGBHashNotation(bool $value): void + { + $this->subject->setRGBHashNotation($value); + + self::assertSame($value, $this->subject->usesRgbHashNotation()); + } + + /** + * @test + */ + public function setRGBHashNotationProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setRGBHashNotation(true)); + } + + /** + * @test + */ + public function shouldRenderSemicolonAfterLastRuleInitiallyReturnsTrue(): void + { + self::assertTrue($this->subject->shouldRenderSemicolonAfterLastRule()); + } + + /** + * @test + * + * @dataProvider provideBooleans + */ + public function setSemicolonAfterLastRuleSetsSemicolonAfterLastRule(bool $value): void + { + $this->subject->setSemicolonAfterLastRule($value); + + self::assertSame($value, $this->subject->shouldRenderSemicolonAfterLastRule()); + } + + /** + * @test + */ + public function setSemicolonAfterLastRuleProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSemicolonAfterLastRule(true)); + } + + /** + * @test + */ + public function getSpaceAfterRuleNameInitiallyReturnsSingleSpace(): void + { + self::assertSame(' ', $this->subject->getSpaceAfterRuleName()); + } + + /** + * @test + */ + public function setSpaceAfterRuleNameSetsSpaceAfterRuleName(): void + { + $value = "\n"; + $this->subject->setSpaceAfterRuleName($value); + + self::assertSame($value, $this->subject->getSpaceAfterRuleName()); + } + + /** + * @test + */ + public function setSpaceAfterRuleNameProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSpaceAfterRuleName("\n")); + } + + /** + * @test + */ + public function getSpaceBeforeRulesInitiallyReturnsEmptyString(): void + { + self::assertSame('', $this->subject->getSpaceBeforeRules()); + } + + /** + * @test + */ + public function setSpaceBeforeRulesSetsSpaceBeforeRules(): void + { + $value = ' '; + $this->subject->setSpaceBeforeRules($value); + + self::assertSame($value, $this->subject->getSpaceBeforeRules()); + } + + /** + * @test + */ + public function setSpaceBeforeRulesProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSpaceBeforeRules(' ')); + } + + /** + * @test + */ + public function getSpaceAfterRulesInitiallyReturnsEmptyString(): void + { + self::assertSame('', $this->subject->getSpaceAfterRules()); + } + + /** + * @test + */ + public function setSpaceAfterRulesSetsSpaceAfterRules(): void + { + $value = ' '; + $this->subject->setSpaceAfterRules($value); + + self::assertSame($value, $this->subject->getSpaceAfterRules()); + } + + /** + * @test + */ + public function setSpaceAfterRulesProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSpaceAfterRules(' ')); + } + + /** + * @test + */ + public function getSpaceBetweenRulesInitiallyReturnsEmptyString(): void + { + self::assertSame('', $this->subject->getSpaceBetweenRules()); + } + + /** + * @test + */ + public function setSpaceBetweenRulesSetsSpaceBetweenRules(): void + { + $value = ' '; + $this->subject->setSpaceBetweenRules($value); + + self::assertSame($value, $this->subject->getSpaceBetweenRules()); + } + + /** + * @test + */ + public function setSpaceBetweenRulesProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSpaceBetweenRules(' ')); + } + + /** + * @test + */ + public function getSpaceBeforeBlocksInitiallyReturnsEmptyString(): void + { + self::assertSame('', $this->subject->getSpaceBeforeBlocks()); + } + + /** + * @test + */ + public function setSpaceBeforeBlocksSetsSpaceBeforeBlocks(): void + { + $value = ' '; + $this->subject->setSpaceBeforeBlocks($value); + + self::assertSame($value, $this->subject->getSpaceBeforeBlocks()); + } + + /** + * @test + */ + public function setSpaceBeforeBlocksProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSpaceBeforeBlocks(' ')); + } + + /** + * @test + */ + public function getSpaceAfterBlocksInitiallyReturnsEmptyString(): void + { + self::assertSame('', $this->subject->getSpaceAfterBlocks()); + } + + /** + * @test + */ + public function setSpaceAfterBlocksSetsSpaceAfterBlocks(): void + { + $value = ' '; + $this->subject->setSpaceAfterBlocks($value); + + self::assertSame($value, $this->subject->getSpaceAfterBlocks()); + } + + /** + * @test + */ + public function setSpaceAfterBlocksProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSpaceAfterBlocks(' ')); + } + + /** + * @test + */ + public function getSpaceBetweenBlocksInitiallyReturnsNewline(): void + { + self::assertSame("\n", $this->subject->getSpaceBetweenBlocks()); + } + + /** + * @test + */ + public function setSpaceBetweenBlocksSetsSpaceBetweenBlocks(): void + { + $value = ' '; + $this->subject->setSpaceBetweenBlocks($value); + + self::assertSame($value, $this->subject->getSpaceBetweenBlocks()); + } + + /** + * @test + */ + public function setSpaceBetweenBlocksProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSpaceBetweenBlocks(' ')); + } + + /** + * @test + */ + public function getContentBeforeAtRuleBlockInitiallyReturnsEmptyString(): void + { + self::assertSame('', $this->subject->getContentBeforeAtRuleBlock()); + } + + /** + * @test + */ + public function setBeforeAtRuleBlockSetsBeforeAtRuleBlock(): void + { + $value = ' '; + $this->subject->setBeforeAtRuleBlock($value); + + self::assertSame($value, $this->subject->getContentBeforeAtRuleBlock()); + } + + /** + * @test + */ + public function setBeforeAtRuleBlockProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setBeforeAtRuleBlock(' ')); + } + + /** + * @test + */ + public function getContentAfterAtRuleBlockInitiallyReturnsEmptyString(): void + { + self::assertSame('', $this->subject->getContentAfterAtRuleBlock()); + } + + /** + * @test + */ + public function setAfterAtRuleBlockSetsAfterAtRuleBlock(): void + { + $value = ' '; + $this->subject->setAfterAtRuleBlock($value); + + self::assertSame($value, $this->subject->getContentAfterAtRuleBlock()); + } + + /** + * @test + */ + public function setAfterAtRuleBlockProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setAfterAtRuleBlock(' ')); + } + + /** + * @test + */ + public function getSpaceBeforeSelectorSeparatorInitiallyReturnsEmptyString(): void + { + self::assertSame('', $this->subject->getSpaceBeforeSelectorSeparator()); + } + + /** + * @test + */ + public function setSpaceBeforeSelectorSeparatorSetsSpaceBeforeSelectorSeparator(): void + { + $value = ' '; + $this->subject->setSpaceBeforeSelectorSeparator($value); + + self::assertSame($value, $this->subject->getSpaceBeforeSelectorSeparator()); + } + + /** + * @test + */ + public function setSpaceBeforeSelectorSeparatorProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSpaceBeforeSelectorSeparator(' ')); + } + + /** + * @test + */ + public function getSpaceAfterSelectorSeparatorInitiallyReturnsSpace(): void + { + self::assertSame(' ', $this->subject->getSpaceAfterSelectorSeparator()); + } + + /** + * @test + */ + public function setSpaceAfterSelectorSeparatorSetsSpaceAfterSelectorSeparator(): void + { + $value = ' '; + $this->subject->setSpaceAfterSelectorSeparator($value); + + self::assertSame($value, $this->subject->getSpaceAfterSelectorSeparator()); + } + + /** + * @test + */ + public function setSpaceAfterSelectorSeparatorProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSpaceAfterSelectorSeparator(' ')); + } + + /** + * @test + */ + public function getSpaceBeforeListArgumentSeparatorInitiallyReturnsEmptyString(): void + { + self::assertSame('', $this->subject->getSpaceBeforeListArgumentSeparator()); + } + + /** + * @test + */ + public function setSpaceBeforeListArgumentSeparatorSetsSpaceBeforeListArgumentSeparator(): void + { + $value = ' '; + $this->subject->setSpaceBeforeListArgumentSeparator($value); + + self::assertSame($value, $this->subject->getSpaceBeforeListArgumentSeparator()); + } + + /** + * @test + */ + public function setSpaceBeforeListArgumentSeparatorProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSpaceBeforeListArgumentSeparator(' ')); + } + + /** + * @test + */ + public function getSpaceBeforeListArgumentSeparatorsInitiallyReturnsEmptyArray(): void + { + self::assertSame([], $this->subject->getSpaceBeforeListArgumentSeparators()); + } + + /** + * @test + */ + public function setSpaceBeforeListArgumentSeparatorsSetsSpaceBeforeListArgumentSeparators(): void + { + $value = ['/' => ' ']; + $this->subject->setSpaceBeforeListArgumentSeparators($value); + + self::assertSame($value, $this->subject->getSpaceBeforeListArgumentSeparators()); + } + + /** + * @test + */ + public function setSpaceBeforeListArgumentSeparatorsProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSpaceBeforeListArgumentSeparators([])); + } + + /** + * @test + */ + public function getSpaceAfterListArgumentSeparatorInitiallyReturnsEmptyString(): void + { + self::assertSame('', $this->subject->getSpaceAfterListArgumentSeparator()); + } + + /** + * @test + */ + public function setSpaceAfterListArgumentSeparatorSetsSpaceAfterListArgumentSeparator(): void + { + $value = ' '; + $this->subject->setSpaceAfterListArgumentSeparator($value); + + self::assertSame($value, $this->subject->getSpaceAfterListArgumentSeparator()); + } + + /** + * @test + */ + public function setSpaceAfterListArgumentSeparatorProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSpaceAfterListArgumentSeparator(' ')); + } + + /** + * @test + */ + public function getSpaceAfterListArgumentSeparatorsInitiallyReturnsEmptyArray(): void + { + self::assertSame([], $this->subject->getSpaceAfterListArgumentSeparators()); + } + + /** + * @test + */ + public function setSpaceAfterListArgumentSeparatorsSetsSpaceAfterListArgumentSeparators(): void + { + $value = [',' => ' ']; + $this->subject->setSpaceAfterListArgumentSeparators($value); + + self::assertSame($value, $this->subject->getSpaceAfterListArgumentSeparators()); + } + + /** + * @test + */ + public function setSpaceAfterListArgumentSeparatorsProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSpaceAfterListArgumentSeparators([])); + } + + /** + * @test + */ + public function getSpaceBeforeOpeningBraceInitiallyReturnsSpace(): void + { + self::assertSame(' ', $this->subject->getSpaceBeforeOpeningBrace()); + } + + /** + * @test + */ + public function setSpaceBeforeOpeningBraceSetsSpaceBeforeOpeningBrace(): void + { + $value = "\t"; + $this->subject->setSpaceBeforeOpeningBrace($value); + + self::assertSame($value, $this->subject->getSpaceBeforeOpeningBrace()); + } + + /** + * @test + */ + public function setSpaceBeforeOpeningBraceProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setSpaceBeforeOpeningBrace(' ')); + } + + /** + * @test + */ + public function getContentBeforeDeclarationBlockInitiallyReturnsEmptyString(): void + { + self::assertSame('', $this->subject->getContentBeforeDeclarationBlock()); + } + + /** + * @test + */ + public function setBeforeDeclarationBlockSetsBeforeDeclarationBlock(): void + { + $value = ' '; + $this->subject->setBeforeDeclarationBlock($value); + + self::assertSame($value, $this->subject->getContentBeforeDeclarationBlock()); + } + + /** + * @test + */ + public function setBeforeDeclarationBlockProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setBeforeDeclarationBlock(' ')); + } + + /** + * @test + */ + public function getContentAfterDeclarationBlockSelectorsInitiallyReturnsEmptyString(): void + { + self::assertSame('', $this->subject->getContentAfterDeclarationBlockSelectors()); + } + + /** + * @test + */ + public function setAfterDeclarationBlockSelectorsSetsAfterDeclarationBlockSelectors(): void + { + $value = ' '; + $this->subject->setAfterDeclarationBlockSelectors($value); + + self::assertSame($value, $this->subject->getContentAfterDeclarationBlockSelectors()); + } + + /** + * @test + */ + public function setAfterDeclarationBlockSelectorsProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setAfterDeclarationBlockSelectors(' ')); + } + + /** + * @test + */ + public function getContentAfterDeclarationBlockInitiallyReturnsEmptyString(): void + { + self::assertSame('', $this->subject->getContentAfterDeclarationBlock()); + } + + /** + * @test + */ + public function setAfterDeclarationBlockSetsAfterDeclarationBlock(): void + { + $value = ' '; + $this->subject->setAfterDeclarationBlock($value); + + self::assertSame($value, $this->subject->getContentAfterDeclarationBlock()); + } + + /** + * @test + */ + public function setAfterDeclarationBlockProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setAfterDeclarationBlock(' ')); + } + + /** + * @test + */ + public function getIndentationInitiallyReturnsTab(): void + { + self::assertSame("\t", $this->subject->getIndentation()); + } + + /** + * @test + */ + public function setIndentationSetsIndentation(): void + { + $value = ' '; + $this->subject->setIndentation($value); + + self::assertSame($value, $this->subject->getIndentation()); + } + + /** + * @test + */ + public function setIndentationProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setIndentation(' ')); + } + + /** + * @test + */ + public function shouldIgnoreExceptionsInitiallyReturnsFalse(): void + { + self::assertFalse($this->subject->shouldIgnoreExceptions()); + } + + /** + * @test + * + * @dataProvider provideBooleans + */ + public function setIgnoreExceptionsSetsIgnoreExceptions(bool $value): void + { + $this->subject->setIgnoreExceptions($value); + + self::assertSame($value, $this->subject->shouldIgnoreExceptions()); + } + + /** + * @test + */ + public function setIgnoreExceptionsProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setIgnoreExceptions(true)); + } + + /** + * @test + */ + public function shouldRenderCommentsInitiallyReturnsFalse(): void + { + self::assertFalse($this->subject->shouldRenderComments()); + } + + /** + * @test + * + * @dataProvider provideBooleans + */ + public function setRenderCommentsSetsRenderComments(bool $value): void + { + $this->subject->setRenderComments($value); + + self::assertSame($value, $this->subject->shouldRenderComments()); + } + + /** + * @test + */ + public function setRenderCommentsProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->setRenderComments(true)); + } + + /** + * @test + */ + public function getIndentationLevelInitiallyReturnsZero(): void + { + self::assertSame(0, $this->subject->getIndentationLevel()); + } + + /** + * @test + */ + public function indentWithTabsByDefaultSetsIndentationToOneTab(): void + { + $this->subject->indentWithTabs(); + + self::assertSame("\t", $this->subject->getIndentation()); + } + + /** + * @return array, 1: string}> + */ + public static function provideTabIndentation(): array + { + return [ + 'zero tabs' => [0, ''], + 'one tab' => [1, "\t"], + 'two tabs' => [2, "\t\t"], + 'three tabs' => [3, "\t\t\t"], + ]; + } + + /** + * @test + * @dataProvider provideTabIndentation + */ + public function indentWithTabsSetsIndentationToTheProvidedNumberOfTabs( + int $numberOfTabs, + string $expectedIndentation + ): void { + $this->subject->indentWithTabs($numberOfTabs); + + self::assertSame($expectedIndentation, $this->subject->getIndentation()); + } + + /** + * @test + */ + public function indentWithTabsProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->indentWithTabs()); + } + + /** + * @test + */ + public function indentWithSpacesByDefaultSetsIndentationToTwoSpaces(): void + { + $this->subject->indentWithSpaces(); + + self::assertSame(' ', $this->subject->getIndentation()); + } + + /** + * @return array, 1: string}> + */ + public static function provideSpaceIndentation(): array + { + return [ + 'zero spaces' => [0, ''], + 'one space' => [1, ' '], + 'two spaces' => [2, ' '], + 'three spaces' => [3, ' '], + 'four spaces' => [4, ' '], + ]; + } + + /** + * @test + * @dataProvider provideSpaceIndentation + */ + public function indentWithSpacesSetsIndentationToTheProvidedNumberOfSpaces( + int $numberOfSpaces, + string $expectedIndentation + ): void { + $this->subject->indentWithSpaces($numberOfSpaces); + + self::assertSame($expectedIndentation, $this->subject->getIndentation()); + } + + /** + * @test + */ + public function indentWithSpacesProvidesFluentInterface(): void + { + self::assertSame($this->subject, $this->subject->indentWithSpaces()); + } + + /** + * @test + */ + public function nextLevelReturnsOutputFormatInstance(): void + { + self::assertInstanceOf(OutputFormat::class, $this->subject->nextLevel()); + } + + /** + * @test + */ + public function nextLevelReturnsDifferentInstance(): void + { + self::assertNotSame($this->subject, $this->subject->nextLevel()); + } + + /** + * @test + */ + public function nextLevelReturnsCloneWithSameProperties(): void + { + $space = ' '; + $this->subject->setSpaceAfterRuleName($space); + + self::assertSame($space, $this->subject->nextLevel()->getSpaceAfterRuleName()); + } + + /** + * @test + */ + public function nextLevelReturnsInstanceWithIndentationLevelIncreasedByOne(): void + { + $originalIndentationLevel = $this->subject->getIndentationLevel(); + + self::assertSame($originalIndentationLevel + 1, $this->subject->nextLevel()->getIndentationLevel()); + } + + /** + * @test + */ + public function nextLevelReturnsInstanceWithDifferentFormatterInstance(): void + { + $formatter = $this->subject->getFormatter(); + + self::assertNotSame($formatter, $this->subject->nextLevel()->getFormatter()); + } + + /** + * @test + */ + public function beLenientSetsIgnoreExceptionsToTrue(): void + { + $this->subject->setIgnoreExceptions(false); + + $this->subject->beLenient(); + + self::assertTrue($this->subject->shouldIgnoreExceptions()); + } + + /** + * @test + */ + public function getFormatterReturnsOutputFormatterInstance(): void + { + self::assertInstanceOf(OutputFormatter::class, $this->subject->getFormatter()); + } + + /** + * @test + */ + public function getFormatterCalledTwoTimesReturnsSameInstance(): void + { + $firstCallResult = $this->subject->getFormatter(); + $secondCallResult = $this->subject->getFormatter(); + + self::assertSame($firstCallResult, $secondCallResult); + } + + /** + * @test + */ + public function createReturnsOutputFormatInstance(): void + { + self::assertInstanceOf(OutputFormat::class, OutputFormat::create()); + } + + /** + * @test + */ + public function createCreatesInstanceWithDefaultSettings(): void + { + self::assertEquals(new OutputFormat(), OutputFormat::create()); + } + + /** + * @test + */ + public function createCalledTwoTimesReturnsDifferentInstances(): void + { + $firstCallResult = OutputFormat::create(); + $secondCallResult = OutputFormat::create(); + + self::assertNotSame($firstCallResult, $secondCallResult); + } + + /** + * @test + */ + public function createCompactReturnsOutputFormatInstance(): void + { + self::assertInstanceOf(OutputFormat::class, OutputFormat::createCompact()); + } + + /** + * @test + */ + public function createCompactCalledTwoTimesReturnsDifferentInstances(): void + { + $firstCallResult = OutputFormat::createCompact(); + $secondCallResult = OutputFormat::createCompact(); + + self::assertNotSame($firstCallResult, $secondCallResult); + } + + /** + * @test + */ + public function createCompactReturnsInstanceWithSpaceBeforeRulesSetToEmptyString(): void + { + $newInstance = OutputFormat::createCompact(); + + self::assertSame('', $newInstance->getSpaceBeforeRules()); + } + + /** + * @test + */ + public function createCompactReturnsInstanceWithSpaceBetweenRulesSetToEmptyString(): void + { + $newInstance = OutputFormat::createCompact(); + + self::assertSame('', $newInstance->getSpaceBetweenRules()); + } + + /** + * @test + */ + public function createCompactReturnsInstanceWithSpaceAfterRulesSetToEmptyString(): void + { + $newInstance = OutputFormat::createCompact(); + + self::assertSame('', $newInstance->getSpaceAfterRules()); + } + + /** + * @test + */ + public function createCompactReturnsInstanceWithSpaceBeforeBlocksSetToEmptyString(): void + { + $newInstance = OutputFormat::createCompact(); + + self::assertSame('', $newInstance->getSpaceBeforeBlocks()); + } + + /** + * @test + */ + public function createCompactReturnsInstanceWithSpaceBetweenBlocksSetToEmptyString(): void + { + $newInstance = OutputFormat::createCompact(); + + self::assertSame('', $newInstance->getSpaceBetweenBlocks()); + } + + /** + * @test + */ + public function createCompactReturnsInstanceWithSpaceAfterBlocksSetToEmptyString(): void + { + $newInstance = OutputFormat::createCompact(); + + self::assertSame('', $newInstance->getSpaceAfterBlocks()); + } + + /** + * @test + */ + public function createCompactReturnsInstanceWithSpaceAfterRuleNameSetToEmptyString(): void + { + $newInstance = OutputFormat::createCompact(); + + self::assertSame('', $newInstance->getSpaceAfterRuleName()); + } + + /** + * @test + */ + public function createCompactReturnsInstanceWithSpaceBeforeOpeningBraceSetToEmptyString(): void + { + $newInstance = OutputFormat::createCompact(); + + self::assertSame('', $newInstance->getSpaceBeforeOpeningBrace()); + } + + /** + * @test + */ + public function createCompactReturnsInstanceWithSpaceAfterSelectorSeparatorSetToEmptyString(): void + { + $newInstance = OutputFormat::createCompact(); + + self::assertSame('', $newInstance->getSpaceAfterSelectorSeparator()); + } + + /** + * @test + */ + public function createCompactReturnsInstanceWithSpaceAfterListArgumentSeparatorsSetToEmptyArray(): void + { + $newInstance = OutputFormat::createCompact(); + + self::assertSame([], $newInstance->getSpaceAfterListArgumentSeparators()); + } + + /** + * @test + */ + public function createCompactReturnsInstanceWithRenderSemicolonAfterLastRuleDisabled(): void + { + $newInstance = OutputFormat::createCompact(); + + self::assertFalse($newInstance->shouldRenderSemicolonAfterLastRule()); + } + + /** + * @test + */ + public function createCompactReturnsInstanceWithRenderCommentsDisabled(): void + { + $newInstance = OutputFormat::createCompact(); + + self::assertFalse($newInstance->shouldRenderComments()); + } + + /** + * @test + */ + public function createPrettyReturnsOutputFormatInstance(): void + { + self::assertInstanceOf(OutputFormat::class, OutputFormat::createPretty()); + } + + /** + * @test + */ + public function createPrettyCalledTwoTimesReturnsDifferentInstances(): void + { + $firstCallResult = OutputFormat::createPretty(); + $secondCallResult = OutputFormat::createPretty(); + + self::assertNotSame($firstCallResult, $secondCallResult); + } + + /** + * @test + */ + public function createPrettyReturnsInstanceWithSpaceBeforeRulesSetToNewline(): void + { + $newInstance = OutputFormat::createPretty(); + + self::assertSame("\n", $newInstance->getSpaceBeforeRules()); + } + + /** + * @test + */ + public function createPrettyReturnsInstanceWithSpaceBetweenRulesSetToNewline(): void + { + $newInstance = OutputFormat::createPretty(); + + self::assertSame("\n", $newInstance->getSpaceBetweenRules()); + } + + /** + * @test + */ + public function createPrettyReturnsInstanceWithSpaceAfterRulesSetToNewline(): void + { + $newInstance = OutputFormat::createPretty(); + + self::assertSame("\n", $newInstance->getSpaceAfterRules()); + } + + /** + * @test + */ + public function createPrettyReturnsInstanceWithSpaceBeforeBlocksSetToNewline(): void + { + $newInstance = OutputFormat::createPretty(); + + self::assertSame("\n", $newInstance->getSpaceBeforeBlocks()); + } + + /** + * @test + */ + public function createPrettyReturnsInstanceWithSpaceBetweenBlocksSetToTwoNewlines(): void + { + $newInstance = OutputFormat::createPretty(); + + self::assertSame("\n\n", $newInstance->getSpaceBetweenBlocks()); + } + + /** + * @test + */ + public function createPrettyReturnsInstanceWithSpaceAfterBlocksSetToNewline(): void + { + $newInstance = OutputFormat::createPretty(); + + self::assertSame("\n", $newInstance->getSpaceAfterBlocks()); + } + + /** + * @test + */ + public function createPrettyReturnsInstanceWithSpaceAfterRuleNameSetToSpace(): void + { + $newInstance = OutputFormat::createPretty(); + + self::assertSame(' ', $newInstance->getSpaceAfterRuleName()); + } + + /** + * @test + */ + public function createPrettyReturnsInstanceWithSpaceBeforeOpeningBraceSetToSpace(): void + { + $newInstance = OutputFormat::createPretty(); + + self::assertSame(' ', $newInstance->getSpaceBeforeOpeningBrace()); + } + + /** + * @test + */ + public function createPrettyReturnsInstanceWithSpaceAfterSelectorSeparatorSetToSpace(): void + { + $newInstance = OutputFormat::createPretty(); + + self::assertSame(' ', $newInstance->getSpaceAfterSelectorSeparator()); + } + + /** + * @test + */ + public function createPrettyReturnsInstanceWithSpaceAfterListArgumentSeparatorsSetToSpaceForCommaOnly(): void + { + $newInstance = OutputFormat::createPretty(); + + self::assertSame([',' => ' '], $newInstance->getSpaceAfterListArgumentSeparators()); + } + + /** + * @test + */ + public function createPrettyReturnsInstanceWithRenderCommentsEnabled(): void + { + $newInstance = OutputFormat::createPretty(); + + self::assertTrue($newInstance->shouldRenderComments()); + } +} diff --git a/tests/Unit/OutputFormatterTest.php b/tests/Unit/OutputFormatterTest.php new file mode 100644 index 000000000..2caf30e40 --- /dev/null +++ b/tests/Unit/OutputFormatterTest.php @@ -0,0 +1,622 @@ +outputFormat = new OutputFormat(); + $this->subject = new OutputFormatter($this->outputFormat); + } + + /** + * @test + */ + public function spaceAfterRuleNameReturnsSpaceAfterRuleNameFromOutputFormat(): void + { + $space = ' '; + $this->outputFormat->setSpaceAfterRuleName($space); + + self::assertSame($space, $this->subject->spaceAfterRuleName()); + } + + /** + * @test + */ + public function spaceBeforeRulesReturnsSpaceBeforeRulesFromOutputFormat(): void + { + $space = ' '; + $this->outputFormat->setSpaceBeforeRules($space); + + self::assertSame($space, $this->subject->spaceBeforeRules()); + } + + /** + * @test + */ + public function spaceAfterRulesReturnsSpaceAfterRulesFromOutputFormat(): void + { + $space = ' '; + $this->outputFormat->setSpaceAfterRules($space); + + self::assertSame($space, $this->subject->spaceAfterRules()); + } + + /** + * @test + */ + public function spaceBetweenRulesReturnsSpaceBetweenRulesFromOutputFormat(): void + { + $space = ' '; + $this->outputFormat->setSpaceBetweenRules($space); + + self::assertSame($space, $this->subject->spaceBetweenRules()); + } + + /** + * @test + */ + public function spaceBeforeBlocksReturnsSpaceBeforeBlocksFromOutputFormat(): void + { + $space = ' '; + $this->outputFormat->setSpaceBeforeBlocks($space); + + self::assertSame($space, $this->subject->spaceBeforeBlocks()); + } + + /** + * @test + */ + public function spaceAfterBlocksReturnsSpaceAfterBlocksFromOutputFormat(): void + { + $space = ' '; + $this->outputFormat->setSpaceAfterBlocks($space); + + self::assertSame($space, $this->subject->spaceAfterBlocks()); + } + + /** + * @test + */ + public function spaceBetweenBlocksReturnsSpaceBetweenBlocksFromOutputFormat(): void + { + $space = ' '; + $this->outputFormat->setSpaceBetweenBlocks($space); + + self::assertSame($space, $this->subject->spaceBetweenBlocks()); + } + + /** + * @test + */ + public function spaceBeforeSelectorSeparatorReturnsSpaceBeforeSelectorSeparatorFromOutputFormat(): void + { + $space = ' '; + $this->outputFormat->setSpaceBeforeSelectorSeparator($space); + + self::assertSame($space, $this->subject->spaceBeforeSelectorSeparator()); + } + + /** + * @test + */ + public function spaceAfterSelectorSeparatorReturnsSpaceAfterSelectorSeparatorFromOutputFormat(): void + { + $space = ' '; + $this->outputFormat->setSpaceAfterSelectorSeparator($space); + + self::assertSame($space, $this->subject->spaceAfterSelectorSeparator()); + } + + /** + * @test + */ + public function spaceBeforeListArgumentSeparatorReturnsSpaceSetForSpecificSeparator(): void + { + $separator = ','; + $space = ' '; + $this->outputFormat->setSpaceBeforeListArgumentSeparators([$separator => $space]); + $defaultSpace = "\t\t\t\t"; + $this->outputFormat->setSpaceBeforeListArgumentSeparator($defaultSpace); + + self::assertSame($space, $this->subject->spaceBeforeListArgumentSeparator($separator)); + } + + /** + * @test + */ + public function spaceBeforeListArgumentSeparatorWithoutSpecificSettingReturnsDefaultSpace(): void + { + $space = ' '; + $this->outputFormat->setSpaceBeforeListArgumentSeparators([',' => $space]); + $defaultSpace = "\t\t\t\t"; + $this->outputFormat->setSpaceBeforeListArgumentSeparator($defaultSpace); + + self::assertSame($defaultSpace, $this->subject->spaceBeforeListArgumentSeparator(';')); + } + + /** + * @test + */ + public function spaceAfterListArgumentSeparatorReturnsSpaceSetForSpecificSeparator(): void + { + $separator = ','; + $space = ' '; + $this->outputFormat->setSpaceAfterListArgumentSeparators([$separator => $space]); + $defaultSpace = "\t\t\t\t"; + $this->outputFormat->setSpaceAfterListArgumentSeparator($defaultSpace); + + self::assertSame($space, $this->subject->spaceAfterListArgumentSeparator($separator)); + } + + /** + * @test + */ + public function spaceAfterListArgumentSeparatorWithoutSpecificSettingReturnsDefaultSpace(): void + { + $space = ' '; + $this->outputFormat->setSpaceAfterListArgumentSeparators([',' => $space]); + $defaultSpace = "\t\t\t\t"; + $this->outputFormat->setSpaceAfterListArgumentSeparator($defaultSpace); + + self::assertSame($defaultSpace, $this->subject->spaceAfterListArgumentSeparator(';')); + } + + /** + * @test + */ + public function spaceBeforeOpeningBraceReturnsSpaceBeforeOpeningBraceFromOutputFormat(): void + { + $space = ' '; + $this->outputFormat->setSpaceBeforeOpeningBrace($space); + + self::assertSame($space, $this->subject->spaceBeforeOpeningBrace()); + } + + /** + * @test + */ + public function implodeForEmptyValuesReturnsEmptyString(): void + { + $values = []; + + $result = $this->subject->implode(', ', $values); + + self::assertSame('', $result); + } + + /** + * @test + */ + public function implodeWithOneStringValueReturnsStringValue(): void + { + $value = 'tea'; + $values = [$value]; + + $result = $this->subject->implode(', ', $values); + + self::assertSame($value, $result); + } + + /** + * @test + */ + public function implodeWithMultipleStringValuesReturnsValuesSeparatedBySeparator(): void + { + $value1 = 'tea'; + $value2 = 'coffee'; + $values = [$value1, $value2]; + $separator = ', '; + + $result = $this->subject->implode($separator, $values); + + self::assertSame($value1 . $separator . $value2, $result); + } + + /** + * @test + */ + public function implodeWithOneRenderableReturnsRenderedRenderable(): void + { + $renderable = $this->createMock(Renderable::class); + $renderedRenderable = 'tea'; + $renderable->method('render')->with($this->outputFormat)->willReturn($renderedRenderable); + $values = [$renderable]; + + $result = $this->subject->implode(', ', $values); + + self::assertSame($renderedRenderable, $result); + } + + /** + * @test + */ + public function implodeWithMultipleRenderablesReturnsRenderedRenderablesSeparatedBySeparator(): void + { + $renderable1 = $this->createMock(Renderable::class); + $renderedRenderable1 = 'tea'; + $renderable1->method('render')->with($this->outputFormat)->willReturn($renderedRenderable1); + $renderable2 = $this->createMock(Renderable::class); + $renderedRenderable2 = 'coffee'; + $renderable2->method('render')->with($this->outputFormat)->willReturn($renderedRenderable2); + $values = [$renderable1, $renderable2]; + $separator = ', '; + + $result = $this->subject->implode($separator, $values); + + self::assertSame($renderedRenderable1 . $separator . $renderedRenderable2, $result); + } + + /** + * @test + */ + public function implodeWithIncreaseLevelFalseUsesDefaultIndentationLevelForRendering(): void + { + $renderable = $this->createMock(Renderable::class); + $renderedRenderable = 'tea'; + $renderable->method('render')->with($this->outputFormat)->willReturn($renderedRenderable); + $values = [$renderable]; + + $result = $this->subject->implode(', ', $values, false); + + self::assertSame($renderedRenderable, $result); + } + + /** + * @test + */ + public function implodeWithIncreaseLevelTrueIncreasesIndentationLevelForRendering(): void + { + $renderable = $this->createMock(Renderable::class); + $renderedRenderable = 'tea'; + $renderable->method('render')->with($this->outputFormat->nextLevel())->willReturn($renderedRenderable); + $values = [$renderable]; + + $result = $this->subject->implode(', ', $values, true); + + self::assertSame($renderedRenderable, $result); + } + + /** + * @return array + */ + public function provideUnchangedStringForRemoveLastSemicolon(): array + { + return [ + 'empty string' => [''], + 'string without semicolon' => ['earl-grey: hot'], + 'string with trailing semicolon' => ['Earl Grey: hot;'], + 'string with semicolon in the middle' => ['Earl Grey: hot; Coffee: Americano'], + 'string with semicolons in the middle and trailing' => ['Earl Grey: hot; Coffee: Americano;'], + ]; + } + + /** + * @test + * @dataProvider provideUnchangedStringForRemoveLastSemicolon + */ + public function removeLastSemicolonWithSemicolonAfterLastRuleEnabledReturnsUnchangedArgument(string $string): void + { + $this->outputFormat->setSemicolonAfterLastRule(true); + + $result = $this->subject->removeLastSemicolon($string); + + self::assertSame($string, $result); + } + + /** + * @return array + */ + public function provideChangedStringForRemoveLastSemicolon(): array + { + return [ + 'empty string' => ['', ''], + 'non-empty string without semicolon' => ['Earl Grey: hot', 'Earl Grey: hot'], + 'just 1 semicolon' => [';', ''], + 'just 2 semicolons' => [';;', ';'], + 'string with trailing semicolon' => ['Earl Grey: hot;', 'Earl Grey: hot'], + 'string with semicolon in the middle' => [ + 'Earl Grey: hot; Coffee: Americano', + 'Earl Grey: hot Coffee: Americano', + ], + 'string with semicolon in the middle and trailing' => [ + 'Earl Grey: hot; Coffee: Americano;', + 'Earl Grey: hot; Coffee: Americano', + ], + 'string with 2 semicolons in the middle' => ['tea; coffee; Club-Mate', 'tea; coffee Club-Mate'], + 'string with 2 semicolons in the middle surrounded by spaces' => [ + 'Earl Grey: hot ; Coffee: Americano ; Club-Mate: cold', + 'Earl Grey: hot ; Coffee: Americano Club-Mate: cold', + ], + 'string with 2 adjacent semicolons in the middle' => [ + 'Earl Grey: hot;; Coffee: Americano', + 'Earl Grey: hot; Coffee: Americano', + ], + 'string with 3 adjacent semicolons in the middle' => [ + 'Earl Grey: hot;;; Coffee: Americano', + 'Earl Grey: hot;; Coffee: Americano', + ], + ]; + } + + /** + * @test + * @dataProvider provideChangedStringForRemoveLastSemicolon + */ + public function removeLastSemicolonWithSemicolonAfterLastRuleDisabledRemovesLastSemicolon( + string $input, + string $expected + ): void { + $this->outputFormat->setSemicolonAfterLastRule(false); + + $result = $this->subject->removeLastSemicolon($input); + + self::assertSame($expected, $result); + } + + /** + * @test + */ + public function commentsWithEmptyCommentableAndRenderCommentsDisabledDoesNotReturnSpaceBetweenBlocks(): void + { + $this->outputFormat->setRenderComments(false); + $spaceBetweenBlocks = ' between-space '; + $this->outputFormat->setSpaceBetweenBlocks($spaceBetweenBlocks); + + $commentable = $this->createMock(Commentable::class); + $commentable->method('getComments')->willReturn([]); + + $result = $this->subject->comments($commentable); + + self::assertStringNotContainsString($spaceBetweenBlocks, $result); + } + + /** + * @test + */ + public function commentsWithEmptyCommentableAndRenderCommentsDisabledDoesNotReturnSpaceAfterBlocks(): void + { + $this->outputFormat->setRenderComments(false); + $spaceAfterBlocks = ' after-space '; + $this->outputFormat->setSpaceAfterBlocks($spaceAfterBlocks); + + $commentable = $this->createMock(Commentable::class); + $commentable->method('getComments')->willReturn([]); + + $result = $this->subject->comments($commentable); + + self::assertStringNotContainsString($spaceAfterBlocks, $result); + } + + /** + * @test + */ + public function commentsWithEmptyCommentableAndRenderCommentsDisabledReturnsEmptyString(): void + { + $this->outputFormat->setRenderComments(false); + + $commentable = $this->createMock(Commentable::class); + $commentable->method('getComments')->willReturn([]); + + $result = $this->subject->comments($commentable); + + self::assertSame('', $result); + } + + /** + * @test + */ + public function commentsWithEmptyCommentableAndRenderCommentsEnabledDoesNotReturnSpaceBetweenBlocks(): void + { + $this->outputFormat->setRenderComments(true); + $spaceBetweenBlocks = ' between-space '; + $this->outputFormat->setSpaceBetweenBlocks($spaceBetweenBlocks); + + $commentable = $this->createMock(Commentable::class); + $commentable->method('getComments')->willReturn([]); + + $result = $this->subject->comments($commentable); + + self::assertStringNotContainsString($spaceBetweenBlocks, $result); + } + + /** + * @test + */ + public function commentsWithEmptyCommentableAndRenderCommentsEnabledDoesNotReturnSpaceAfterBlocks(): void + { + $this->outputFormat->setRenderComments(true); + $spaceAfterBlocks = ' after-space '; + $this->outputFormat->setSpaceAfterBlocks($spaceAfterBlocks); + + $commentable = $this->createMock(Commentable::class); + $commentable->method('getComments')->willReturn([]); + + $result = $this->subject->comments($commentable); + + self::assertStringNotContainsString($spaceAfterBlocks, $result); + } + + /** + * @test + */ + public function commentsWithEmptyCommentableAndRenderCommentsEnabledReturnsEmptyString(): void + { + $this->outputFormat->setRenderComments(true); + + $commentable = $this->createMock(Commentable::class); + $commentable->method('getComments')->willReturn([]); + + $result = $this->subject->comments($commentable); + + self::assertSame('', $result); + } + + /** + * @test + */ + public function commentsWithCommentableWithOneCommentAndRenderCommentsDisabledReturnsEmptyString(): void + { + $this->outputFormat->setRenderComments(false); + + $commentText = 'I am a teapot.'; + $comment = new Comment($commentText); + $commentable = $this->createMock(Commentable::class); + $commentable->method('getComments')->willReturn([$comment]); + + $result = $this->subject->comments($commentable); + + self::assertSame('', $result); + } + + /** + * @test + */ + public function commentsWithCommentableWithOneCommentRendersComment(): void + { + $this->outputFormat->setRenderComments(true); + + $commentText = 'I am a teapot.'; + $comment = new Comment($commentText); + $commentable = $this->createMock(Commentable::class); + $commentable->method('getComments')->willReturn([$comment]); + + $result = $this->subject->comments($commentable); + + self::assertStringContainsString('/*' . $commentText . '*/', $result); + } + + /** + * @test + */ + public function commentsWithCommentableWithOneCommentPutsSpaceAfterBlocksAfterRenderedComment(): void + { + $this->outputFormat->setRenderComments(true); + $afterSpace = ' after-space '; + $this->outputFormat->setSpaceAfterBlocks($afterSpace); + + $commentText = 'I am a teapot.'; + $comment = new Comment($commentText); + $commentable = $this->createMock(Commentable::class); + $commentable->method('getComments')->willReturn([$comment]); + + $result = $this->subject->comments($commentable); + + self::assertSame('/*' . $commentText . '*/' . $afterSpace, $result); + } + + /** + * @test + */ + public function commentsWithCommentableWithTwoCommentsPutsSpaceAfterBlocksAfterLastRenderedComment(): void + { + $this->outputFormat->setRenderComments(true); + $afterSpace = ' after-space '; + $this->outputFormat->setSpaceAfterBlocks($afterSpace); + + $commentText1 = 'I am a teapot.'; + $comment1 = new Comment($commentText1); + $commentText2 = 'But I am not.'; + $comment2 = new Comment($commentText2); + $commentable = $this->createMock(Commentable::class); + $commentable->method('getComments')->willReturn([$comment1, $comment2]); + + $result = $this->subject->comments($commentable); + + self::assertStringContainsString('/*' . $commentText2 . '*/' . $afterSpace, $result); + } + + /** + * @test + */ + public function commentsWithCommentableWithTwoCommentsSeparatesCommentsBySpaceBetweenBlocks(): void + { + $this->outputFormat->setRenderComments(true); + $betweenSpace = ' between-space '; + $this->outputFormat->setSpaceBetweenBlocks($betweenSpace); + + $commentText1 = 'I am a teapot.'; + $comment1 = new Comment($commentText1); + $commentText2 = 'But I am not.'; + $comment2 = new Comment($commentText2); + $commentable = $this->createMock(Commentable::class); + $commentable->method('getComments')->willReturn([$comment1, $comment2]); + + $result = $this->subject->comments($commentable); + + $expected = '/*' . $commentText1 . '*/' . $betweenSpace . '/*' . $commentText2 . '*/'; + self::assertStringContainsString($expected, $result); + } + + /** + * @test + */ + public function commentsWithCommentableWithMoreThanTwoCommentsPutsSpaceAfterBlocksAfterLastRenderedComment(): void + { + $this->outputFormat->setRenderComments(true); + $afterSpace = ' after-space '; + $this->outputFormat->setSpaceAfterBlocks($afterSpace); + + $commentText1 = 'I am a teapot.'; + $comment1 = new Comment($commentText1); + $commentText2 = 'But I am not.'; + $comment2 = new Comment($commentText2); + $commentText3 = 'So what am I then?'; + $comment3 = new Comment($commentText3); + $commentable = $this->createMock(Commentable::class); + $commentable->method('getComments')->willReturn([$comment1, $comment2, $comment3]); + + $result = $this->subject->comments($commentable); + + self::assertStringContainsString('/*' . $commentText3 . '*/' . $afterSpace, $result); + } + + /** + * @test + */ + public function commentsWithCommentableWithMoreThanTwoCommentsSeparatesCommentsBySpaceBetweenBlocks(): void + { + $this->outputFormat->setRenderComments(true); + $betweenSpace = ' between-space '; + $this->outputFormat->setSpaceBetweenBlocks($betweenSpace); + + $commentText1 = 'I am a teapot.'; + $comment1 = new Comment($commentText1); + $commentText2 = 'But I am not.'; + $comment2 = new Comment($commentText2); + $commentText3 = 'So what am I then?'; + $comment3 = new Comment($commentText3); + $commentable = $this->createMock(Commentable::class); + $commentable->method('getComments')->willReturn([$comment1, $comment2, $comment3]); + + $result = $this->subject->comments($commentable); + + $expected = '/*' . $commentText1 . '*/' + . $betweenSpace . '/*' . $commentText2 . '*/' + . $betweenSpace . '/*' . $commentText3 . '*/'; + self::assertStringContainsString($expected, $result); + } +} diff --git a/tests/Unit/Parsing/OutputExceptionTest.php b/tests/Unit/Parsing/OutputExceptionTest.php new file mode 100644 index 000000000..73db4ebd5 --- /dev/null +++ b/tests/Unit/Parsing/OutputExceptionTest.php @@ -0,0 +1,76 @@ +getMessage()); + } + + /** + * @test + */ + public function getLineNumberByDefaultReturnsNull(): void + { + $subject = new OutputException('foo'); + + self::assertNull($subject->getLineNumber()); + } + + /** + * @test + */ + public function getLineNumberReturnsLineNumberProvidedToConstructor(): void + { + $lineNumber = 42; + $subject = new OutputException('foo', $lineNumber); + + self::assertSame($lineNumber, $subject->getLineNumber()); + } + + /** + * @test + */ + public function getMessageWithLineNumberProvidedIncludesLineNumber(): void + { + $lineNumber = 17; + $exception = new OutputException('foo', $lineNumber); + + self::assertStringContainsString(' [line no: ' . $lineNumber . ']', $exception->getMessage()); + } + + /** + * @test + */ + public function canBeThrown(): void + { + $this->expectException(OutputException::class); + + throw new OutputException('foo'); + } +} diff --git a/tests/Unit/Parsing/SourceExceptionTest.php b/tests/Unit/Parsing/SourceExceptionTest.php new file mode 100644 index 000000000..923f6c490 --- /dev/null +++ b/tests/Unit/Parsing/SourceExceptionTest.php @@ -0,0 +1,78 @@ +getMessage()); + } + + /** + * @test + */ + public function getLineNumberByDefaultReturnsNull(): void + { + $subject = new SourceException('foo'); + + self::assertNull($subject->getLineNumber()); + } + + /** + * @test + */ + public function getLineNumberReturnsLineNumberProvidedToConstructor(): void + { + $lineNumber = 42; + $subject = new SourceException('foo', $lineNumber); + + self::assertSame($lineNumber, $subject->getLineNumber()); + } + + /** + * @test + */ + public function getMessageWithLineNumberProvidedIncludesLineNumber(): void + { + $lineNumber = 17; + $exception = new SourceException('foo', $lineNumber); + + self::assertStringContainsString(' [line no: ' . $lineNumber . ']', $exception->getMessage()); + } + + /** + * @test + */ + public function getMessageWithLineNumberProvidedIncludesMessage(): void + { + $message = 'There is no flatware.'; + $exception = new SourceException($message, 17); + + self::assertStringContainsString($message, $exception->getMessage()); + } + + /** + * @test + */ + public function canBeThrown(): void + { + $this->expectException(SourceException::class); + + throw new SourceException('foo'); + } +} diff --git a/tests/Unit/Parsing/UnexpectedEOFExceptionTest.php b/tests/Unit/Parsing/UnexpectedEOFExceptionTest.php new file mode 100644 index 000000000..b3c2ffcc9 --- /dev/null +++ b/tests/Unit/Parsing/UnexpectedEOFExceptionTest.php @@ -0,0 +1,178 @@ +getLineNumber()); + } + + /** + * @test + */ + public function getLineNumberReturnsLineNumberProvidedToConstructor(): void + { + $lineNumber = 42; + $subject = new UnexpectedEOFException('expected', 'found', 'literal', $lineNumber); + + self::assertSame($lineNumber, $subject->getLineNumber()); + } + + /** + * @test + */ + public function getMessageWithLineNumberProvidedIncludesLineNumber(): void + { + $lineNumber = 17; + $exception = new UnexpectedEOFException('expected', 'found', 'literal', $lineNumber); + + self::assertStringContainsString(' [line no: ' . $lineNumber . ']', $exception->getMessage()); + } + + /** + * @test + */ + public function canBeThrown(): void + { + $this->expectException(UnexpectedEOFException::class); + + throw new UnexpectedEOFException('expected', 'found'); + } + + /** + * @test + */ + public function messageByDefaultRefersToTokenNotFound(): void + { + $expected = 'tea'; + $found = 'coffee'; + + $exception = new UnexpectedEOFException($expected, $found); + + $expectedMessage = 'Token “' . $expected . '” (literal) not found. Got “' . $found . '”.'; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } + + /** + * @test + */ + public function messageForInvalidMatchTypeRefersToTokenNotFound(): void + { + $expected = 'tea'; + $found = 'coffee'; + + // @phpstan-ignore-next-line argument.type We're explicitly testing with an invalid value here. + $exception = new UnexpectedEOFException($expected, $found, 'coding'); + + $expectedMessage = 'Token “' . $expected . '” (coding) not found. Got “' . $found . '”.'; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } + + /** + * @test + */ + public function messageForLiteralMatchTypeRefersToTokenNotFound(): void + { + $expected = 'tea'; + $found = 'coffee'; + + $exception = new UnexpectedEOFException($expected, $found, 'literal'); + + $expectedMessage = 'Token “' . $expected . '” (literal) not found. Got “' . $found . '”.'; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } + + /** + * @test + */ + public function messageForSearchMatchTypeRefersToNoResults(): void + { + $expected = 'tea'; + $found = 'coffee'; + + $exception = new UnexpectedEOFException($expected, $found, 'search'); + + $expectedMessage = 'Search for “' . $expected . '” returned no results. Context: “' . $found . '”.'; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } + + /** + * @test + */ + public function messageForCountMatchTypeRefersToNumberOfCharacters(): void + { + $expected = 'tea'; + $found = 'coffee'; + + $exception = new UnexpectedEOFException($expected, $found, 'count'); + + $expectedMessage = 'Next token was expected to have ' . $expected . ' chars. Context: “' . $found . '”.'; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } + + /** + * @test + */ + public function messageForIdentifierMatchTypeRefersToIdentifier(): void + { + $expected = 'tea'; + $found = 'coffee'; + + $exception = new UnexpectedEOFException($expected, $found, 'identifier'); + + $expectedMessage = 'Identifier expected. Got “' . $found . '”'; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } + + /** + * @test + */ + public function messageForCustomMatchTypeMentionsExpectedAndFound(): void + { + $expected = 'tea'; + $found = 'coffee'; + + $exception = new UnexpectedEOFException($expected, $found, 'custom'); + + $expectedMessage = $expected . ' ' . $found; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } + + /** + * @test + */ + public function messageForCustomMatchTypeTrimsMessage(): void + { + $expected = 'tea'; + $found = 'coffee'; + + $exception = new UnexpectedEOFException(' ' . $expected, $found . ' ', 'custom'); + + $expectedMessage = $expected . ' ' . $found; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } +} diff --git a/tests/Unit/Parsing/UnexpectedTokenExceptionTest.php b/tests/Unit/Parsing/UnexpectedTokenExceptionTest.php new file mode 100644 index 000000000..6adb3d7e0 --- /dev/null +++ b/tests/Unit/Parsing/UnexpectedTokenExceptionTest.php @@ -0,0 +1,178 @@ +getLineNumber()); + } + + /** + * @test + */ + public function getLineNumberReturnsLineNumberProvidedToConstructor(): void + { + $lineNumber = 42; + $subject = new UnexpectedTokenException('expected', 'found', 'literal', $lineNumber); + + self::assertSame($lineNumber, $subject->getLineNumber()); + } + + /** + * @test + */ + public function getMessageWithLineNumberProvidedIncludesLineNumber(): void + { + $lineNumber = 17; + $exception = new UnexpectedTokenException('expected', 'found', 'literal', $lineNumber); + + self::assertStringContainsString(' [line no: ' . $lineNumber . ']', $exception->getMessage()); + } + + /** + * @test + */ + public function canBeThrown(): void + { + $this->expectException(UnexpectedTokenException::class); + + throw new UnexpectedTokenException('expected', 'found'); + } + + /** + * @test + */ + public function messageByDefaultRefersToTokenNotFound(): void + { + $expected = 'tea'; + $found = 'coffee'; + + $exception = new UnexpectedTokenException($expected, $found); + + $expectedMessage = 'Token “' . $expected . '” (literal) not found. Got “' . $found . '”.'; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } + + /** + * @test + */ + public function messageForInvalidMatchTypeRefersToTokenNotFound(): void + { + $expected = 'tea'; + $found = 'coffee'; + + // @phpstan-ignore-next-line argument.type We're explicitly testing with an invalid value here. + $exception = new UnexpectedTokenException($expected, $found, 'coding'); + + $expectedMessage = 'Token “' . $expected . '” (coding) not found. Got “' . $found . '”.'; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } + + /** + * @test + */ + public function messageForLiteralMatchTypeRefersToTokenNotFound(): void + { + $expected = 'tea'; + $found = 'coffee'; + + $exception = new UnexpectedTokenException($expected, $found, 'literal'); + + $expectedMessage = 'Token “' . $expected . '” (literal) not found. Got “' . $found . '”.'; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } + + /** + * @test + */ + public function messageForSearchMatchTypeRefersToNoResults(): void + { + $expected = 'tea'; + $found = 'coffee'; + + $exception = new UnexpectedTokenException($expected, $found, 'search'); + + $expectedMessage = 'Search for “' . $expected . '” returned no results. Context: “' . $found . '”.'; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } + + /** + * @test + */ + public function messageForCountMatchTypeRefersToNumberOfCharacters(): void + { + $expected = 'tea'; + $found = 'coffee'; + + $exception = new UnexpectedTokenException($expected, $found, 'count'); + + $expectedMessage = 'Next token was expected to have ' . $expected . ' chars. Context: “' . $found . '”.'; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } + + /** + * @test + */ + public function messageForIdentifierMatchTypeRefersToIdentifier(): void + { + $expected = 'tea'; + $found = 'coffee'; + + $exception = new UnexpectedTokenException($expected, $found, 'identifier'); + + $expectedMessage = 'Identifier expected. Got “' . $found . '”'; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } + + /** + * @test + */ + public function messageForCustomMatchTypeMentionsExpectedAndFound(): void + { + $expected = 'tea'; + $found = 'coffee'; + + $exception = new UnexpectedTokenException($expected, $found, 'custom'); + + $expectedMessage = $expected . ' ' . $found; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } + + /** + * @test + */ + public function messageForCustomMatchTypeTrimsMessage(): void + { + $expected = 'tea'; + $found = 'coffee'; + + $exception = new UnexpectedTokenException(' ' . $expected, $found . ' ', 'custom'); + + $expectedMessage = $expected . ' ' . $found; + self::assertStringContainsString($expectedMessage, $exception->getMessage()); + } +} diff --git a/tests/Unit/Position/Fixtures/ConcretePosition.php b/tests/Unit/Position/Fixtures/ConcretePosition.php new file mode 100644 index 000000000..0db387065 --- /dev/null +++ b/tests/Unit/Position/Fixtures/ConcretePosition.php @@ -0,0 +1,13 @@ +subject = new ConcretePosition(); + } + + /** + * @test + */ + public function getLineNumberInitiallyReturnsNull(): void + { + self::assertNull($this->subject->getLineNumber()); + } + + /** + * @test + */ + public function getColumnNumberInitiallyReturnsNull(): void + { + self::assertNull($this->subject->getColumnNumber()); + } + + /** + * @return array}> + */ + public function provideLineNumber(): array + { + return [ + 'line 1' => [1], + 'line 42' => [42], + ]; + } + + /** + * @test + * + * @param int<1, max> $lineNumber + * + * @dataProvider provideLineNumber + */ + public function setPositionOnVirginSetsLineNumber(int $lineNumber): void + { + $this->subject->setPosition($lineNumber); + + self::assertSame($lineNumber, $this->subject->getLineNumber()); + } + + /** + * @test + * + * @param int<1, max> $lineNumber + * + * @dataProvider provideLineNumber + */ + public function setPositionSetsNewLineNumber(int $lineNumber): void + { + $this->subject->setPosition(99); + + $this->subject->setPosition($lineNumber); + + self::assertSame($lineNumber, $this->subject->getLineNumber()); + } + + /** + * @test + */ + public function setPositionWithNullClearsLineNumber(): void + { + $this->subject->setPosition(99); + + $this->subject->setPosition(null); + + self::assertNull($this->subject->getLineNumber()); + } + + /** + * @return array}> + */ + public function provideColumnNumber(): array + { + return [ + 'column 0' => [0], + 'column 14' => [14], + 'column 39' => [39], + ]; + } + + /** + * @test + * + * @param int<0, max> $columnNumber + * + * @dataProvider provideColumnNumber + */ + public function setPositionOnVirginSetsColumnNumber(int $columnNumber): void + { + $this->subject->setPosition(1, $columnNumber); + + self::assertSame($columnNumber, $this->subject->getColumnNumber()); + } + + /** + * @test + * + * @dataProvider provideColumnNumber + */ + public function setPositionSetsNewColumnNumber(int $columnNumber): void + { + $this->subject->setPosition(1, 99); + + $this->subject->setPosition(2, $columnNumber); + + self::assertSame($columnNumber, $this->subject->getColumnNumber()); + } + + /** + * @test + */ + public function setPositionWithoutColumnNumberClearsColumnNumber(): void + { + $this->subject->setPosition(1, 99); + + $this->subject->setPosition(2); + + self::assertNull($this->subject->getColumnNumber()); + } + + /** + * @test + */ + public function setPositionWithNullForColumnNumberClearsColumnNumber(): void + { + $this->subject->setPosition(1, 99); + + $this->subject->setPosition(2, null); + + self::assertNull($this->subject->getColumnNumber()); + } + + /** + * @return DataProvider, 1: int<0, max>}> + */ + public function provideLineAndColumnNumber(): DataProvider + { + return DataProvider::cross($this->provideLineNumber(), $this->provideColumnNumber()); + } + + /** + * @test + * + * @dataProvider provideLineAndColumnNumber + */ + public function setPositionOnVirginSetsLineAndColumnNumber(int $lineNumber, int $columnNumber): void + { + $this->subject->setPosition($lineNumber, $columnNumber); + + self::assertSame($lineNumber, $this->subject->getLineNumber()); + self::assertSame($columnNumber, $this->subject->getColumnNumber()); + } + + /** + * @test + * + * @dataProvider provideLineAndColumnNumber + */ + public function setPositionSetsNewLineAndColumnNumber(int $lineNumber, int $columnNumber): void + { + $this->subject->setPosition(98, 99); + + $this->subject->setPosition($lineNumber, $columnNumber); + + self::assertSame($lineNumber, $this->subject->getLineNumber()); + self::assertSame($columnNumber, $this->subject->getColumnNumber()); + } +} diff --git a/tests/Unit/Property/CSSNamespaceTest.php b/tests/Unit/Property/CSSNamespaceTest.php new file mode 100644 index 000000000..2e4d99222 --- /dev/null +++ b/tests/Unit/Property/CSSNamespaceTest.php @@ -0,0 +1,34 @@ +subject = new CSSNamespace(new CSSString('http://www.w3.org/2000/svg')); + } + + /** + * @test + */ + public function implementsCSSListItem(): void + { + self::assertInstanceOf(CSSListItem::class, $this->subject); + } +} diff --git a/tests/Unit/Property/CharsetTest.php b/tests/Unit/Property/CharsetTest.php new file mode 100644 index 000000000..e0645f5ef --- /dev/null +++ b/tests/Unit/Property/CharsetTest.php @@ -0,0 +1,34 @@ +subject = new Charset(new CSSString('UTF-8')); + } + + /** + * @test + */ + public function implementsCSSListItem(): void + { + self::assertInstanceOf(CSSListItem::class, $this->subject); + } +} diff --git a/tests/Unit/Property/ImportTest.php b/tests/Unit/Property/ImportTest.php new file mode 100644 index 000000000..4ec028e3f --- /dev/null +++ b/tests/Unit/Property/ImportTest.php @@ -0,0 +1,35 @@ +subject = new Import(new URL(new CSSString('https://example.org/')), null); + } + + /** + * @test + */ + public function implementsCSSListItem(): void + { + self::assertInstanceOf(CSSListItem::class, $this->subject); + } +} diff --git a/tests/Unit/Property/Selector/SpecificityCalculatorTest.php b/tests/Unit/Property/Selector/SpecificityCalculatorTest.php new file mode 100644 index 000000000..088bd5179 --- /dev/null +++ b/tests/Unit/Property/Selector/SpecificityCalculatorTest.php @@ -0,0 +1,94 @@ +}> + */ + public static function provideSelectorsAndSpecificities(): array + { + return [ + 'element' => ['a', 1], + 'element and descendant with pseudo-selector' => ['ol li::before', 3], + 'class' => ['.highlighted', 10], + 'element with class' => ['li.green', 11], + 'class with pseudo-selector' => ['.help:hover', 20], + 'ID' => ['#file', 100], + 'ID and descendant class' => ['#test .help', 110], + ]; + } + + /** + * @test + * + * @param non-empty-string $selector + * @param int<0, max> $expectedSpecificity + * + * @dataProvider provideSelectorsAndSpecificities + */ + public function calculateReturnsSpecificityForProvidedSelector( + string $selector, + int $expectedSpecificity + ): void { + self::assertSame($expectedSpecificity, SpecificityCalculator::calculate($selector)); + } + + /** + * @test + * + * @param non-empty-string $selector + * @param int<0, max> $expectedSpecificity + * + * @dataProvider provideSelectorsAndSpecificities + */ + public function calculateAfterClearingCacheReturnsSpecificityForProvidedSelector( + string $selector, + int $expectedSpecificity + ): void { + SpecificityCalculator::clearCache(); + + self::assertSame($expectedSpecificity, SpecificityCalculator::calculate($selector)); + } + + /** + * @test + */ + public function calculateCalledTwoTimesReturnsSameSpecificityForProvidedSelector(): void + { + $selector = '#test .help'; + + $firstResult = SpecificityCalculator::calculate($selector); + $secondResult = SpecificityCalculator::calculate($selector); + + self::assertSame($firstResult, $secondResult); + } + + /** + * @test + */ + public function calculateCalledReturnsSameSpecificityForProvidedSelectorBeforeAndAfterClearingCache(): void + { + $selector = '#test .help'; + + $firstResult = SpecificityCalculator::calculate($selector); + SpecificityCalculator::clearCache(); + $secondResult = SpecificityCalculator::calculate($selector); + + self::assertSame($firstResult, $secondResult); + } +} diff --git a/tests/Unit/Property/SelectorTest.php b/tests/Unit/Property/SelectorTest.php new file mode 100644 index 000000000..e53e8274d --- /dev/null +++ b/tests/Unit/Property/SelectorTest.php @@ -0,0 +1,144 @@ +getSelector()); + } + + /** + * @test + */ + public function setSelectorOverwritesSelectorProvidedToConstructor(): void + { + $subject = new Selector('a'); + + $selector = 'input'; + $subject->setSelector($selector); + + self::assertSame($selector, $subject->getSelector()); + } + + /** + * @return array}> + */ + public static function provideSelectorsAndSpecificities(): array + { + return [ + 'element' => ['a', 1], + 'element and descendant with pseudo-selector' => ['ol li::before', 3], + 'class' => ['.highlighted', 10], + 'element with class' => ['li.green', 11], + 'class with pseudo-selector' => ['.help:hover', 20], + 'ID' => ['#file', 100], + 'ID and descendant class' => ['#test .help', 110], + '`not`' => [':not(#your-mug)', 100], + // TODO, broken: The specificity should be the highest of the `:not` arguments, not the sum. + '`not` with multiple arguments' => [':not(#your-mug, .their-mug)', 110], + ]; + } + + /** + * @test + * + * @param non-empty-string $selector + * @param int<0, max> $expectedSpecificity + * + * @dataProvider provideSelectorsAndSpecificities + */ + public function getSpecificityByDefaultReturnsSpecificityOfSelectorProvidedToConstructor( + string $selector, + int $expectedSpecificity + ): void { + $subject = new Selector($selector); + + self::assertSame($expectedSpecificity, $subject->getSpecificity()); + } + + /** + * @test + * + * @param non-empty-string $selector + * @param int<0, max> $expectedSpecificity + * + * @dataProvider provideSelectorsAndSpecificities + */ + public function getSpecificityReturnsSpecificityOfSelectorLastProvidedViaSetSelector( + string $selector, + int $expectedSpecificity + ): void { + $subject = new Selector('p'); + + $subject->setSelector($selector); + + self::assertSame($expectedSpecificity, $subject->getSpecificity()); + } + + /** + * @test + * + * @dataProvider provideSelectorsAndSpecificities + */ + public function isValidForValidSelectorReturnsTrue(string $selector): void + { + self::assertTrue(Selector::isValid($selector)); + } + + /** + * @return array + */ + public static function provideInvalidSelectors(): array + { + return [ + // This is currently broken. + // 'empty string' => [''], + 'percent sign' => ['%'], + // This is currently broken. + // 'hash only' => ['#'], + // This is currently broken. + // 'dot only' => ['.'], + 'slash' => ['/'], + 'less-than sign' => ['<'], + // This is currently broken. + // 'whitespace only' => [" \t\n\r"], + ]; + } + + /** + * @test + * + * @dataProvider provideInvalidSelectors + */ + public function isValidForInvalidSelectorReturnsFalse(string $selector): void + { + self::assertFalse(Selector::isValid($selector)); + } +} diff --git a/tests/Unit/Rule/RuleTest.php b/tests/Unit/Rule/RuleTest.php new file mode 100644 index 000000000..008bcfc18 --- /dev/null +++ b/tests/Unit/Rule/RuleTest.php @@ -0,0 +1,73 @@ +}> + */ + public static function provideRulesAndExpectedParsedValueListTypes(): array + { + return [ + 'src (e.g. in @font-face)' => [ + " + src: url('../fonts/open-sans-italic-300.woff2') format('woff2'), + url('../fonts/open-sans-italic-300.ttf') format('truetype'); + ", + [RuleValueList::class, RuleValueList::class], + ], + ]; + } + + /** + * @test + * + * @param list $expectedTypeClassnames + * + * @dataProvider provideRulesAndExpectedParsedValueListTypes + */ + public function parsesValuesIntoExpectedTypeList(string $rule, array $expectedTypeClassnames): void + { + $subject = Rule::parse(new ParserState($rule, Settings::create())); + + $value = $subject->getValue(); + self::assertInstanceOf(ValueList::class, $value); + + $actualClassnames = \array_map( + /** + * @param Value|string $component + */ + static function ($component): string { + return \is_string($component) ? 'string' : \get_class($component); + }, + $value->getListComponents() + ); + + self::assertSame($expectedTypeClassnames, $actualClassnames); + } +} diff --git a/tests/Unit/RuleSet/AtRuleSetTest.php b/tests/Unit/RuleSet/AtRuleSetTest.php new file mode 100644 index 000000000..0e3e0c974 --- /dev/null +++ b/tests/Unit/RuleSet/AtRuleSetTest.php @@ -0,0 +1,33 @@ +subject = new AtRuleSet('supports'); + } + + /** + * @test + */ + public function implementsCSSListItem(): void + { + self::assertInstanceOf(CSSListItem::class, $this->subject); + } +} diff --git a/tests/Unit/RuleSet/DeclarationBlockTest.php b/tests/Unit/RuleSet/DeclarationBlockTest.php new file mode 100644 index 000000000..4b20e9fc8 --- /dev/null +++ b/tests/Unit/RuleSet/DeclarationBlockTest.php @@ -0,0 +1,281 @@ +subject = new DeclarationBlock(); + } + + /** + * @test + */ + public function implementsCSSElement(): void + { + self::assertInstanceOf(CSSElement::class, $this->subject); + } + + /** + * @test + */ + public function implementsCSSListItem(): void + { + self::assertInstanceOf(CSSListItem::class, $this->subject); + } + + /** + * @test + */ + public function implementsPositionable(): void + { + self::assertInstanceOf(Positionable::class, $this->subject); + } + + /** + * @test + */ + public function getLineNumberByDefaultReturnsNull(): void + { + $result = $this->subject->getLineNumber(); + + self::assertNull($result); + } + + /** + * @return array|null}> + */ + public function provideLineNumber(): array + { + return [ + 'null' => [null], + 'line 1' => [1], + 'line 42' => [42], + ]; + } + + /** + * @test + * + * @param int<1, max>|null $lineNumber + * + * @dataProvider provideLineNumber + */ + public function getLineNumberReturnsLineNumberPassedToConstructor(?int $lineNumber): void + { + $subject = new DeclarationBlock($lineNumber); + + $result = $subject->getLineNumber(); + + self::assertSame($lineNumber, $result); + } + + /** + * @return array + */ + public static function provideSelector(): array + { + return [ + 'type' => ['body'], + 'class' => ['.teapot'], + 'type & class' => ['img.teapot'], + 'id' => ['#my-mug'], + 'type & id' => ['h2#my-mug'], + 'pseudo-class' => [':hover'], + 'type & pseudo-class' => ['a:hover'], + '`not`' => [':not(#your-mug)'], + '`not` with multiple arguments' => [':not(#your-mug, .their-mug)'], + 'pseudo-element' => ['::before'], + 'attribute with `"`' => ['[alt="{}()[]\\"\',"]'], + 'attribute with `\'`' => ['[alt=\'{}()[]"\\\',\']'], + ]; + } + + /** + * @test + * + * @param non-empty-string $selector + * + * @dataProvider provideSelector + */ + public function parsesSingleSelector(string $selector): void + { + $subject = DeclarationBlock::parse(new ParserState($selector . ' {}', Settings::create())); + + self::assertInstanceOf(DeclarationBlock::class, $subject); + self::assertSame([$selector], self::getSelectorsAsStrings($subject)); + } + + /** + * @return DataProvider + */ + public static function provideTwoSelectors(): DataProvider + { + return DataProvider::cross(self::provideSelector(), self::provideSelector()); + } + + /** + * @test + * + * @param non-empty-string $firstSelector + * @param non-empty-string $secondSelector + * + * @dataProvider provideTwoSelectors + */ + public function parsesTwoCommaSeparatedSelectors(string $firstSelector, string $secondSelector): void + { + $joinedSelectors = $firstSelector . ', ' . $secondSelector; + + $subject = DeclarationBlock::parse(new ParserState($joinedSelectors . ' {}', Settings::create())); + + self::assertInstanceOf(DeclarationBlock::class, $subject); + self::assertSame([$firstSelector, $secondSelector], self::getSelectorsAsStrings($subject)); + } + + /** + * @return array + */ + public static function provideInvalidSelector(): array + { + // TODO: the `parse` method consumes the first character without inspection, + // so the 'lone' test strings are prefixed with a space. + return [ + 'lone `(`' => [' ('], + 'lone `)`' => [' )'], + 'unclosed `(`' => [':not(#your-mug'], + 'extra `)`' => [':not(#your-mug))'], + ]; + } + + /** + * @test + * + * @param non-empty-string $selector + * + * @dataProvider provideInvalidSelector + */ + public function parseSkipsBlockWithInvalidSelector(string $selector): void + { + static $nextCss = ' .next {}'; + $css = $selector . ' {}' . $nextCss; + $parserState = new ParserState($css, Settings::create()); + + $subject = DeclarationBlock::parse($parserState); + + self::assertNull($subject); + self::assertTrue($parserState->comes($nextCss)); + } + + /** + * @return array + */ + private static function getSelectorsAsStrings(DeclarationBlock $declarationBlock): array + { + return \array_map( + static function (Selector $selectorObject): string { + return $selectorObject->getSelector(); + }, + $declarationBlock->getSelectors() + ); + } + + /** + * @test + */ + public function getRuleSetOnVirginReturnsARuleSet(): void + { + $result = $this->subject->getRuleSet(); + + self::assertInstanceOf(RuleSet::class, $result); + } + + /** + * @test + */ + public function getRuleSetAfterRulesSetReturnsARuleSet(): void + { + $this->subject->setRules([new Rule('color')]); + + $result = $this->subject->getRuleSet(); + + self::assertInstanceOf(RuleSet::class, $result); + } + + /** + * @test + */ + public function getRuleSetOnVirginReturnsObjectWithoutRules(): void + { + $result = $this->subject->getRuleSet(); + + self::assertSame([], $result->getRules()); + } + + /** + * @test + * + * @param list $propertyNamesToSet + * + * @dataProvider providePropertyNames + */ + public function getRuleSetReturnsObjectWithRulesSet(array $propertyNamesToSet): void + { + $rules = self::createRulesFromPropertyNames($propertyNamesToSet); + $this->subject->setRules($rules); + + $result = $this->subject->getRuleSet(); + + self::assertSame($rules, $result->getRules()); + } + + /** + * @test + */ + public function getRuleSetByDefaultReturnsObjectWithNullLineNumber(): void + { + $result = $this->subject->getRuleSet(); + + self::assertNull($result->getLineNumber()); + } + + /** + * @test + * + * @param int<1, max>|null $lineNumber + * + * @dataProvider provideLineNumber + */ + public function getRuleSetReturnsObjectWithLineNumberPassedToConstructor(?int $lineNumber): void + { + $subject = new DeclarationBlock($lineNumber); + + $result = $subject->getRuleSet(); + + self::assertSame($lineNumber, $result->getLineNumber()); + } +} diff --git a/tests/Unit/RuleSet/RuleContainerTest.php b/tests/Unit/RuleSet/RuleContainerTest.php new file mode 100644 index 000000000..bcb641a22 --- /dev/null +++ b/tests/Unit/RuleSet/RuleContainerTest.php @@ -0,0 +1,1200 @@ +subject); + } + + /** + * @return array}> + */ + public static function providePropertyNames(): array + { + return [ + 'no properties' => [[]], + 'one property' => [['color']], + 'two different properties' => [['color', 'display']], + 'two of the same property' => [['color', 'color']], + ]; + } + + /** + * @return array + */ + public static function provideAnotherPropertyName(): array + { + return [ + 'property name `color` maybe matching that of existing declaration' => ['color'], + 'property name `display` maybe matching that of existing declaration' => ['display'], + 'property name `width` not matching that of existing declaration' => ['width'], + ]; + } + + /** + * @return DataProvider, 1: string}> + */ + public static function provideInitialPropertyNamesAndAnotherPropertyName(): DataProvider + { + return DataProvider::cross(self::providePropertyNames(), self::provideAnotherPropertyName()); + } + + /** + * @test + * + * @param list $initialPropertyNames + * + * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName + */ + public function addRuleWithoutPositionWithoutSiblingAddsRuleAfterInitialRules( + array $initialPropertyNames, + string $propertyNameToAdd + ): void { + $ruleToAdd = new Rule($propertyNameToAdd); + $this->setRulesFromPropertyNames($initialPropertyNames); + + $this->subject->addRule($ruleToAdd); + + $rules = $this->subject->getRules(); + self::assertSame($ruleToAdd, \end($rules)); + } + + /** + * @test + * + * @param list $initialPropertyNames + * + * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName + */ + public function addRuleWithoutPositionWithoutSiblingSetsValidLineNumber( + array $initialPropertyNames, + string $propertyNameToAdd + ): void { + $ruleToAdd = new Rule($propertyNameToAdd); + $this->setRulesFromPropertyNames($initialPropertyNames); + + $this->subject->addRule($ruleToAdd); + + self::assertIsInt($ruleToAdd->getLineNumber(), 'line number not set'); + self::assertGreaterThanOrEqual(1, $ruleToAdd->getLineNumber(), 'line number not valid'); + } + + /** + * @test + * + * @param list $initialPropertyNames + * + * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName + */ + public function addRuleWithoutPositionWithoutSiblingSetsValidColumnNumber( + array $initialPropertyNames, + string $propertyNameToAdd + ): void { + $ruleToAdd = new Rule($propertyNameToAdd); + $this->setRulesFromPropertyNames($initialPropertyNames); + + $this->subject->addRule($ruleToAdd); + + self::assertIsInt($ruleToAdd->getColumnNumber(), 'column number not set'); + self::assertGreaterThanOrEqual(0, $ruleToAdd->getColumnNumber(), 'column number not valid'); + } + + /** + * @test + * + * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName + * + * @param list $initialPropertyNames + */ + public function addRuleWithOnlyLineNumberWithoutSiblingAddsRule( + array $initialPropertyNames, + string $propertyNameToAdd + ): void { + $ruleToAdd = new Rule($propertyNameToAdd); + $ruleToAdd->setPosition(42); + $this->setRulesFromPropertyNames($initialPropertyNames); + + $this->subject->addRule($ruleToAdd); + + self::assertContains($ruleToAdd, $this->subject->getRules()); + } + + /** + * @test + * + * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName + * + * @param list $initialPropertyNames + */ + public function addRuleWithOnlyLineNumberWithoutSiblingSetsColumnNumber( + array $initialPropertyNames, + string $propertyNameToAdd + ): void { + $ruleToAdd = new Rule($propertyNameToAdd); + $ruleToAdd->setPosition(42); + $this->setRulesFromPropertyNames($initialPropertyNames); + + $this->subject->addRule($ruleToAdd); + + self::assertIsInt($ruleToAdd->getColumnNumber(), 'column number not set'); + self::assertGreaterThanOrEqual(0, $ruleToAdd->getColumnNumber(), 'column number not valid'); + } + + /** + * @test + * + * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName + * + * @param list $initialPropertyNames + */ + public function addRuleWithOnlyLineNumberWithoutSiblingPreservesLineNumber( + array $initialPropertyNames, + string $propertyNameToAdd + ): void { + $ruleToAdd = new Rule($propertyNameToAdd); + $ruleToAdd->setPosition(42); + $this->setRulesFromPropertyNames($initialPropertyNames); + + $this->subject->addRule($ruleToAdd); + + self::assertSame(42, $ruleToAdd->getLineNumber(), 'line number not preserved'); + } + + /** + * @test + * + * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName + * + * @param list $initialPropertyNames + */ + public function addRuleWithOnlyColumnNumberWithoutSiblingAddsRuleAfterInitialRules( + array $initialPropertyNames, + string $propertyNameToAdd + ): void { + $ruleToAdd = new Rule($propertyNameToAdd); + $ruleToAdd->setPosition(null, 42); + $this->setRulesFromPropertyNames($initialPropertyNames); + + $this->subject->addRule($ruleToAdd); + + $rules = $this->subject->getRules(); + self::assertSame($ruleToAdd, \end($rules)); + } + + /** + * @test + * + * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName + * + * @param list $initialPropertyNames + */ + public function addRuleWithOnlyColumnNumberWithoutSiblingSetsLineNumber( + array $initialPropertyNames, + string $propertyNameToAdd + ): void { + $ruleToAdd = new Rule($propertyNameToAdd); + $ruleToAdd->setPosition(null, 42); + $this->setRulesFromPropertyNames($initialPropertyNames); + + $this->subject->addRule($ruleToAdd); + + self::assertIsInt($ruleToAdd->getLineNumber(), 'line number not set'); + self::assertGreaterThanOrEqual(1, $ruleToAdd->getLineNumber(), 'line number not valid'); + } + + /** + * @test + * + * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName + * + * @param list $initialPropertyNames + */ + public function addRuleWithOnlyColumnNumberWithoutSiblingPreservesColumnNumber( + array $initialPropertyNames, + string $propertyNameToAdd + ): void { + $ruleToAdd = new Rule($propertyNameToAdd); + $ruleToAdd->setPosition(null, 42); + $this->setRulesFromPropertyNames($initialPropertyNames); + + $this->subject->addRule($ruleToAdd); + + self::assertSame(42, $ruleToAdd->getColumnNumber(), 'column number not preserved'); + } + + /** + * @test + * + * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName + * + * @param list $initialPropertyNames + */ + public function addRuleWithCompletePositionWithoutSiblingAddsRule( + array $initialPropertyNames, + string $propertyNameToAdd + ): void { + $ruleToAdd = new Rule($propertyNameToAdd); + $ruleToAdd->setPosition(42, 64); + $this->setRulesFromPropertyNames($initialPropertyNames); + + $this->subject->addRule($ruleToAdd); + + self::assertContains($ruleToAdd, $this->subject->getRules()); + } + + /** + * @test + * + * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName + * + * @param list $initialPropertyNames + */ + public function addRuleWithCompletePositionWithoutSiblingPreservesPosition( + array $initialPropertyNames, + string $propertyNameToAdd + ): void { + $ruleToAdd = new Rule($propertyNameToAdd); + $ruleToAdd->setPosition(42, 64); + $this->setRulesFromPropertyNames($initialPropertyNames); + + $this->subject->addRule($ruleToAdd); + + self::assertSame(42, $ruleToAdd->getLineNumber(), 'line number not preserved'); + self::assertSame(64, $ruleToAdd->getColumnNumber(), 'column number not preserved'); + } + + /** + * @return array, 1: int<0, max>}> + */ + public static function provideInitialPropertyNamesAndIndexOfOne(): array + { + $initialPropertyNamesSets = self::providePropertyNames(); + + // Provide sets with each possible index for the initially set `Rule`s. + $initialPropertyNamesAndIndexSets = []; + foreach ($initialPropertyNamesSets as $setName => $data) { + $initialPropertyNames = $data[0]; + for ($index = 0; $index < \count($initialPropertyNames); ++$index) { + $initialPropertyNamesAndIndexSets[$setName . ', index ' . $index] = + [$initialPropertyNames, $index]; + } + } + + return $initialPropertyNamesAndIndexSets; + } + + /** + * @return DataProvider, 1: int<0, max>, 2: string}> + */ + public static function provideInitialPropertyNamesAndSiblingIndexAndPropertyNameToAdd(): DataProvider + { + return DataProvider::cross( + self::provideInitialPropertyNamesAndIndexOfOne(), + self::provideAnotherPropertyName() + ); + } + + /** + * @test + * + * @param non-empty-list $initialPropertyNames + * @param int<0, max> $siblingIndex + * + * @dataProvider provideInitialPropertyNamesAndSiblingIndexAndPropertyNameToAdd + */ + public function addRuleWithSiblingInsertsRuleBeforeSibling( + array $initialPropertyNames, + int $siblingIndex, + string $propertyNameToAdd + ): void { + $ruleToAdd = new Rule($propertyNameToAdd); + $this->setRulesFromPropertyNames($initialPropertyNames); + $sibling = $this->subject->getRules()[$siblingIndex]; + + $this->subject->addRule($ruleToAdd, $sibling); + + $rules = $this->subject->getRules(); + $siblingPosition = \array_search($sibling, $rules, true); + self::assertIsInt($siblingPosition); + self::assertSame($siblingPosition - 1, \array_search($ruleToAdd, $rules, true)); + } + + /** + * @test + * + * @param non-empty-list $initialPropertyNames + * @param int<0, max> $siblingIndex + * + * @dataProvider provideInitialPropertyNamesAndSiblingIndexAndPropertyNameToAdd + */ + public function addRuleWithSiblingSetsValidLineNumber( + array $initialPropertyNames, + int $siblingIndex, + string $propertyNameToAdd + ): void { + $ruleToAdd = new Rule($propertyNameToAdd); + $this->setRulesFromPropertyNames($initialPropertyNames); + $sibling = $this->subject->getRules()[$siblingIndex]; + + $this->subject->addRule($ruleToAdd, $sibling); + + self::assertIsInt($ruleToAdd->getLineNumber(), 'line number not set'); + self::assertGreaterThanOrEqual(1, $ruleToAdd->getLineNumber(), 'line number not valid'); + } + + /** + * @test + * + * @param non-empty-list $initialPropertyNames + * @param int<0, max> $siblingIndex + * + * @dataProvider provideInitialPropertyNamesAndSiblingIndexAndPropertyNameToAdd + */ + public function addRuleWithSiblingSetsValidColumnNumber( + array $initialPropertyNames, + int $siblingIndex, + string $propertyNameToAdd + ): void { + $ruleToAdd = new Rule($propertyNameToAdd); + $this->setRulesFromPropertyNames($initialPropertyNames); + $sibling = $this->subject->getRules()[$siblingIndex]; + + $this->subject->addRule($ruleToAdd, $sibling); + + self::assertIsInt($ruleToAdd->getColumnNumber(), 'column number not set'); + self::assertGreaterThanOrEqual(0, $ruleToAdd->getColumnNumber(), 'column number not valid'); + } + + /** + * @test + * + * @param list $initialPropertyNames + * + * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName + */ + public function addRuleWithSiblingNotInSetAddsRuleAfterInitialRules( + array $initialPropertyNames, + string $propertyNameToAdd + ): void { + $ruleToAdd = new Rule($propertyNameToAdd); + $this->setRulesFromPropertyNames($initialPropertyNames); + + // `display` is sometimes in `$initialPropertyNames` and sometimes the `$propertyNameToAdd`. + // Choosing this for the bogus sibling allows testing all combinations of whether it is or isn't. + $this->subject->addRule($ruleToAdd, new Rule('display')); + + $rules = $this->subject->getRules(); + self::assertSame($ruleToAdd, \end($rules)); + } + + /** + * @test + * + * @param list $initialPropertyNames + * + * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName + */ + public function addRuleWithSiblingNotInSetSetsValidLineNumber( + array $initialPropertyNames, + string $propertyNameToAdd + ): void { + $ruleToAdd = new Rule($propertyNameToAdd); + $this->setRulesFromPropertyNames($initialPropertyNames); + + // `display` is sometimes in `$initialPropertyNames` and sometimes the `$propertyNameToAdd`. + // Choosing this for the bogus sibling allows testing all combinations of whether it is or isn't. + $this->subject->addRule($ruleToAdd, new Rule('display')); + + self::assertIsInt($ruleToAdd->getLineNumber(), 'line number not set'); + self::assertGreaterThanOrEqual(1, $ruleToAdd->getLineNumber(), 'line number not valid'); + } + + /** + * @test + * + * @param list $initialPropertyNames + * + * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName + */ + public function addRuleWithSiblingNotInSetSetsValidColumnNumber( + array $initialPropertyNames, + string $propertyNameToAdd + ): void { + $ruleToAdd = new Rule($propertyNameToAdd); + $this->setRulesFromPropertyNames($initialPropertyNames); + + // `display` is sometimes in `$initialPropertyNames` and sometimes the `$propertyNameToAdd`. + // Choosing this for the bogus sibling allows testing all combinations of whether it is or isn't. + $this->subject->addRule($ruleToAdd, new Rule('display')); + + self::assertIsInt($ruleToAdd->getColumnNumber(), 'column number not set'); + self::assertGreaterThanOrEqual(0, $ruleToAdd->getColumnNumber(), 'column number not valid'); + } + + /** + * @test + * + * @param non-empty-list $initialPropertyNames + * @param int<0, max> $indexToRemove + * + * @dataProvider provideInitialPropertyNamesAndIndexOfOne + */ + public function removeRuleRemovesRuleInSet(array $initialPropertyNames, int $indexToRemove): void + { + $this->setRulesFromPropertyNames($initialPropertyNames); + $ruleToRemove = $this->subject->getRules()[$indexToRemove]; + + $this->subject->removeRule($ruleToRemove); + + self::assertNotContains($ruleToRemove, $this->subject->getRules()); + } + + /** + * @test + * + * @param non-empty-list $initialPropertyNames + * @param int<0, max> $indexToRemove + * + * @dataProvider provideInitialPropertyNamesAndIndexOfOne + */ + public function removeRuleRemovesExactlyOneRule(array $initialPropertyNames, int $indexToRemove): void + { + $this->setRulesFromPropertyNames($initialPropertyNames); + $ruleToRemove = $this->subject->getRules()[$indexToRemove]; + + $this->subject->removeRule($ruleToRemove); + + self::assertCount(\count($initialPropertyNames) - 1, $this->subject->getRules()); + } + + /** + * @test + * + * @param list $initialPropertyNames + * + * @dataProvider provideInitialPropertyNamesAndAnotherPropertyName + */ + public function removeRuleWithRuleNotInSetKeepsSetUnchanged( + array $initialPropertyNames, + string $propertyNameToRemove + ): void { + $this->setRulesFromPropertyNames($initialPropertyNames); + $initialRules = $this->subject->getRules(); + $ruleToRemove = new Rule($propertyNameToRemove); + + $this->subject->removeRule($ruleToRemove); + + self::assertSame($initialRules, $this->subject->getRules()); + } + + /** + * @return array, 1: string, 2: list}> + */ + public static function providePropertyNamesAndPropertyNameToRemoveAndExpectedRemainingPropertyNames(): array + { + return [ + 'removing single rule' => [ + ['color'], + 'color', + [], + ], + 'removing first rule' => [ + ['color', 'display'], + 'color', + ['display'], + ], + 'removing last rule' => [ + ['color', 'display'], + 'display', + ['color'], + ], + 'removing middle rule' => [ + ['color', 'display', 'width'], + 'display', + ['color', 'width'], + ], + 'removing multiple rules' => [ + ['color', 'color'], + 'color', + [], + ], + 'removing multiple rules with another kept' => [ + ['color', 'color', 'display'], + 'color', + ['display'], + ], + 'removing nonexistent rule from empty list' => [ + [], + 'color', + [], + ], + 'removing nonexistent rule from nonempty list' => [ + ['color', 'display'], + 'width', + ['color', 'display'], + ], + ]; + } + + /** + * @test + * + * @param list $initialPropertyNames + * + * @dataProvider providePropertyNamesAndPropertyNameToRemoveAndExpectedRemainingPropertyNames + */ + public function removeMatchingRulesRemovesRulesWithPropertyName( + array $initialPropertyNames, + string $propertyNameToRemove + ): void { + $this->setRulesFromPropertyNames($initialPropertyNames); + + $this->subject->removeMatchingRules($propertyNameToRemove); + + self::assertArrayNotHasKey($propertyNameToRemove, $this->subject->getRulesAssoc()); + } + + /** + * @test + * + * @param list $initialPropertyNames + * @param list $expectedRemainingPropertyNames + * + * @dataProvider providePropertyNamesAndPropertyNameToRemoveAndExpectedRemainingPropertyNames + */ + public function removeMatchingRulesWithPropertyNameKeepsOtherRules( + array $initialPropertyNames, + string $propertyNameToRemove, + array $expectedRemainingPropertyNames + ): void { + $this->setRulesFromPropertyNames($initialPropertyNames); + + $this->subject->removeMatchingRules($propertyNameToRemove); + + $remainingRules = $this->subject->getRulesAssoc(); + if ($expectedRemainingPropertyNames === []) { + self::assertSame([], $remainingRules); + } + foreach ($expectedRemainingPropertyNames as $expectedPropertyName) { + self::assertArrayHasKey($expectedPropertyName, $remainingRules); + } + } + + /** + * @return array, 1: string, 2: list}> + */ + public static function providePropertyNamesAndPropertyNamePrefixToRemoveAndExpectedRemainingPropertyNames(): array + { + return [ + 'removing shorthand rule' => [ + ['font'], + 'font', + [], + ], + 'removing longhand rule' => [ + ['font-size'], + 'font', + [], + ], + 'removing shorthand and longhand rule' => [ + ['font', 'font-size'], + 'font', + [], + ], + 'removing shorthand rule with another kept' => [ + ['font', 'color'], + 'font', + ['color'], + ], + 'removing longhand rule with another kept' => [ + ['font-size', 'color'], + 'font', + ['color'], + ], + 'keeping other rules whose property names begin with the same characters' => [ + ['contain', 'container', 'container-type'], + 'contain', + ['container', 'container-type'], + ], + ]; + } + + /** + * @test + * + * @param list $initialPropertyNames + * + * @dataProvider providePropertyNamesAndPropertyNamePrefixToRemoveAndExpectedRemainingPropertyNames + */ + public function removeMatchingRulesRemovesRulesWithPropertyNamePrefix( + array $initialPropertyNames, + string $propertyNamePrefix + ): void { + $propertyNamePrefixWithHyphen = $propertyNamePrefix . '-'; + $this->setRulesFromPropertyNames($initialPropertyNames); + + $this->subject->removeMatchingRules($propertyNamePrefixWithHyphen); + + $remainingRules = $this->subject->getRulesAssoc(); + self::assertArrayNotHasKey($propertyNamePrefix, $remainingRules); + foreach (\array_keys($remainingRules) as $remainingPropertyName) { + self::assertStringStartsNotWith($propertyNamePrefixWithHyphen, $remainingPropertyName); + } + } + + /** + * @test + * + * @param list $initialPropertyNames + * @param list $expectedRemainingPropertyNames + * + * @dataProvider providePropertyNamesAndPropertyNamePrefixToRemoveAndExpectedRemainingPropertyNames + */ + public function removeMatchingRulesWithPropertyNamePrefixKeepsOtherRules( + array $initialPropertyNames, + string $propertyNamePrefix, + array $expectedRemainingPropertyNames + ): void { + $propertyNamePrefixWithHyphen = $propertyNamePrefix . '-'; + $this->setRulesFromPropertyNames($initialPropertyNames); + + $this->subject->removeMatchingRules($propertyNamePrefixWithHyphen); + + $remainingRules = $this->subject->getRulesAssoc(); + if ($expectedRemainingPropertyNames === []) { + self::assertSame([], $remainingRules); + } + foreach ($expectedRemainingPropertyNames as $expectedPropertyName) { + self::assertArrayHasKey($expectedPropertyName, $remainingRules); + } + } + + /** + * @test + * + * @param list $propertyNamesToRemove + * + * @dataProvider providePropertyNames + */ + public function removeAllRulesRemovesAllRules(array $propertyNamesToRemove): void + { + $this->setRulesFromPropertyNames($propertyNamesToRemove); + + $this->subject->removeAllRules(); + + self::assertSame([], $this->subject->getRules()); + } + + /** + * @test + * + * @param list $propertyNamesToSet + * + * @dataProvider providePropertyNames + */ + public function setRulesOnVirginSetsRulesWithoutPositionInOrder(array $propertyNamesToSet): void + { + $rulesToSet = self::createRulesFromPropertyNames($propertyNamesToSet); + + $this->subject->setRules($rulesToSet); + + self::assertSame($rulesToSet, $this->subject->getRules()); + } + + /** + * @return DataProvider, 1: list}> + */ + public static function provideInitialPropertyNamesAndPropertyNamesToSet(): DataProvider + { + return DataProvider::cross(self::providePropertyNames(), self::providePropertyNames()); + } + + /** + * @test + * + * @param list $initialPropertyNames + * @param list $propertyNamesToSet + * + * @dataProvider provideInitialPropertyNamesAndPropertyNamesToSet + */ + public function setRulesReplacesRules(array $initialPropertyNames, array $propertyNamesToSet): void + { + $rulesToSet = self::createRulesFromPropertyNames($propertyNamesToSet); + $this->setRulesFromPropertyNames($initialPropertyNames); + + $this->subject->setRules($rulesToSet); + + self::assertSame($rulesToSet, $this->subject->getRules()); + } + + /** + * @test + */ + public function setRulesWithRuleWithoutPositionSetsValidLineNumber(): void + { + $ruleToSet = new Rule('color'); + + $this->subject->setRules([$ruleToSet]); + + self::assertIsInt($ruleToSet->getLineNumber(), 'line number not set'); + self::assertGreaterThanOrEqual(1, $ruleToSet->getLineNumber(), 'line number not valid'); + } + + /** + * @test + */ + public function setRulesWithRuleWithoutPositionSetsValidColumnNumber(): void + { + $ruleToSet = new Rule('color'); + + $this->subject->setRules([$ruleToSet]); + + self::assertIsInt($ruleToSet->getColumnNumber(), 'column number not set'); + self::assertGreaterThanOrEqual(0, $ruleToSet->getColumnNumber(), 'column number not valid'); + } + + /** + * @test + */ + public function setRulesWithRuleWithOnlyLineNumberSetsColumnNumber(): void + { + $ruleToSet = new Rule('color'); + $ruleToSet->setPosition(42); + + $this->subject->setRules([$ruleToSet]); + + self::assertIsInt($ruleToSet->getColumnNumber(), 'column number not set'); + self::assertGreaterThanOrEqual(0, $ruleToSet->getColumnNumber(), 'column number not valid'); + } + + /** + * @test + */ + public function setRulesWithRuleWithOnlyLineNumberPreservesLineNumber(): void + { + $ruleToSet = new Rule('color'); + $ruleToSet->setPosition(42); + + $this->subject->setRules([$ruleToSet]); + + self::assertSame(42, $ruleToSet->getLineNumber(), 'line number not preserved'); + } + + /** + * @test + */ + public function setRulesWithRuleWithOnlyColumnNumberSetsLineNumber(): void + { + $ruleToSet = new Rule('color'); + $ruleToSet->setPosition(null, 42); + + $this->subject->setRules([$ruleToSet]); + + self::assertIsInt($ruleToSet->getLineNumber(), 'line number not set'); + self::assertGreaterThanOrEqual(1, $ruleToSet->getLineNumber(), 'line number not valid'); + } + + /** + * @test + */ + public function setRulesWithRuleWithOnlyColumnNumberPreservesColumnNumber(): void + { + $ruleToSet = new Rule('color'); + $ruleToSet->setPosition(null, 42); + + $this->subject->setRules([$ruleToSet]); + + self::assertSame(42, $ruleToSet->getColumnNumber(), 'column number not preserved'); + } + + /** + * @test + */ + public function setRulesWithRuleWithCompletePositionPreservesPosition(): void + { + $ruleToSet = new Rule('color'); + $ruleToSet->setPosition(42, 64); + + $this->subject->setRules([$ruleToSet]); + + self::assertSame(42, $ruleToSet->getLineNumber(), 'line number not preserved'); + self::assertSame(64, $ruleToSet->getColumnNumber(), 'column number not preserved'); + } + + /** + * @test + * + * @param list $propertyNamesToSet + * + * @dataProvider providePropertyNames + */ + public function getRulesReturnsRulesSet(array $propertyNamesToSet): void + { + $rulesToSet = self::createRulesFromPropertyNames($propertyNamesToSet); + $this->subject->setRules($rulesToSet); + + $result = $this->subject->getRules(); + + self::assertSame($rulesToSet, $result); + } + + /** + * @test + */ + public function getRulesOrdersByLineNumber(): void + { + $first = (new Rule('color'))->setPosition(1, 64); + $second = (new Rule('display'))->setPosition(19, 42); + $third = (new Rule('color'))->setPosition(55, 11); + $this->subject->setRules([$third, $second, $first]); + + $result = $this->subject->getRules(); + + self::assertSame([$first, $second, $third], $result); + } + + /** + * @test + */ + public function getRulesOrdersRulesWithSameLineNumberByColumnNumber(): void + { + $first = (new Rule('color'))->setPosition(1, 11); + $second = (new Rule('display'))->setPosition(1, 42); + $third = (new Rule('color'))->setPosition(1, 64); + $this->subject->setRules([$third, $second, $first]); + + $result = $this->subject->getRules(); + + self::assertSame([$first, $second, $third], $result); + } + + /** + * @return array, 1: string, 2: list}> + */ + public static function providePropertyNamesAndSearchPatternAndMatchingPropertyNames(): array + { + return [ + 'single rule matched' => [ + ['color'], + 'color', + ['color'], + ], + 'first rule matched' => [ + ['color', 'display'], + 'color', + ['color'], + ], + 'last rule matched' => [ + ['color', 'display'], + 'display', + ['display'], + ], + 'middle rule matched' => [ + ['color', 'display', 'width'], + 'display', + ['display'], + ], + 'multiple rules for the same property matched' => [ + ['color', 'color'], + 'color', + ['color'], + ], + 'multiple rules for the same property matched in haystack' => [ + ['color', 'display', 'color', 'width'], + 'color', + ['color'], + ], + 'shorthand rule matched' => [ + ['font'], + 'font-', + ['font'], + ], + 'longhand rule matched' => [ + ['font-size'], + 'font-', + ['font-size'], + ], + 'shorthand and longhand rule matched' => [ + ['font', 'font-size'], + 'font-', + ['font', 'font-size'], + ], + 'shorthand rule matched in haystack' => [ + ['font', 'color'], + 'font-', + ['font'], + ], + 'longhand rule matched in haystack' => [ + ['font-size', 'color'], + 'font-', + ['font-size'], + ], + 'rules whose property names begin with the same characters not matched with pattern match' => [ + ['contain', 'container', 'container-type'], + 'contain-', + ['contain'], + ], + 'rules whose property names begin with the same characters not matched with exact match' => [ + ['contain', 'container', 'container-type'], + 'contain', + ['contain'], + ], + ]; + } + + /** + * @test + * + * @param list $propertyNamesToSet + * @param list $matchingPropertyNames + * + * @dataProvider providePropertyNamesAndSearchPatternAndMatchingPropertyNames + */ + public function getRulesWithPatternReturnsAllMatchingRules( + array $propertyNamesToSet, + string $searchPattern, + array $matchingPropertyNames + ): void { + $rulesToSet = self::createRulesFromPropertyNames($propertyNamesToSet); + // Use `array_values` to ensure canonical numeric array, since `array_filter` does not reindex. + $matchingRules = \array_values( + \array_filter( + $rulesToSet, + static function (Rule $rule) use ($matchingPropertyNames): bool { + return \in_array($rule->getRule(), $matchingPropertyNames, true); + } + ) + ); + $this->subject->setRules($rulesToSet); + + $result = $this->subject->getRules($searchPattern); + + // `Rule`s without pre-set positions are returned in the order set. This is tested separately. + self::assertSame($matchingRules, $result); + } + + /** + * @return array, 1: string}> + */ + public static function providePropertyNamesAndNonMatchingSearchPattern(): array + { + return [ + 'no match in empty list' => [ + [], + 'color', + ], + 'no match for different property' => [ + ['color'], + 'display', + ], + 'no match for property not in list' => [ + ['color', 'display'], + 'width', + ], + ]; + } + + /** + * @test + * + * @param list $propertyNamesToSet + * + * @dataProvider providePropertyNamesAndNonMatchingSearchPattern + */ + public function getRulesWithNonMatchingPatternReturnsEmptyArray( + array $propertyNamesToSet, + string $searchPattern + ): void { + $this->setRulesFromPropertyNames($propertyNamesToSet); + + $result = $this->subject->getRules($searchPattern); + + self::assertSame([], $result); + } + + /** + * @test + */ + public function getRulesWithPatternOrdersRulesByPosition(): void + { + $first = (new Rule('color'))->setPosition(1, 42); + $second = (new Rule('color'))->setPosition(1, 64); + $third = (new Rule('color'))->setPosition(55, 7); + $this->subject->setRules([$third, $second, $first]); + + $result = $this->subject->getRules('color'); + + self::assertSame([$first, $second, $third], $result); + } + + /** + * @return array}> + */ + public static function provideDistinctPropertyNames(): array + { + return [ + 'no properties' => [[]], + 'one property' => [['color']], + 'two properties' => [['color', 'display']], + ]; + } + + /** + * @test + * + * @param list $propertyNamesToSet + * + * @dataProvider provideDistinctPropertyNames + */ + public function getRulesAssocReturnsAllRulesWithDistinctPropertyNames(array $propertyNamesToSet): void + { + $rulesToSet = self::createRulesFromPropertyNames($propertyNamesToSet); + $this->subject->setRules($rulesToSet); + + $result = $this->subject->getRulesAssoc(); + + self::assertSame($rulesToSet, \array_values($result)); + } + + /** + * @test + */ + public function getRulesAssocReturnsLastRuleWithSamePropertyName(): void + { + $firstRule = new Rule('color'); + $lastRule = new Rule('color'); + $this->subject->setRules([$firstRule, $lastRule]); + + $result = $this->subject->getRulesAssoc(); + + self::assertSame([$lastRule], \array_values($result)); + } + + /** + * @test + */ + public function getRulesAssocOrdersRulesByPosition(): void + { + $first = (new Rule('color'))->setPosition(1, 42); + $second = (new Rule('display'))->setPosition(1, 64); + $third = (new Rule('width'))->setPosition(55, 7); + $this->subject->setRules([$third, $second, $first]); + + $result = $this->subject->getRulesAssoc(); + + self::assertSame([$first, $second, $third], \array_values($result)); + } + + /** + * @test + */ + public function getRulesAssocKeysRulesByPropertyName(): void + { + $this->subject->setRules([new Rule('color'), new Rule('display')]); + + $result = $this->subject->getRulesAssoc(); + + foreach ($result as $key => $rule) { + self::assertSame($rule->getRule(), $key); + } + } + + /** + * @test + * + * @param list $propertyNamesToSet + * @param list $matchingPropertyNames + * + * @dataProvider providePropertyNamesAndSearchPatternAndMatchingPropertyNames + */ + public function getRulesAssocWithPatternReturnsAllMatchingPropertyNames( + array $propertyNamesToSet, + string $searchPattern, + array $matchingPropertyNames + ): void { + $this->setRulesFromPropertyNames($propertyNamesToSet); + + $result = $this->subject->getRulesAssoc($searchPattern); + + $resultPropertyNames = \array_keys($result); + \sort($matchingPropertyNames); + \sort($resultPropertyNames); + self::assertSame($matchingPropertyNames, $resultPropertyNames); + } + + /** + * @test + * + * @param list $propertyNamesToSet + * + * @dataProvider providePropertyNamesAndNonMatchingSearchPattern + */ + public function getRulesAssocWithNonMatchingPatternReturnsEmptyArray( + array $propertyNamesToSet, + string $searchPattern + ): void { + $this->setRulesFromPropertyNames($propertyNamesToSet); + + $result = $this->subject->getRulesAssoc($searchPattern); + + self::assertSame([], $result); + } + + /** + * @test + */ + public function getRulesAssocWithPatternOrdersRulesByPosition(): void + { + $first = (new Rule('font'))->setPosition(1, 42); + $second = (new Rule('font-family'))->setPosition(1, 64); + $third = (new Rule('font-weight'))->setPosition(55, 7); + $this->subject->setRules([$third, $second, $first]); + + $result = $this->subject->getRules('font-'); + + self::assertSame([$first, $second, $third], \array_values($result)); + } + + /** + * @param list $propertyNames + */ + private function setRulesFromPropertyNames(array $propertyNames): void + { + $this->subject->setRules(self::createRulesFromPropertyNames($propertyNames)); + } + + /** + * @param list $propertyNames + * + * @return list + */ + private static function createRulesFromPropertyNames(array $propertyNames): array + { + return \array_map( + function (string $propertyName): Rule { + return new Rule($propertyName); + }, + $propertyNames + ); + } +} diff --git a/tests/Unit/RuleSet/RuleSetTest.php b/tests/Unit/RuleSet/RuleSetTest.php new file mode 100644 index 000000000..92c056ca0 --- /dev/null +++ b/tests/Unit/RuleSet/RuleSetTest.php @@ -0,0 +1,82 @@ +subject = new RuleSet(); + } + + /** + * @test + */ + public function implementsCSSElement(): void + { + self::assertInstanceOf(CSSElement::class, $this->subject); + } + + /** + * @test + */ + public function implementsCSSListItem(): void + { + self::assertInstanceOf(CSSListItem::class, $this->subject); + } + + /** + * @test + */ + public function getLineNumberByDefaultReturnsNull(): void + { + $result = $this->subject->getLineNumber(); + + self::assertNull($result); + } + + /** + * @return array|null}> + */ + public function provideLineNumber(): array + { + return [ + 'null' => [null], + 'line 1' => [1], + 'line 42' => [42], + ]; + } + + /** + * @test + * + * @param int<1, max>|null $lineNumber + * + * @dataProvider provideLineNumber + */ + public function getLineNumberReturnsLineNumberPassedToConstructor(?int $lineNumber): void + { + $subject = new RuleSet($lineNumber); + + $result = $subject->getLineNumber(); + + self::assertSame($lineNumber, $result); + } +} diff --git a/tests/SettingsTest.php b/tests/Unit/SettingsTest.php similarity index 85% rename from tests/SettingsTest.php rename to tests/Unit/SettingsTest.php index 642dc8ba6..63f945214 100644 --- a/tests/SettingsTest.php +++ b/tests/Unit/SettingsTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Sabberworm\CSS\Tests; +namespace Sabberworm\CSS\Tests\Unit; use PHPUnit\Framework\TestCase; use Sabberworm\CSS\Settings; @@ -48,7 +48,7 @@ public function createReturnsANewInstanceForEachCall(): void */ public function multibyteSupportByDefaultStateOfMbStringExtension(): void { - self::assertSame(\extension_loaded('mbstring'), $this->subject->bMultibyteSupport); + self::assertSame(\extension_loaded('mbstring'), $this->subject->hasMultibyteSupport()); } /** @@ -78,7 +78,7 @@ public function withMultibyteSupportSetsMultibyteSupport(bool $value): void { $this->subject->withMultibyteSupport($value); - self::assertSame($value, $this->subject->bMultibyteSupport); + self::assertSame($value, $this->subject->hasMultibyteSupport()); } /** @@ -86,7 +86,7 @@ public function withMultibyteSupportSetsMultibyteSupport(bool $value): void */ public function defaultCharsetByDefaultIsUtf8(): void { - self::assertSame('utf-8', $this->subject->sDefaultCharset); + self::assertSame('utf-8', $this->subject->getDefaultCharset()); } /** @@ -105,7 +105,7 @@ public function withDefaultCharsetSetsDefaultCharset(): void $charset = 'ISO-8859-1'; $this->subject->withDefaultCharset($charset); - self::assertSame($charset, $this->subject->sDefaultCharset); + self::assertSame($charset, $this->subject->getDefaultCharset()); } /** @@ -113,7 +113,7 @@ public function withDefaultCharsetSetsDefaultCharset(): void */ public function lenientParsingByDefaultIsTrue(): void { - self::assertTrue($this->subject->bLenientParsing); + self::assertTrue($this->subject->usesLenientParsing()); } /** @@ -132,7 +132,7 @@ public function withLenientParsingSetsLenientParsing(bool $value): void { $this->subject->withLenientParsing($value); - self::assertSame($value, $this->subject->bLenientParsing); + self::assertSame($value, $this->subject->usesLenientParsing()); } /** @@ -150,6 +150,6 @@ public function beStrictSetsLenientParsingToFalse(): void { $this->subject->beStrict(); - self::assertFalse($this->subject->bLenientParsing); + self::assertFalse($this->subject->usesLenientParsing()); } } diff --git a/tests/Unit/Value/CSSStringTest.php b/tests/Unit/Value/CSSStringTest.php new file mode 100644 index 000000000..c54ecdd75 --- /dev/null +++ b/tests/Unit/Value/CSSStringTest.php @@ -0,0 +1,83 @@ +getString()); + } + + /** + * @test + */ + public function setStringSetsString(): void + { + $subject = new CSSString(''); + $string = 'coffee'; + + $subject->setString($string); + + self::assertSame($string, $subject->getString()); + } + + /** + * @test + */ + public function getLineNumberByDefaultReturnsNull(): void + { + $subject = new CSSString(''); + + self::assertNull($subject->getLineNumber()); + } + + /** + * @test + */ + public function getLineNumberReturnsLineNumberProvidedToConstructor(): void + { + $lineNumber = 42; + $subject = new CSSString('', $lineNumber); + + self::assertSame($lineNumber, $subject->getLineNumber()); + } +} diff --git a/tests/Value/CalcRuleValueListTest.php b/tests/Unit/Value/CalcRuleValueListTest.php similarity index 64% rename from tests/Value/CalcRuleValueListTest.php rename to tests/Unit/Value/CalcRuleValueListTest.php index 085548dac..7314e3c51 100644 --- a/tests/Value/CalcRuleValueListTest.php +++ b/tests/Unit/Value/CalcRuleValueListTest.php @@ -1,6 +1,8 @@ getLineNo()); + self::assertNull($subject->getLineNumber()); } /** * @test */ - public function getLineNoReturnsLineNumberProvidedToConstructor(): void + public function getLineNumberReturnsLineNumberProvidedToConstructor(): void { $lineNumber = 42; - $subject = new CalcRuleValueList($lineNumber); - self::assertSame($lineNumber, $subject->getLineNo()); + self::assertSame($lineNumber, $subject->getLineNumber()); } /** diff --git a/tests/Unit/Value/ColorTest.php b/tests/Unit/Value/ColorTest.php new file mode 100644 index 000000000..aaa257553 --- /dev/null +++ b/tests/Unit/Value/ColorTest.php @@ -0,0 +1,474 @@ + + */ + public static function provideValidColorAndExpectedRendering(): array + { + return [ + '3-digit hex color' => [ + '#070', + '#070', + ], + '6-digit hex color that can be represented as 3-digit' => [ + '#007700', + '#070', + ], + '6-digit hex color that cannot be represented as 3-digit' => [ + '#007600', + '#007600', + ], + '4-digit hex color (with alpha)' => [ + '#0707', + 'rgba(0,119,0,.47)', + ], + '8-digit hex color (with alpha)' => [ + '#0077007F', + 'rgba(0,119,0,.5)', + ], + 'legacy rgb that can be represented as 3-digit hex' => [ + 'rgb(0, 119, 0)', + '#070', + ], + 'legacy rgb that cannot be represented as 3-digit hex' => [ + 'rgb(0, 118, 0)', + '#007600', + ], + 'legacy rgb with percentage components' => [ + 'rgb(0%, 60%, 0%)', + 'rgb(0%,60%,0%)', + ], + 'legacy rgba with fractional alpha' => [ + 'rgba(0, 119, 0, 0.5)', + 'rgba(0,119,0,.5)', + ], + 'legacy rgba with percentage alpha' => [ + 'rgba(0, 119, 0, 50%)', + 'rgba(0,119,0,50%)', + ], + 'legacy rgba with percentage components and fractional alpha' => [ + 'rgba(0%, 60%, 0%, 0.5)', + 'rgba(0%,60%,0%,.5)', + ], + 'legacy rgba with percentage components and percentage alpha' => [ + 'rgba(0%, 60%, 0%, 50%)', + 'rgba(0%,60%,0%,50%)', + ], + 'legacy rgb as rgba' => [ + 'rgba(0, 119, 0)', + '#070', + ], + 'legacy rgba as rgb' => [ + 'rgb(0, 119, 0, 0.5)', + 'rgba(0,119,0,.5)', + ], + 'modern rgb' => [ + 'rgb(0 119 0)', + '#070', + ], + 'modern rgb with percentage R' => [ + 'rgb(0% 119 0)', + 'rgb(0% 119 0)', + ], + 'modern rgb with percentage G' => [ + 'rgb(0 60% 0)', + 'rgb(0 60% 0)', + ], + 'modern rgb with percentage B' => [ + 'rgb(0 119 0%)', + 'rgb(0 119 0%)', + ], + 'modern rgb with percentage R&G' => [ + 'rgb(0% 60% 0)', + 'rgb(0% 60% 0)', + ], + 'modern rgb with percentage R&B' => [ + 'rgb(0% 119 0%)', + 'rgb(0% 119 0%)', + ], + 'modern rgb with percentage G&B' => [ + 'rgb(0 60% 0%)', + 'rgb(0 60% 0%)', + ], + 'modern rgb with percentage components' => [ + 'rgb(0% 60% 0%)', + 'rgb(0%,60%,0%)', + ], + 'modern rgb with none as red' => [ + 'rgb(none 119 0)', + 'rgb(none 119 0)', + ], + 'modern rgb with none as green' => [ + 'rgb(0 none 0)', + 'rgb(0 none 0)', + ], + 'modern rgb with none as blue' => [ + 'rgb(0 119 none)', + 'rgb(0 119 none)', + ], + 'modern rgba with fractional alpha' => [ + 'rgb(0 119 0 / 0.5)', + 'rgba(0,119,0,.5)', + ], + 'modern rgba with percentage alpha' => [ + 'rgb(0 119 0 / 50%)', + 'rgba(0,119,0,50%)', + ], + 'modern rgba with percentage R' => [ + 'rgb(0% 119 0 / 0.5)', + 'rgba(0% 119 0/.5)', + ], + 'modern rgba with percentage G' => [ + 'rgb(0 60% 0 / 0.5)', + 'rgba(0 60% 0/.5)', + ], + 'modern rgba with percentage B' => [ + 'rgb(0 119 0% / 0.5)', + 'rgba(0 119 0%/.5)', + ], + 'modern rgba with percentage RGB' => [ + 'rgb(0% 60% 0% / 0.5)', + 'rgba(0%,60%,0%,.5)', + ], + 'modern rgba with percentage components' => [ + 'rgb(0% 60% 0% / 50%)', + 'rgba(0%,60%,0%,50%)', + ], + 'modern rgba with none as alpha' => [ + 'rgb(0 119 0 / none)', + 'rgba(0 119 0/none)', + ], + 'legacy rgb with var for R' => [ + 'rgb(var(--r), 119, 0)', + 'rgb(var(--r),119,0)', + ], + 'legacy rgb with var for G' => [ + 'rgb(0, var(--g), 0)', + 'rgb(0,var(--g),0)', + ], + 'legacy rgb with var for B' => [ + 'rgb(0, 119, var(--b))', + 'rgb(0,119,var(--b))', + ], + 'legacy rgb with var for RG' => [ + 'rgb(var(--rg), 0)', + 'rgb(var(--rg),0)', + ], + 'legacy rgb with var for GB' => [ + 'rgb(0, var(--gb))', + 'rgb(0,var(--gb))', + ], + 'legacy rgba with var for R' => [ + 'rgba(var(--r), 119, 0, 0.5)', + 'rgba(var(--r),119,0,.5)', + ], + 'legacy rgba with var for G' => [ + 'rgba(0, var(--g), 0, 0.5)', + 'rgba(0,var(--g),0,.5)', + ], + 'legacy rgba with var for B' => [ + 'rgb(0, 119, var(--b), 0.5)', + 'rgb(0,119,var(--b),.5)', + ], + 'legacy rgba with var for A' => [ + 'rgba(0, 119, 0, var(--a))', + 'rgba(0,119,0,var(--a))', + ], + 'legacy rgba with var for RG' => [ + 'rgba(var(--rg), 0, 0.5)', + 'rgba(var(--rg),0,.5)', + ], + 'legacy rgba with var for GB' => [ + 'rgba(0, var(--gb), 0.5)', + 'rgba(0,var(--gb),.5)', + ], + 'legacy rgba with var for BA' => [ + 'rgba(0, 119, var(--ba))', + 'rgba(0,119,var(--ba))', + ], + 'legacy rgba with var for RGB' => [ + 'rgba(var(--rgb), 0.5)', + 'rgba(var(--rgb),.5)', + ], + 'legacy rgba with var for GBA' => [ + 'rgba(0, var(--gba))', + 'rgba(0,var(--gba))', + ], + 'modern rgb with var for R' => [ + 'rgb(var(--r) 119 0)', + 'rgb(var(--r),119,0)', + ], + 'modern rgb with var for G' => [ + 'rgb(0 var(--g) 0)', + 'rgb(0,var(--g),0)', + ], + 'modern rgb with var for B' => [ + 'rgb(0 119 var(--b))', + 'rgb(0,119,var(--b))', + ], + 'modern rgb with var for RG' => [ + 'rgb(var(--rg) 0)', + 'rgb(var(--rg),0)', + ], + 'modern rgb with var for GB' => [ + 'rgb(0 var(--gb))', + 'rgb(0,var(--gb))', + ], + 'modern rgba with var for R' => [ + 'rgba(var(--r) 119 0 / 0.5)', + 'rgba(var(--r),119,0,.5)', + ], + 'modern rgba with var for G' => [ + 'rgba(0 var(--g) 0 / 0.5)', + 'rgba(0,var(--g),0,.5)', + ], + 'modern rgba with var for B' => [ + 'rgba(0 119 var(--b) / 0.5)', + 'rgba(0,119,var(--b),.5)', + ], + 'modern rgba with var for A' => [ + 'rgba(0 119 0 / var(--a))', + 'rgba(0,119,0,var(--a))', + ], + 'modern rgba with var for RG' => [ + 'rgba(var(--rg) 0 / 0.5)', + 'rgba(var(--rg),0,.5)', + ], + 'modern rgba with var for GB' => [ + 'rgba(0 var(--gb) / 0.5)', + 'rgba(0,var(--gb),.5)', + ], + 'modern rgba with var for BA' => [ + 'rgba(0 119 var(--ba))', + 'rgba(0,119,var(--ba))', + ], + 'modern rgba with var for RGB' => [ + 'rgba(var(--rgb) / 0.5)', + 'rgba(var(--rgb),.5)', + ], + 'modern rgba with var for GBA' => [ + 'rgba(0 var(--gba))', + 'rgba(0,var(--gba))', + ], + 'rgba with var for RGBA' => [ + 'rgba(var(--rgba))', + 'rgba(var(--rgba))', + ], + 'legacy hsl' => [ + 'hsl(120, 100%, 25%)', + 'hsl(120,100%,25%)', + ], + 'legacy hsl with deg' => [ + 'hsl(120deg, 100%, 25%)', + 'hsl(120deg,100%,25%)', + ], + 'legacy hsl with grad' => [ + 'hsl(133grad, 100%, 25%)', + 'hsl(133grad,100%,25%)', + ], + 'legacy hsl with rad' => [ + 'hsl(2.094rad, 100%, 25%)', + 'hsl(2.094rad,100%,25%)', + ], + 'legacy hsl with turn' => [ + 'hsl(0.333turn, 100%, 25%)', + 'hsl(.333turn,100%,25%)', + ], + 'legacy hsla with fractional alpha' => [ + 'hsla(120, 100%, 25%, 0.5)', + 'hsla(120,100%,25%,.5)', + ], + 'legacy hsla with percentage alpha' => [ + 'hsla(120, 100%, 25%, 50%)', + 'hsla(120,100%,25%,50%)', + ], + 'legacy hsl as hsla' => [ + 'hsla(120, 100%, 25%)', + 'hsl(120,100%,25%)', + ], + 'legacy hsla as hsl' => [ + 'hsl(120, 100%, 25%, 0.5)', + 'hsla(120,100%,25%,.5)', + ], + 'modern hsl' => [ + 'hsl(120 100% 25%)', + 'hsl(120,100%,25%)', + ], + 'modern hsl with none as hue' => [ + 'hsl(none 100% 25%)', + 'hsl(none 100% 25%)', + ], + 'modern hsl with none as saturation' => [ + 'hsl(120 none 25%)', + 'hsl(120 none 25%)', + ], + 'modern hsl with none as lightness' => [ + 'hsl(120 100% none)', + 'hsl(120 100% none)', + ], + 'modern hsla' => [ + 'hsl(120 100% 25% / 0.5)', + 'hsla(120,100%,25%,.5)', + ], + 'modern hsla with none as alpha' => [ + 'hsl(120 100% 25% / none)', + 'hsla(120 100% 25%/none)', + ], + ]; + } + + /** + * @test + * + * @dataProvider provideValidColorAndExpectedRendering + */ + public function parsesAndRendersValidColor(string $color, string $expectedRendering): void + { + $subject = Color::parse(new ParserState($color, Settings::create())); + + $renderedResult = $subject->render(OutputFormat::create()); + + self::assertSame($expectedRendering, $renderedResult); + } + + /** + * Browsers reject all these, thus so should the parser. + * + * @return array + */ + public static function provideInvalidColor(): array + { + return [ + 'hex color with 0 digits' => [ + '#', + ], + 'hex color with 1 digit' => [ + '#f', + ], + 'hex color with 2 digits' => [ + '#f0', + ], + 'hex color with 5 digits' => [ + '#ff000', + ], + 'hex color with 7 digits' => [ + '#ff00000', + ], + 'hex color with 9 digits' => [ + '#ff0000000', + ], + 'rgb color with 0 arguments' => [ + 'rgb()', + ], + 'rgb color with 1 argument' => [ + 'rgb(255)', + ], + 'legacy rgb color with 2 arguments' => [ + 'rgb(255, 0)', + ], + 'legacy rgb color with 5 arguments' => [ + 'rgb(255, 0, 0, 0.5, 0)', + ], + /* + 'legacy rgb color with invalid unit' => [ + 'rgb(255, 0px, 0)', + ], + //*/ + 'legacy rgb color with none as red' => [ + 'rgb(none, 0, 0)', + ], + 'legacy rgb color with none as green' => [ + 'rgb(255, none, 0)', + ], + 'legacy rgb color with none as blue' => [ + 'rgb(255, 0, none)', + ], + 'legacy rgba color with none as alpha' => [ + 'rgba(255, 0, 0, none)', + ], + 'modern rgb color without slash separator for alpha' => [ + 'rgb(255 0 0 0.5)', + ], + 'rgb color with mixed separators, comma first' => [ + 'rgb(255, 0 0)', + ], + 'rgb color with mixed separators, space first' => [ + 'rgb(255 0, 0)', + ], + 'hsl color with 0 arguments' => [ + 'hsl()', + ], + 'hsl color with 1 argument' => [ + 'hsl(0)', + ], + 'legacy hsl color with 2 arguments' => [ + 'hsl(0, 100%)', + ], + 'legacy hsl color with 5 arguments' => [ + 'hsl(0, 100%, 50%, 0.5, 0)', + ], + 'legacy hsl color with none as hue' => [ + 'hsl(none, 100%, 50%)', + ], + 'legacy hsl color with none as saturation' => [ + 'hsl(0, none, 50%)', + ], + 'legacy hsl color with none as lightness' => [ + 'hsl(0, 100%, none)', + ], + 'legacy hsla color with none as alpha' => [ + 'hsl(0, 100%, 50%, none)', + ], + /* + 'legacy hsl color without % for S/L units' => [ + 'hsl(0, 1, 0.5)' + ], + 'legacy hsl color with invalid unit for H' => [ + 'hsl(0px, 100%, 50%)' + ], + //*/ + 'modern hsl color without slash separator for alpha' => [ + 'rgb(0 100% 50% 0.5)', + ], + 'hsl color with mixed separators, comma first' => [ + 'hsl(0, 100% 50%)', + ], + 'hsl color with mixed separators, space first' => [ + 'hsl(0 100%, 50%)', + ], + ]; + } + + /** + * @test + * + * @dataProvider provideInvalidColor + */ + public function throwsExceptionWithInvalidColor(string $color): void + { + $this->expectException(SourceException::class); + + Color::parse(new ParserState($color, Settings::create())); + } +} diff --git a/tests/Unit/Value/Fixtures/ConcreteValue.php b/tests/Unit/Value/Fixtures/ConcreteValue.php new file mode 100644 index 000000000..b6e924805 --- /dev/null +++ b/tests/Unit/Value/Fixtures/ConcreteValue.php @@ -0,0 +1,19 @@ + + */ + public static function provideUnit(): array + { + $units = [ + 'px', + 'pt', + 'pc', + 'cm', + 'mm', + 'mozmm', + 'in', + 'vh', + 'dvh', + 'svh', + 'lvh', + 'vw', + 'vmin', + 'vmax', + 'rem', + '%', + 'em', + 'ex', + 'ch', + 'fr', + 'deg', + 'grad', + 'rad', + 's', + 'ms', + 'turn', + 'Hz', + 'kHz', + ]; + + return \array_combine( + $units, + \array_map( + static function (string $unit): array { + return [$unit]; + }, + $units + ) + ); + } + + /** + * @test + * + * @param non-empty-string $unit + * + * @dataProvider provideUnit + */ + public function parsesUnit(string $unit): void + { + $parsedSize = Size::parse(new ParserState('1' . $unit, Settings::create())); + + self::assertSame($unit, $parsedSize->getUnit()); + } +} diff --git a/tests/Unit/Value/URLTest.php b/tests/Unit/Value/URLTest.php new file mode 100644 index 000000000..66654e647 --- /dev/null +++ b/tests/Unit/Value/URLTest.php @@ -0,0 +1,83 @@ +getURL()); + } + + /** + * @test + */ + public function setUrlReplacesUrl(): void + { + $subject = new URL(new CSSString('http://example.com')); + + $newUrl = new CSSString('http://example.org'); + $subject->setURL($newUrl); + + self::assertSame($newUrl, $subject->getURL()); + } + + /** + * @test + */ + public function getLineNumberByDefaultReturnsNull(): void + { + $subject = new URL(new CSSString('http://example.com')); + + self::assertNull($subject->getLineNumber()); + } + + /** + * @test + */ + public function getLineNumberReturnsLineNumberProvidedToConstructor(): void + { + $lineNumber = 42; + $subject = new URL(new CSSString('http://example.com'), $lineNumber); + + self::assertSame($lineNumber, $subject->getLineNumber()); + } +} diff --git a/tests/Value/ValueTest.php b/tests/Unit/Value/ValueTest.php similarity index 70% rename from tests/Value/ValueTest.php rename to tests/Unit/Value/ValueTest.php index a67cbe814..9a664a771 100644 --- a/tests/Value/ValueTest.php +++ b/tests/Unit/Value/ValueTest.php @@ -2,11 +2,15 @@ declare(strict_types=1); -namespace Sabberworm\CSS\Tests\Value; +namespace Sabberworm\CSS\Tests\Unit\Value; use PHPUnit\Framework\TestCase; +use Sabberworm\CSS\CSSElement; +use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Parsing\ParserState; use Sabberworm\CSS\Settings; +use Sabberworm\CSS\Tests\Unit\Value\Fixtures\ConcreteValue; +use Sabberworm\CSS\Value\CSSFunction; use Sabberworm\CSS\Value\Value; /** @@ -17,28 +21,33 @@ final class ValueTest extends TestCase /** * the default set of delimiters for parsing most values * - * @see \Rule\Rule::listDelimiterForRule + * @see \Sabberworm\CSS\Rule\Rule::listDelimiterForRule * - * @var array + * @var list */ private const DEFAULT_DELIMITERS = [',', ' ', '/']; + /** + * @test + */ + public function implementsCSSElement(): void + { + $subject = new ConcreteValue(); + + self::assertInstanceOf(CSSElement::class, $subject); + } + /** * @return array */ public static function provideArithmeticOperator(): array { - $units = ['+', '-', '*', '/']; - - return \array_combine( - $units, - \array_map( - function (string $unit): array { - return [$unit]; - }, - $units - ) - ); + return [ + '+' => ['+'], + '-' => ['-'], + '*' => ['*'], + '/' => ['/'], + ]; } /** @@ -53,11 +62,12 @@ public function parsesArithmeticInFunctions(string $operator): void self::DEFAULT_DELIMITERS ); - self::assertSame('max(300px,50vh ' . $operator . ' 10px)', (string) $subject); + self::assertInstanceOf(CSSFunction::class, $subject); + self::assertSame('max(300px,50vh ' . $operator . ' 10px)', $subject->render(OutputFormat::createCompact())); } /** - * @return array + * @return array * The first datum is a template for the parser (using `sprintf` insertion marker `%s` for some expression). * The second is for the expected result, which may have whitespace and trailing semicolon removed. */ @@ -95,7 +105,11 @@ public function parsesArithmeticWithMultipleOperatorsInFunctions( self::DEFAULT_DELIMITERS ); - self::assertSame(\sprintf($expectedResultTemplate, $expression), (string) $subject); + self::assertInstanceOf(CSSFunction::class, $subject); + self::assertSame( + \sprintf($expectedResultTemplate, $expression), + $subject->render(OutputFormat::createCompact()) + ); } /** @@ -123,6 +137,10 @@ public function parsesArithmeticWithMalformedOperandsInFunctions(string $leftOpe self::DEFAULT_DELIMITERS ); - self::assertSame('max(300px,' . $leftOperand . ' + ' . $rightOperand . ')', (string) $subject); + self::assertInstanceOf(CSSFunction::class, $subject); + self::assertSame( + 'max(300px,' . $leftOperand . ' + ' . $rightOperand . ')', + $subject->render(OutputFormat::createCompact()) + ); } } diff --git a/tests/UnitDeprecated/.gitkeep b/tests/UnitDeprecated/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/tests/Value/SizeTest.php b/tests/Value/SizeTest.php deleted file mode 100644 index 3aecce051..000000000 --- a/tests/Value/SizeTest.php +++ /dev/null @@ -1,51 +0,0 @@ - - */ - public static function provideUnit(): array - { - $units = [ - 'px', 'pt', 'pc', - 'cm', 'mm', 'mozmm', 'in', - 'vh', 'dvh', 'svh', 'lvh', - 'vw', 'vmin', 'vmax', 'rem', - '%', 'em', 'ex', 'ch', 'fr', - 'deg', 'grad', 'rad', 's', 'ms', 'turn', 'Hz', 'kHz', - ]; - - return \array_combine( - $units, - \array_map( - function (string $unit): array { - return [$unit]; - }, - $units - ) - ); - } - - /** - * @test - * - * @dataProvider provideUnit - */ - public function parsesUnit(string $unit): void - { - $subject = Size::parse(new ParserState('1' . $unit, Settings::create())); - - self::assertSame($unit, $subject->getUnit()); - } -} diff --git a/tests/fixtures/-fault-tolerance.css b/tests/fixtures/-fault-tolerance.css index 7a9221570..7d4e61051 100644 --- a/tests/fixtures/-fault-tolerance.css +++ b/tests/fixtures/-fault-tolerance.css @@ -1,15 +1,15 @@ .test1 { - //gaga: hello; + //gaga: hello; } .test2 { - *hello: 1; - hello: 2.2; - hello: 2000000000000.2; + *hello: 1; + hello: 2.2; + hello: 2000000000000.2; } #test { - #hello: 1} + #hello: 1} #test2 { - help: none; \ No newline at end of file + help: none; diff --git a/tests/fixtures/1readme.css b/tests/fixtures/1readme.css index f782fad91..adfa9f997 100644 --- a/tests/fixtures/1readme.css +++ b/tests/fixtures/1readme.css @@ -4,7 +4,7 @@ font-family: "CrassRoots"; src: url("../media/cr.ttf") } - + html, body { - font-size: 1.6em + font-size: 1.6em } diff --git a/tests/fixtures/2readme.css b/tests/fixtures/2readme.css index 9b8dbccab..8deae9808 100644 --- a/tests/fixtures/2readme.css +++ b/tests/fixtures/2readme.css @@ -1,5 +1,5 @@ #header { - margin: 10px 2em 1cm 2%; - font-family: Verdana, Helvetica, "Gill Sans", sans-serif; - color: red !important; + margin: 10px 2em 1cm 2%; + font-family: Verdana, Helvetica, "Gill Sans", sans-serif; + color: red !important; } diff --git a/tests/fixtures/atrules.css b/tests/fixtures/atrules.css index 58b27c1f6..d4b4d21a9 100644 --- a/tests/fixtures/atrules.css +++ b/tests/fixtures/atrules.css @@ -1,28 +1,28 @@ @charset "utf-8"; @font-face { - font-family: "CrassRoots"; - src: url("../media/cr.ttf") + font-family: "CrassRoots"; + src: url("../media/cr.ttf") } html, body { - font-size: -0.6em + font-size: -0.6em } @keyframes mymove { - from { top: 0px; } - to { top: 200px; } + from { top: 0px; } + to { top: 200px; } } @-moz-keyframes some-move { - from { top: 0px; } - to { top: 200px; } + from { top: 0px; } + to { top: 200px; } } @supports ( (perspective: 10px) or (-moz-perspective: 10px) or (-webkit-perspective: 10px) or (-ms-perspective: 10px) or (-o-perspective: 10px) ) { - body { - font-family: 'Helvetica'; - } + body { + font-family: 'Helvetica'; + } } @page :pseudo-class { @@ -54,4 +54,4 @@ html, body { @region-style #intro { p { color: blue; } -} \ No newline at end of file +} diff --git a/tests/fixtures/calc.css b/tests/fixtures/calc.css index 9fd0d9735..e794ade68 100644 --- a/tests/fixtures/calc.css +++ b/tests/fixtures/calc.css @@ -1,7 +1,7 @@ div { width: calc(100% / 4); } div { margin-top: calc(-120% - 4px); } div { - height: -webkit-calc(9/16 * 100%)!important; - width: -moz-calc((50px - 50%)*2); + height: calc(9/16 * 100%)!important; + width: calc((50px - 50%)*2); } div { width: calc(50% - ( ( 4% ) * 0.5 ) ); } diff --git a/tests/fixtures/case-insensitivity.css b/tests/fixtures/case-insensitivity.css index 43716029d..bfc20ffed 100644 --- a/tests/fixtures/case-insensitivity.css +++ b/tests/fixtures/case-insensitivity.css @@ -6,10 +6,10 @@ } #myid { - CaSe: insensitive !imPORTANT; - frequency: 30hz; - font-size: 1EM; - color: RGB(255, 255, 0); - color: hSL(40, 40%, 30%); - font-Family: Arial; /* The value needs to remain capitalized */ -} \ No newline at end of file + CaSe: insensitive !imPORTANT; + frequency: 30hz; + font-size: 1EM; + color: RGB(255, 255, 0); + color: hSL(40, 40%, 30%); + font-Family: Arial; /* The value needs to remain capitalized */ +} diff --git a/tests/fixtures/colortest.css b/tests/fixtures/colortest.css index f834aa772..f344137b4 100644 --- a/tests/fixtures/colortest.css +++ b/tests/fixtures/colortest.css @@ -1,29 +1,29 @@ #mine { - color: red; - border-color: rgb(10, 100, 230); - border-color: rgba(10, 100, 231, 0.3); - outline-color: #222; - background-color: #232323; + color: red; + border-color: rgb(10, 100, 230); + border-color: rgba(10, 100, 231, 0.3); + outline-color: #222; + background-color: #232323; } #yours { - background-color: hsl(220, 10%, 220%); - background-color: hsla(220, 10%, 220%, 0.3); - outline-color: #22; + background-color: hsl(220, 10%, 220%); + background-color: hsla(220, 10%, 220%, 0.3); + outline-color: #22; } #variables { - background-color: rgb(var(--some-rgb)); - background-color: rgb(var(--r), var(--g), var(--b)); - background-color: rgb(255, var(--g), var(--b)); - background-color: rgb(255, 255, var(--b)); - background-color: rgb(255, var(--rg)); + background-color: rgb(var(--some-rgb)); + background-color: rgb(var(--r), var(--g), var(--b)); + background-color: rgb(255, var(--g), var(--b)); + background-color: rgb(255, 255, var(--b)); + background-color: rgb(255, var(--rg)); - background-color: hsl(var(--some-hsl)); + background-color: hsl(var(--some-hsl)); } #variables-alpha { - background-color: rgba(var(--some-rgb), 0.1); - background-color: rgba(var(--some-rg), 255, 0.1); - background-color: hsla(var(--some-hsl), 0.1); + background-color: rgba(var(--some-rgb), 0.1); + background-color: rgba(var(--some-rg), 255, 0.1); + background-color: hsla(var(--some-hsl), 0.1); } diff --git a/tests/fixtures/create-shorthands.css b/tests/fixtures/create-shorthands.css deleted file mode 100644 index 721980436..000000000 --- a/tests/fixtures/create-shorthands.css +++ /dev/null @@ -1,6 +0,0 @@ -body { - font-size: 2em; font-family: Helvetica,Arial,sans-serif; font-weight: bold; - border-width: 2px; border-color: #999; border-style: dotted; - background-color: #fff; background-image: url('foobar.png'); background-repeat: repeat-y; - margin-top: 2px; margin-right: 3px; margin-bottom: 4px; margin-left: 5px; -} diff --git a/tests/fixtures/escaped-tokens.css b/tests/fixtures/escaped-tokens.css index 333c65692..97e2dfeb3 100644 --- a/tests/fixtures/escaped-tokens.css +++ b/tests/fixtures/escaped-tokens.css @@ -2,6 +2,6 @@ * Special case function-like tokens, with an escape backslash followed by a non-newline and non-hex digit character, should be parsed as the appropriate \Sabberworm\CSS\Value\ type */ body { - background: u\rl("//example.org/picture.jpg"); - height: ca\lc(100% - 1px); -} + background: u\rl("//example.org/picture.jpg"); + height: ca\lc(100% - 1px); +} diff --git a/tests/fixtures/expand-shorthands.css b/tests/fixtures/expand-shorthands.css deleted file mode 100644 index 89aab1e21..000000000 --- a/tests/fixtures/expand-shorthands.css +++ /dev/null @@ -1,7 +0,0 @@ -body { - font: italic 500 14px/1.618 "Trebuchet MS", Georgia, serif; - border: 2px solid #f0f; - background: #ccc url("/images/foo.png") no-repeat left top; - margin: 1em !important; - padding: 2px 6px 3px; -} diff --git a/tests/fixtures/ie-hacks.css b/tests/fixtures/ie-hacks.css deleted file mode 100644 index 3f5f215eb..000000000 --- a/tests/fixtures/ie-hacks.css +++ /dev/null @@ -1,9 +0,0 @@ -p { - padding-right: .75rem \9; - background-image: none \9; - color:red\9\0; - background-color:red \9 \0; - background-color:red \9 \0 !important; - content: "red \9\0"; - content: "red\0abc"; -} diff --git a/tests/fixtures/ie.css b/tests/fixtures/ie.css index 6c0fb3811..9f070e24a 100644 --- a/tests/fixtures/ie.css +++ b/tests/fixtures/ie.css @@ -1,6 +1,6 @@ .nav-thumb-wrapper:hover img, a.activeSlide img { - filter: alpha(opacity=100); - -moz-opacity: 1; - -khtml-opacity: 1; - opacity: 1; -} + filter: alpha(opacity=100); + -moz-opacity: 1; + -khtml-opacity: 1; + opacity: 1; +} diff --git a/tests/fixtures/inner-color.css b/tests/fixtures/inner-color.css index 7fb28b6db..541a65f30 100644 --- a/tests/fixtures/inner-color.css +++ b/tests/fixtures/inner-color.css @@ -1,3 +1,3 @@ test { - background: -webkit-gradient(linear, 0 0, 0 bottom, from(#006cad), to(hsl(202, 100%, 49%))); -} \ No newline at end of file + background: -webkit-gradient(linear, 0 0, 0 bottom, from(#006cad), to(hsl(202, 100%, 49%))); +} diff --git a/tests/fixtures/missing-property-value.css b/tests/fixtures/missing-property-value.css index 33eb473d2..22d87c8f4 100644 --- a/tests/fixtures/missing-property-value.css +++ b/tests/fixtures/missing-property-value.css @@ -1,4 +1,4 @@ div { - display: inline-block; - display: + display: inline-block; + display: } diff --git a/tests/fixtures/namespaces.css b/tests/fixtures/namespaces.css index c396c9745..3577c026d 100644 --- a/tests/fixtures/namespaces.css +++ b/tests/fixtures/namespaces.css @@ -10,9 +10,9 @@ foo|test { - gaga: 1; + gaga: 1; } |test { - gaga: 2; -} \ No newline at end of file + gaga: 2; +} diff --git a/tests/fixtures/nested.css b/tests/fixtures/nested.css index b59dc80e2..e1f41fe69 100644 --- a/tests/fixtures/nested.css +++ b/tests/fixtures/nested.css @@ -1,17 +1,17 @@ html { - some: -test(val1); + some: -test(val1); } html { - some-other: -test(val1); + some-other: -test(val1); } @media screen { - html { - some: -test(val2); - } + html { + some: -test(val2); + } } #unrelated { - other: yes; + other: yes; } diff --git a/tests/fixtures/slashed.css b/tests/fixtures/slashed.css index 5b629be5b..86f4ee826 100644 --- a/tests/fixtures/slashed.css +++ b/tests/fixtures/slashed.css @@ -1,4 +1,4 @@ .test { - font: 12px/1.5 Verdana, Arial, sans-serif; - border-radius: 5px 10px 5px 10px / 10px 5px 10px 5px; + font: 12px/1.5 Verdana, Arial, sans-serif; + border-radius: 5px 10px 5px 10px / 10px 5px 10px 5px; } diff --git a/tests/fixtures/specificity.css b/tests/fixtures/specificity.css index 82a2939a1..1a7a0121b 100644 --- a/tests/fixtures/specificity.css +++ b/tests/fixtures/specificity.css @@ -3,5 +3,5 @@ .help:hover, li.green, ol li::before { - font-family: Helvetica; + font-family: Helvetica; } diff --git a/tests/fixtures/url.css b/tests/fixtures/url.css index 93aae97f6..feb91bc33 100644 --- a/tests/fixtures/url.css +++ b/tests/fixtures/url.css @@ -1,4 +1,4 @@ body { background: #FFFFFF url("https://somesite.com/images/someimage.gif") repeat top center; } body { - background-url: url("https://somesite.com/images/someimage.gif"); -} \ No newline at end of file + background-url: url("https://somesite.com/images/someimage.gif"); +} diff --git a/tests/fixtures/values.css b/tests/fixtures/values.css index 35dbd729f..f00c0768e 100644 --- a/tests/fixtures/values.css +++ b/tests/fixtures/values.css @@ -1,15 +1,15 @@ #header { - margin: 10px 2em 1cm 2%; - font-family: Verdana, Helvetica, "Gill Sans", sans-serif; - font-size: 10px; - color: red !important; - background-color: green; - background-color: rgba(0,128,0,0.7); - frequency: 30Hz; + margin: 10px 2em 1cm 2%; + font-family: Verdana, Helvetica, "Gill Sans", sans-serif; + font-size: 10px; + color: red !important; + background-color: green; + background-color: rgba(0,128,0,0.7); + frequency: 30Hz; transform: rotate(1turn); } body { - color: green; - font: 75% "Lucida Grande", "Trebuchet MS", Verdana, sans-serif; + color: green; + font: 75% "Lucida Grande", "Trebuchet MS", Verdana, sans-serif; } diff --git a/tests/fixtures/whitespace.css b/tests/fixtures/whitespace.css index 6b21c24f6..de127ece0 100644 --- a/tests/fixtures/whitespace.css +++ b/tests/fixtures/whitespace.css @@ -1,3 +1,3 @@ .test { - background-image : url ( 4px ) ; + background-image : url ( 4px ) ; }