test_css.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. import pytest
  2. from pandas.errors import CSSWarning
  3. import pandas._testing as tm
  4. from pandas.io.formats.css import CSSResolver
  5. def assert_resolves(css, props, inherited=None):
  6. resolve = CSSResolver()
  7. actual = resolve(css, inherited=inherited)
  8. assert props == actual
  9. def assert_same_resolution(css1, css2, inherited=None):
  10. resolve = CSSResolver()
  11. resolved1 = resolve(css1, inherited=inherited)
  12. resolved2 = resolve(css2, inherited=inherited)
  13. assert resolved1 == resolved2
  14. @pytest.mark.parametrize(
  15. "name,norm,abnorm",
  16. [
  17. (
  18. "whitespace",
  19. "hello: world; foo: bar",
  20. " \t hello \t :\n world \n ; \n foo: \tbar\n\n",
  21. ),
  22. ("case", "hello: world; foo: bar", "Hello: WORLD; foO: bar"),
  23. ("empty-decl", "hello: world; foo: bar", "; hello: world;; foo: bar;\n; ;"),
  24. ("empty-list", "", ";"),
  25. ],
  26. )
  27. def test_css_parse_normalisation(name, norm, abnorm):
  28. assert_same_resolution(norm, abnorm)
  29. @pytest.mark.parametrize(
  30. "invalid_css,remainder",
  31. [
  32. # No colon
  33. ("hello-world", ""),
  34. ("border-style: solid; hello-world", "border-style: solid"),
  35. (
  36. "border-style: solid; hello-world; font-weight: bold",
  37. "border-style: solid; font-weight: bold",
  38. ),
  39. # Unclosed string fail
  40. # Invalid size
  41. ("font-size: blah", "font-size: 1em"),
  42. ("font-size: 1a2b", "font-size: 1em"),
  43. ("font-size: 1e5pt", "font-size: 1em"),
  44. ("font-size: 1+6pt", "font-size: 1em"),
  45. ("font-size: 1unknownunit", "font-size: 1em"),
  46. ("font-size: 10", "font-size: 1em"),
  47. ("font-size: 10 pt", "font-size: 1em"),
  48. # Too many args
  49. ("border-top: 1pt solid red green", "border-top: 1pt solid green"),
  50. ],
  51. )
  52. def test_css_parse_invalid(invalid_css, remainder):
  53. with tm.assert_produces_warning(CSSWarning):
  54. assert_same_resolution(invalid_css, remainder)
  55. @pytest.mark.parametrize(
  56. "shorthand,expansions",
  57. [
  58. ("margin", ["margin-top", "margin-right", "margin-bottom", "margin-left"]),
  59. ("padding", ["padding-top", "padding-right", "padding-bottom", "padding-left"]),
  60. (
  61. "border-width",
  62. [
  63. "border-top-width",
  64. "border-right-width",
  65. "border-bottom-width",
  66. "border-left-width",
  67. ],
  68. ),
  69. (
  70. "border-color",
  71. [
  72. "border-top-color",
  73. "border-right-color",
  74. "border-bottom-color",
  75. "border-left-color",
  76. ],
  77. ),
  78. (
  79. "border-style",
  80. [
  81. "border-top-style",
  82. "border-right-style",
  83. "border-bottom-style",
  84. "border-left-style",
  85. ],
  86. ),
  87. ],
  88. )
  89. def test_css_side_shorthands(shorthand, expansions):
  90. top, right, bottom, left = expansions
  91. assert_resolves(
  92. f"{shorthand}: 1pt", {top: "1pt", right: "1pt", bottom: "1pt", left: "1pt"}
  93. )
  94. assert_resolves(
  95. f"{shorthand}: 1pt 4pt", {top: "1pt", right: "4pt", bottom: "1pt", left: "4pt"}
  96. )
  97. assert_resolves(
  98. f"{shorthand}: 1pt 4pt 2pt",
  99. {top: "1pt", right: "4pt", bottom: "2pt", left: "4pt"},
  100. )
  101. assert_resolves(
  102. f"{shorthand}: 1pt 4pt 2pt 0pt",
  103. {top: "1pt", right: "4pt", bottom: "2pt", left: "0pt"},
  104. )
  105. with tm.assert_produces_warning(CSSWarning):
  106. assert_resolves(f"{shorthand}: 1pt 1pt 1pt 1pt 1pt", {})
  107. @pytest.mark.parametrize(
  108. "shorthand,sides",
  109. [
  110. ("border-top", ["top"]),
  111. ("border-right", ["right"]),
  112. ("border-bottom", ["bottom"]),
  113. ("border-left", ["left"]),
  114. ("border", ["top", "right", "bottom", "left"]),
  115. ],
  116. )
  117. def test_css_border_shorthand_sides(shorthand, sides):
  118. def create_border_dict(sides, color=None, style=None, width=None):
  119. resolved = {}
  120. for side in sides:
  121. if color:
  122. resolved[f"border-{side}-color"] = color
  123. if style:
  124. resolved[f"border-{side}-style"] = style
  125. if width:
  126. resolved[f"border-{side}-width"] = width
  127. return resolved
  128. assert_resolves(
  129. f"{shorthand}: 1pt red solid", create_border_dict(sides, "red", "solid", "1pt")
  130. )
  131. @pytest.mark.parametrize(
  132. "prop, expected",
  133. [
  134. ("1pt red solid", ("red", "solid", "1pt")),
  135. ("red 1pt solid", ("red", "solid", "1pt")),
  136. ("red solid 1pt", ("red", "solid", "1pt")),
  137. ("solid 1pt red", ("red", "solid", "1pt")),
  138. ("red solid", ("red", "solid", "1.500000pt")),
  139. # Note: color=black is not CSS conforming
  140. # (See https://drafts.csswg.org/css-backgrounds/#border-shorthands)
  141. ("1pt solid", ("black", "solid", "1pt")),
  142. ("1pt red", ("red", "none", "1pt")),
  143. ("red", ("red", "none", "1.500000pt")),
  144. ("1pt", ("black", "none", "1pt")),
  145. ("solid", ("black", "solid", "1.500000pt")),
  146. # Sizes
  147. ("1em", ("black", "none", "12pt")),
  148. ],
  149. )
  150. def test_css_border_shorthands(prop, expected):
  151. color, style, width = expected
  152. assert_resolves(
  153. f"border-left: {prop}",
  154. {
  155. "border-left-color": color,
  156. "border-left-style": style,
  157. "border-left-width": width,
  158. },
  159. )
  160. @pytest.mark.parametrize(
  161. "style,inherited,equiv",
  162. [
  163. ("margin: 1px; margin: 2px", "", "margin: 2px"),
  164. ("margin: 1px", "margin: 2px", "margin: 1px"),
  165. ("margin: 1px; margin: inherit", "margin: 2px", "margin: 2px"),
  166. (
  167. "margin: 1px; margin-top: 2px",
  168. "",
  169. "margin-left: 1px; margin-right: 1px; "
  170. + "margin-bottom: 1px; margin-top: 2px",
  171. ),
  172. ("margin-top: 2px", "margin: 1px", "margin: 1px; margin-top: 2px"),
  173. ("margin: 1px", "margin-top: 2px", "margin: 1px"),
  174. (
  175. "margin: 1px; margin-top: inherit",
  176. "margin: 2px",
  177. "margin: 1px; margin-top: 2px",
  178. ),
  179. ],
  180. )
  181. def test_css_precedence(style, inherited, equiv):
  182. resolve = CSSResolver()
  183. inherited_props = resolve(inherited)
  184. style_props = resolve(style, inherited=inherited_props)
  185. equiv_props = resolve(equiv)
  186. assert style_props == equiv_props
  187. @pytest.mark.parametrize(
  188. "style,equiv",
  189. [
  190. (
  191. "margin: 1px; margin-top: inherit",
  192. "margin-bottom: 1px; margin-right: 1px; margin-left: 1px",
  193. ),
  194. ("margin-top: inherit", ""),
  195. ("margin-top: initial", ""),
  196. ],
  197. )
  198. def test_css_none_absent(style, equiv):
  199. assert_same_resolution(style, equiv)
  200. @pytest.mark.parametrize(
  201. "size,resolved",
  202. [
  203. ("xx-small", "6pt"),
  204. ("x-small", f"{7.5:f}pt"),
  205. ("small", f"{9.6:f}pt"),
  206. ("medium", "12pt"),
  207. ("large", f"{13.5:f}pt"),
  208. ("x-large", "18pt"),
  209. ("xx-large", "24pt"),
  210. ("8px", "6pt"),
  211. ("1.25pc", "15pt"),
  212. (".25in", "18pt"),
  213. ("02.54cm", "72pt"),
  214. ("25.4mm", "72pt"),
  215. ("101.6q", "72pt"),
  216. ("101.6q", "72pt"),
  217. ],
  218. )
  219. @pytest.mark.parametrize("relative_to", [None, "16pt"]) # invariant to inherited size
  220. def test_css_absolute_font_size(size, relative_to, resolved):
  221. if relative_to is None:
  222. inherited = None
  223. else:
  224. inherited = {"font-size": relative_to}
  225. assert_resolves(f"font-size: {size}", {"font-size": resolved}, inherited=inherited)
  226. @pytest.mark.parametrize(
  227. "size,relative_to,resolved",
  228. [
  229. ("1em", None, "12pt"),
  230. ("1.0em", None, "12pt"),
  231. ("1.25em", None, "15pt"),
  232. ("1em", "16pt", "16pt"),
  233. ("1.0em", "16pt", "16pt"),
  234. ("1.25em", "16pt", "20pt"),
  235. ("1rem", "16pt", "12pt"),
  236. ("1.0rem", "16pt", "12pt"),
  237. ("1.25rem", "16pt", "15pt"),
  238. ("100%", None, "12pt"),
  239. ("125%", None, "15pt"),
  240. ("100%", "16pt", "16pt"),
  241. ("125%", "16pt", "20pt"),
  242. ("2ex", None, "12pt"),
  243. ("2.0ex", None, "12pt"),
  244. ("2.50ex", None, "15pt"),
  245. ("inherit", "16pt", "16pt"),
  246. ("smaller", None, "10pt"),
  247. ("smaller", "18pt", "15pt"),
  248. ("larger", None, f"{14.4:f}pt"),
  249. ("larger", "15pt", "18pt"),
  250. ],
  251. )
  252. def test_css_relative_font_size(size, relative_to, resolved):
  253. if relative_to is None:
  254. inherited = None
  255. else:
  256. inherited = {"font-size": relative_to}
  257. assert_resolves(f"font-size: {size}", {"font-size": resolved}, inherited=inherited)