# CSS-Properties-p6 The CSS::Properties module is a set of related classes for parsing, manipulation and generation of CSS property sets, including inheritance, and defaults. Classess in this module -------- * `CSS::Properties` - property list manipulation class. * `CSS::Properties::Font` - property font manipulation * `CSS::Units` - units and postfix operators (e.g. `12pt`) * `CSS::Box` - CSS Box model implementation. * `CSS::PageBox` - CSS Box model for paged media See Also -------- * [CSS](https://github.com/p6-css/CSS-raku) - Top level CSS manipulation class ## Basic Construction ``` use v6; use CSS::Units :pt; use CSS::Properties; my $style = "color:red !important; padding: 1pt"; my CSS::Properties $css .= new( :$style ); say $css.important("color"); # True $css.border-color = 'red'; $css.margin = [5pt, 2pt, 5pt, 2pt]; $css.margin = 5pt; # set margin on all 4 sides # set text alignment $css.text-align = 'right'; say ~$css; # border-color:red; color:red!important; margin:5pt; padding:1pt; text-align:right; ``` ## Methods ### new method new( Str :$style, CSS::Properties() :$inherit, CSS::Properties() :$copy, Str :$units = 'pt', Numeric :$em = $inherit.em // 12, Numeric :$viewport-width, Numeric :$viewport-height, Numeric :$reference-width, *%props, ) returns CSS::Properties Options: - `Str :$style` CSS property list to parse - `CSS::Properties() :$inherit` Properties to be formally inherited - `CSS::Properties() :$copy` Additional properties to be copied in - `Str :$units` # measurement units, such as 'pt', 'px', 'in', etc - `Numeric :$em = 12` initial font size - `Numeric :$viewport-width` for use as `vw` length units - `Numeric :$viewport-height` for use as `vh` length units - `Numeric :$reference-width` for use in box values - `*%props` - CSS property settings ### measure # Converts a value to a numeric quantity; my Numeric $font-size = $css.measure: :font-size; # get current font size $font-size = $css.measure: :font-size; # compute a smaller font $font-size = $css.measure: :font-size(120%); # compute a larger font my $weight = $css.measure: :font-weight; # get current font weight 100..900 $weight = $css.measure: :font-weight; # compute bold font weight This function is implemented for `font-size`, `font-weight`, `letter-spacing`, `line-height`, and `word-spacing`. It also works for box related properties: `width`, `height`, `{min|max}-{width|height}`, `border-{top|right|bottom|left}-width`, and `{padding|margin}-{top|right|bottom|left}`. The `reference-width` attribute represents the width of a containing element; which needs to set for correct calculation of percentage box related quantities: $css.reference-width = 80pt; say $css.measure: :width(75%); # 60 ## CSS Property Accessors - color values are converted to Color objects - other values are converted to strings or numeric, as appropriate - the .type method returns additional type information - box properties are arrays that contain four sides. For example, 'margin' contains 'margin-top', 'margin-right', 'margin-bottom' and 'margin-left'; - there are also some container properties that may be accessed directly or via a hash; for example, The 'font' accessor returns a hash containing 'font-size', 'font-family', and other font properties. ``` use CSS::Properties; my CSS::Properties $css .= new: :style("color: orange; text-align: CENTER; margin: 2pt; font: 12pt Helvetica"); say $css.color.hex; # (FF A5 00) say $css.color.type; # 'rgb' say $css.text-align; # 'center' say $css.text-align.type; # 'keyw' (keyword) # access margin-top, directly and through margin container say $css.margin-top; # '2' say $css.margin-top.type; # 'pt' say $css.margin; # [2 2 2 2] say $css.margin[0]; # '2' say $css.margin[0].type; # 'pt' # access font-family directly and through font container say $css.font-family; # 'Helvetica' say $css.font-family.type; # 'ident' say $css.font; # 'Helvetica; ``` - The simplest ways of setting a property is to assign a string value which is parsed as CSS. - Unit values are also recognized. E.g. `16pt`. - Colors can be assigned to color objects - Also the type and value can be assigned as a pair. ``` use CSS::Properties; use CSS::Units :pt; use Color; my CSS::Properties $css .= new; # assign to container $css.font = "14pt Helvetica"; # assign to component properties $css.font-weight = 'bold'; # string $css.line-height = 16pt; # unit value $css.border-color = Color.new(0, 255, 0); $css.font-style = :keyw; # type/value pair say ~$css; # font:italic bold 14pt/16pt Helvetica; ``` ## CSS Modules ## Conformance Levels Processing defaults to CSS level 3 (class CSS::Module::CSS3). This can be configured via the :module option: ``` use CSS::Properties; use CSS::Module; use CSS::Module::CSS1; use CSS::Module::CSS21; use CSS::Module::CSS3; my $style = 'color: red; azimuth: left'; my CSS::Module $module = CSS::Module::CSS1.module; my CSS::Properties $css1 .= new: :$style, :$module; ## warnings: dropping unknown property: azimuth $module = CSS::Module::CSS21.module; my CSS::Properties $css21 .= new: :$style, :$module; ## (no warnings) my CSS::Properties $css3 .= new: :$style; # CSS3 is the default # -- or -- $module = CSS::Module::CSS3.module; $css3 .= new: :$style, :$module; ``` ### '@font-face' Properties `@font-face` is a sub-module of `CSS3`. To process a set of `@font-face` declarations, such as: ``` @font-face { font-family: myFirstFont; src: url(sansation_light.woff); } ``` ``` use CSS::Properties; use CSS::Module::CSS3; my $style = "font-family: myFirstFont; src: url(sansation_light.woff)"; my $module = CSS::Module::CSS3.module.sub-module<@font-face>; my CSS::Properties $font-face-css .= new( :$style, :$module); ``` ## Default values Most properties have a default value. If a property is reset to its default value it will be omitted from stringification: my $css = (require CSS::Properties).new; say $css.background-image; # none $css.background-image = 'url(camellia.png)'; say ~$css; # "background-image: url(camellia.png);" $css.background-image = $css.info("background-image").default; say ~$css; # "" ## Deleting properties Properties can be deleted via the `delete` method, or by assigning the property to `Nil`: my CSS::Properties $css .= new: :style("background-position:top left; border-top-color:red; border-bottom-color: green; color: blue"); # delete background position $css.background-position = Nil; # delete all border colors $css.delete: "border-color"; ## Inheritance A child class can inherit from one or more parent classes. This follows CSS standards: - not all properties are inherited by default; for example `color` is, but `margin` is not. - the `inherit` keyword can be used in the child property to ensure inheritance. - `initial` will reset the child property to the default value To inherit a css object or style string: - pass it as a `:inherit` option, when constructing the object, or - use the `inherit` method ``` use CSS::Properties; my $parent-style = "margin-top:5pt; margin-left: 15pt; color:rgb(0,0,255) !important"; my $style = "margin-top:25pt; margin-right: initial; margin-left: inherit; color:purple"; my CSS::Properties $css .= new: :$style, :inherit($parent-style); say $css.color; # #7F007Frgb (purple) say $css.handling("margin-left"); # inherit say $css.margin-left; # 15pt ``` ## Optimization and Serialization The `.write` or `.Str` methods can be used to produce CSS. Properties are optimized and normalized: - properties with default values are omitted - multiple component properties are generally consolidated to container properties (e.g. `font-family: Courier` and `font-size: 12pt` are consolidated to `font: 12pt Courier`). - rgb masks are translated to color-names, where possible ``` use CSS::Properties; my CSS::Properties $css .= new( :style("background-repeat:repeat; border-style: groove; border-width: 2pt 2pt; color: rgb(255,0,0);") ); # - 'border-width' and 'border-style' are consolidated to the 'border' container property # - rgb(255,0,0) is mapped to 'red' say $css.write; # "border:2pt groove; color: red;" ``` Notice that: - `background-repeat` was omitted because it has the default value - `border-style` and `border-width` have been consolidated to the `border` container property. This is possible because all four borders have common values - `color` has been translated from a color mask to a color `$.write` Options include: - `:!optimize` - turn off optimization. Don't, combine component properties into container properties (`border-style`, `border-width`, ... => `border`), or combine edges (`margin-top`, `margin-left`, ... => `margin`). - `:!terse` - enable multi-line output - `:!color-names` - don't translate RGB values to color-names ASTs can also be directly optimized: ``` use CSS::Properties; use CSS::Module::CSS3; use CSS::Writer; my CSS::Properties $css .= new; my $module = CSS::Module::CSS3.module; my $actions = $module.actions.new; my CSS::Writer $writer .= new: :color-names, :terse; my $declarations = "border-bottom-color:red; border-bottom-style:solid; border-bottom-width:1px; border-left-color:red; border-left-style:solid; border-left-width:1px; border-right-color:red; border-right-style:solid; border-right-width:1px; border-top-color:red; border-top-style:solid; border-top-width:1px;"; my $p = $module.grammar.parse($declarations, :$actions, :rule); my %ast = $css.optimize($p.ast); say $writer.write(|%ast); # border:1px solid red; ``` ## Property Meta-data The `info` method gives property specific meta-data, on all (component or container properties). It returns an object of type CSS::Properties::Property: ``` use CSS::Properties; my CSS::Properties $css .= new; my $margin-info = $css.info("margin"); say $margin-info.synopsis; # {1,4} say $margin-info.edges; # [margin-top margin-right margin-bottom margin-left] say $margin-info.inherit; # True (property is inherited) ``` ## Data Introspection The `properties` method, gives a list of current properties. Only component properties are returned. E.g. `font-family` may be returned; but `font` never is. ``` use CSS::Properties; my $style = "margin-top: 10%; margin-right: 5mm; margin-bottom: auto"; my CSS::Properties $css .= new: :$style; for $css.properties -> $prop { my $val = $css."$prop"(); say "$prop: $val {$val.type}"; } ``` Gives: ``` margin-top: 10 percent margin-bottom: auto keyw margin-right: 5 mm ``` ## Length Units CSS::Units is a convenience module that provides some simple post-fix length unit definitions. The `:ops` export overloads `+` and `-` to perform unit calculations. `+css` and `-css` are also available as more explicit infix operators: All infix operators convert to the left-hand operand's units. ``` use CSS::Units :ops, :pt, :px, :in, :mm; my $css = (require CSS::Properties).new: :margin[5pt, 10px, .1in, 2mm]; # display margins in millimeters say "%.2f mm".sprintf(.scale("mm")) for $css.margin.list; ``` ## Box Model ### Overview Excerpt from [CSS 2.2 Specification Chapter 8 - Box Model](https://www.w3.org/TR/CSS22/box.html#box-dimensions): ![Box Model](doc/boxdim.png) The margin, border, and padding can be broken down into top, right, bottom, and left segments (e.g., in the diagram, "LM" for left margin, "RP" for right padding, "TB" for top border, etc.). The perimeter of each of the four areas (content, padding, border, and margin) is called an "edge", so each box has four edges: - *Content Edge* or *Inner Edge* - The content edge surrounds the rectangle given by the width and height of the box, which often depend on the element's rendered content. The four content edges define the box's content box. - *Padding Edge* - The padding edge surrounds the box padding. If the padding has 0 width, the padding edge is the same as the content edge. The four padding edges define the box's padding box. - *Border Edge* - The border edge surrounds the box's border. If the border has 0 width, the border edge is the same as the padding edge. The four border edges define the box's border box. - *Margin Edge or Outer Edge* - The margin edge surrounds the box margin. If the margin has 0 width, the margin edge is the same as the border edge. The four margin edges define the box's margin box. ### `CSS::Box` `CSS::Box` is an abstract class for modelling Box Model elements. use CSS::Box; use CSS::Units :px, :pt, :em, :percent; use CSS::Properties; my $style = q:to"END"; width: 300px; border: 25px solid green; padding: 25px; margin: 25px; font: italic bold 10pt/12pt times-roman; END my CSS::Properties $css .= new: :$style; my $top = 80pt; my $right = 50pt; my $bottom = 10pt; my $left = 10pt; my CSS::Box $box .= new( :$top, :$left, :$bottom, :$right, :$css ); say $box.padding; # dimensions of padding box; say $box.margin; # dimensions of margin box; say $box.border-right; # vertical position of right border say $box.border-width; # border-right - border-left say $box.width("border"); # border-width say $box.height("content"); # height of content box say $box.font.family; # 'times-roman' # calculate some relative font lengths say $box.font-length(1.5em); # 15 say $box.font-length(200%); # 20 say $box.font-length('larger'); # 12 ### Box Methods #### new my CSS::Box $box .= new( :$top, :$left, :$bottom, :$right, :$css, :$parent ); The box `new` constructor accepts: - any two of `:top`, `:bottom` or `:height`, - and any two of `:left`, `:right` or `:width`. A `:$parent` CSS::Box object is required for correct calculations when any CSS `padding` or `margin`values are expressed as percentage values. #### font say "font-size is {$box.font.em}"; The '.font' accessor returns an object of type `CSS::Properties::Font`, with accessor methods: `em`, `ex`, `weight`, `family`, `style`, `leading`, `find-font`, `fontconfig-pattern` and `measure` methods. #### measure This method converts various length units to normalized base units (default 'pt'). use CSS::Units :mm, :in, :pt, :px; use CSS::Box; use CSS::Properties; my CSS::Box $box .= new; # default base units is points say [(1mm, 1in, 1pt, 1px).map: {$box.measure($_)}]; # produces: [2.8346pt 72pt 1pt 0.75pt] # change base units to inches $box .= new: :units; say [(1in, 72pt).map: {$box.measure($_)}]; # produces: [1in, 1in] #### top, right, bottom, left These methods return measured positions of each of the four corners of the inner content box. They are rw accessors, e.g.: $box.top += 5; Outer boxes will grow and shrink, retaining their original width and height. #### padding, margin, border This returns all four corners (measured) of the given box, e.g.: my Numeric ($top, $right, $bottom, $left) = $box.padding #### content This returns all four corners (measured) of the content box, e.g.: my Numeric ($top, $right, $bottom, $left) = $box.content; These values are rw. The box can be both moved and resized, by adjusting this array. $box.content = (10, 50, 35, 10); # 40x25 box, top-left @ 10,10 Outer boxes, will grow or shrink to retain their original widths. #### [padding|margin|border|content]-[width|height] say "margin box is size {$box.margin-width} X {$box.margin-height}"; This family of accessors return the measured width, or height of the given box. #### [padding|margin|border|content]-[top|right|bottom|left] say "margin left, top is ({$box.margin-left}, {$box.margin-top})"; This family of accessors return the measured x or y position of the given edge #### translate, move These methods can be used to adjust the position of the content box. $box.translate(10, 20); # translate box 10, 20 in X, Y directions $box.move(40, 50); # move top left corner to (X, Y) = (40, 50) ## Appendix : CSS3 Properties Name | Default | Inherit | Type | Synopsis --- | --- | --- | --- | --- azimuth | center | Yes | | \ \| [[ left-side \| far-left \| left \| center-left \| center \| center-right \| right \| far-right \| right-side ] \|\| behind ] \| leftwards \| rightwards background | | | hash | ['background-color' \|\| 'background-image' \|\| 'background-repeat' \|\| 'background-attachment' \|\| 'background-position'] background-attachment | scroll | | | scroll \| fixed background-color | transparent | | | \ \| transparent background-image | none | | | \ \| none background-position | 0% 0% | | | [ [ \ \| \ \| left \| center \| right ] [ \ \| \ \| top \| center \| bottom ]? ] \| [ [ left \| center \| right ] \|\| [ top \| center \| bottom ] ] background-repeat | repeat | | | repeat \| repeat-x \| repeat-y \| no-repeat border | | | hash,box | [ 'border-width' \|\| 'border-style' \|\| 'border-color' ] border-bottom | | | hash | [ 'border-bottom-width' \|\| 'border-bottom-style' \|\| 'border-bottom-color' ] border-bottom-color | the value of the 'color' property | | | \ \| transparent border-bottom-style | none | | | \ border-bottom-width | medium | | | \ border-collapse | separate | Yes | | collapse \| separate border-color | | | box | [ \ \| transparent ]{1,4} border-left | | | hash | [ 'border-left-width' \|\| 'border-left-style' \|\| 'border-left-color' ] border-left-color | the value of the 'color' property | | | \ \| transparent border-left-style | none | | | \ border-left-width | medium | | | \ border-right | | | hash | [ 'border-right-width' \|\| 'border-right-style' \|\| 'border-right-color' ] border-right-color | the value of the 'color' property | | | \ \| transparent border-right-style | none | | | \ border-right-width | medium | | | \ border-spacing | 0 | Yes | | \ \? border-style | | | box | \{1,4} border-top | | | hash | [ 'border-top-width' \|\| 'border-top-style' \|\| 'border-top-color' ] border-top-color | the value of the 'color' property | | | \ \| transparent border-top-style | none | | | \ border-top-width | medium | | | \ border-width | | | box | \{1,4} bottom | auto | | | \ \| \ \| auto caption-side | top | Yes | | top \| bottom clear | none | | | none \| left \| right \| both clip | auto | | | \ \| auto color | depends on user agent | Yes | | \ content | normal | | | normal \| none \| [ \ \| \ \| \ \| \ \| attr(\) \| open-quote \| close-quote \| no-open-quote \| no-close-quote ]+ counter-increment | none | | | none \| [ \ \? ]+ counter-reset | none | | | none \| [ \ \? ]+ cue | | | hash | [ 'cue-before' \|\| 'cue-after' ] cue-after | none | | | \ \| none cue-before | none | | | \ \| none cursor | auto | Yes | | [ [\ ,]* [ auto \| crosshair \| default \| pointer \| move \| e-resize \| ne-resize \| nw-resize \| n-resize \| se-resize \| sw-resize \| s-resize \| w-resize \| text \| wait \| help \| progress ] ] direction | ltr | Yes | | ltr \| rtl display | inline | | | inline \| block \| list-item \| inline-block \| table \| inline-table \| table-row-group \| table-header-group \| table-footer-group \| table-row \| table-column-group \| table-column \| table-cell \| table-caption \| none elevation | level | Yes | | \ \| below \| level \| above \| higher \| lower empty-cells | show | Yes | | show \| hide float | none | | | left \| right \| none font | | Yes | hash | [ [ \<‘font-style’\> \|\| \ \|\| \<‘font-weight’\> \|\| \<‘font-stretch’\> ]? \<‘font-size’\> [ / \<‘line-height’\> ]? \<‘font-family’\> ] \| caption \| icon \| menu \| message-box \| small-caption \| status-bar font-family | depends on user agent | Yes | | [ \ \| \ ]\# font-feature-settings | normal | Yes | | normal \| \\# font-kerning | auto | Yes | | auto \| normal \| none font-language-override | normal | Yes | | normal \| \ font-size | medium | Yes | | \ \| \ \| \ \| \ font-size-adjust | none | Yes | | none \| auto \| \ font-stretch | normal | Yes | | normal \| ultra-condensed \| extra-condensed \| condensed \| semi-condensed \| semi-expanded \| expanded \| extra-expanded \| ultra-expanded font-style | normal | Yes | | normal \| italic \| oblique font-synthesis | weight style | Yes | | none \| [ weight \|\| style ] font-variant | normal | Yes | | normal \| none \| [ \ \|\| \ \|\| \ \|\| \ \|\| stylistic(\) \|\| historical-forms \|\| styleset(\ \#) \|\| character-variant(\ \#) \|\| swash(\) \|\| ornaments(\) \|\| annotation(\) \|\| [ small-caps \| all-small-caps \| petite-caps \| all-petite-caps \| unicase \| titling-caps ] \|\| \ \|\| \ \|\| \ \|\| ordinal \|\| slashed-zero \|\| \ \|\| \ \|\| ruby ] font-variant-alternates | normal | Yes | | normal \| [ stylistic(\) \|\| historical-forms \|\| styleset(\\#) \|\| character-variant(\\#) \|\| swash(\) \|\| ornaments(\) \|\| annotation(\) ] font-variant-caps | normal | Yes | | normal \| small-caps \| all-small-caps \| petite-caps \| all-petite-caps \| unicase \| titling-caps font-variant-east-asian | normal | Yes | | normal \| [ \ \|\| \ \|\| ruby ] font-variant-ligatures | normal | Yes | | normal \| none \| [ \ \|\| \ \|\| \ \|\| \ ] font-variant-numeric | normal | Yes | | normal \| [ \ \|\| \ \|\| \ \|\| ordinal \|\| slashed-zero ] font-variant-position | normal | Yes | | normal \| sub \| super font-weight | normal | Yes | | normal \| bold \| bolder \| lighter \| 100 \| 200 \| 300 \| 400 \| 500 \| 600 \| 700 \| 800 \| 900 height | auto | | | \ \| \ \| auto left | auto | | | \ \| \ \| auto letter-spacing | normal | Yes | | normal \| \ line-height | normal | Yes | | normal \| \ \| \ \| \ list-style | | Yes | hash | [ 'list-style-type' \|\| 'list-style-position' \|\| 'list-style-image' ] list-style-image | none | Yes | | \ \| none list-style-position | outside | Yes | | inside \| outside list-style-type | disc | Yes | | disc \| circle \| square \| decimal \| decimal-leading-zero \| lower-roman \| upper-roman \| lower-greek \| lower-latin \| upper-latin \| armenian \| georgian \| lower-alpha \| upper-alpha \| none margin | | | box | \{1,4} margin-bottom | 0 | | | \ margin-left | 0 | | | \ margin-right | 0 | | | \ margin-top | 0 | | | \ max-height | none | | | \ \| \ \| none max-width | none | | | \ \| \ \| none min-height | 0 | | | \ \| \ min-width | 0 | | | \ \| \ opacity | 1.0 | | | \ orphans | 2 | Yes | | \ outline | | | hash | [ 'outline-color' \|\| 'outline-style' \|\| 'outline-width' ] outline-color | invert | | | \ \| invert outline-style | none | | | [ none \| hidden \| dotted \| dashed \| solid \| double \| groove \| ridge \| inset \| outset ] outline-width | medium | | | thin \| medium \| thick \| \ overflow | visible | | | visible \| hidden \| scroll \| auto padding | | | box | \{1,4} padding-bottom | 0 | | | \ padding-left | 0 | | | \ padding-right | 0 | | | \ padding-top | 0 | | | \ page-break-after | auto | | | auto \| always \| avoid \| left \| right page-break-before | auto | | | auto \| always \| avoid \| left \| right page-break-inside | auto | | | avoid \| auto pause | | | | [ [\ \| \]{1,2} ] pause-after | 0 | | | \ \| \ pause-before | 0 | | | \ \| \ pitch | medium | Yes | | \ \| x-low \| low \| medium \| high \| x-high pitch-range | 50 | Yes | | \ play-during | auto | | | \ [ mix \|\| repeat ]? \| auto \| none position | static | | | static \| relative \| absolute \| fixed quotes | depends on user agent | Yes | | [\ \]+ \| none richness | 50 | Yes | | \ right | auto | | | \ \| \ \| auto size | auto | | | \{1,2} \| auto \| [ \ \|\| [ portrait \| landscape] ] speak | normal | Yes | | normal \| none \| spell-out speak-header | once | Yes | | once \| always speak-numeral | continuous | Yes | | digits \| continuous speak-punctuation | none | Yes | | code \| none speech-rate | medium | Yes | | \ \| x-slow \| slow \| medium \| fast \| x-fast \| faster \| slower stress | 50 | Yes | | \ table-layout | auto | | | auto \| fixed text-align | a nameless value that acts as 'left' if 'direction' is 'ltr', 'right' if 'direction' is 'rtl' | Yes | | left \| right \| center \| justify text-decoration | none | | | none \| [ underline \|\| overline \|\| line-through \|\| blink ] text-indent | 0 | Yes | | \ \| \ text-transform | none | Yes | | capitalize \| uppercase \| lowercase \| none top | auto | | | \ \| \ \| auto unicode-bidi | normal | | | normal \| embed \| bidi-override vertical-align | baseline | | | baseline \| sub \| super \| top \| text-top \| middle \| bottom \| text-bottom \| \ \| \ visibility | visible | Yes | | visible \| hidden \| collapse voice-family | depends on user agent | Yes | | [\ \| \ ]\# volume | medium | Yes | | \ \| \ \| silent \| x-soft \| soft \| medium \| loud \| x-loud white-space | normal | Yes | | normal \| pre \| nowrap \| pre-wrap \| pre-line widows | 2 | Yes | | \ width | auto | | | \ \| \ \| auto word-spacing | normal | Yes | | normal \| \ z-index | auto | | | auto \| \ The above markdown table was produced with the following code snippet ``` use v6; say .join(' | '); say ('---' xx 5).join(' | '); my $css = (require CSS::Properties).new; for $css.properties(:all).sort -> $name { with $css.info($name) { my @type; @type.push: 'hash' if .children; @type.push: 'box' if .box; my $synopsis-escaped = .synopsis.subst(/ # ]>>/, '\\', :g); say ($name, .default // '', .inherit ?? 'Yes' !! '', @type.join(','), $synopsis-escaped, ).join(' | '); } } ```