style/values/specified/
align.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! Values for CSS Box Alignment properties
6//!
7//! https://drafts.csswg.org/css-align/
8
9use crate::parser::{Parse, ParserContext};
10use cssparser::Parser;
11use std::fmt::{self, Write};
12use style_traits::{CssWriter, KeywordsCollectFn, ParseError, SpecifiedValueInfo, ToCss};
13
14/// Constants shared by multiple CSS Box Alignment properties
15#[derive(
16    Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem,
17)]
18#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
19#[repr(C)]
20pub struct AlignFlags(u8);
21bitflags! {
22    impl AlignFlags: u8 {
23        // Enumeration stored in the lower 5 bits:
24        /// {align,justify}-{content,items,self}: 'auto'
25        const AUTO = 0;
26        /// 'normal'
27        const NORMAL = 1;
28        /// 'start'
29        const START = 2;
30        /// 'end'
31        const END = 3;
32        /// 'flex-start'
33        const FLEX_START = 4;
34        /// 'flex-end'
35        const FLEX_END = 5;
36        /// 'center'
37        const CENTER = 6;
38        /// 'left'
39        const LEFT = 7;
40        /// 'right'
41        const RIGHT = 8;
42        /// 'baseline'
43        const BASELINE = 9;
44        /// 'last-baseline'
45        const LAST_BASELINE = 10;
46        /// 'stretch'
47        const STRETCH = 11;
48        /// 'self-start'
49        const SELF_START = 12;
50        /// 'self-end'
51        const SELF_END = 13;
52        /// 'space-between'
53        const SPACE_BETWEEN = 14;
54        /// 'space-around'
55        const SPACE_AROUND = 15;
56        /// 'space-evenly'
57        const SPACE_EVENLY = 16;
58        /// `anchor-center`
59        const ANCHOR_CENTER = 17;
60
61        // Additional flags stored in the upper bits:
62        /// 'legacy' (mutually exclusive w. SAFE & UNSAFE)
63        const LEGACY = 1 << 5;
64        /// 'safe'
65        const SAFE = 1 << 6;
66        /// 'unsafe' (mutually exclusive w. SAFE)
67        const UNSAFE = 1 << 7;
68
69        /// Mask for the additional flags above.
70        const FLAG_BITS = 0b11100000;
71    }
72}
73
74impl AlignFlags {
75    /// Returns the enumeration value stored in the lower 5 bits.
76    #[inline]
77    pub fn value(&self) -> Self {
78        *self & !AlignFlags::FLAG_BITS
79    }
80
81    /// Returns an updated value with the same flags.
82    #[inline]
83    pub fn with_value(&self, value: AlignFlags) -> Self {
84        debug_assert!(!value.intersects(Self::FLAG_BITS));
85        value | self.flags()
86    }
87
88    /// Returns the flags stored in the upper 3 bits.
89    #[inline]
90    pub fn flags(&self) -> Self {
91        *self & AlignFlags::FLAG_BITS
92    }
93}
94
95impl ToCss for AlignFlags {
96    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
97    where
98        W: Write,
99    {
100        let flags = self.flags();
101        let value = self.value();
102        match flags {
103            AlignFlags::LEGACY => {
104                dest.write_str("legacy")?;
105                if value.is_empty() {
106                    return Ok(());
107                }
108                dest.write_char(' ')?;
109            },
110            AlignFlags::SAFE => dest.write_str("safe ")?,
111            AlignFlags::UNSAFE => dest.write_str("unsafe ")?,
112            _ => {
113                debug_assert_eq!(flags, AlignFlags::empty());
114            },
115        }
116
117        dest.write_str(match value {
118            AlignFlags::AUTO => "auto",
119            AlignFlags::NORMAL => "normal",
120            AlignFlags::START => "start",
121            AlignFlags::END => "end",
122            AlignFlags::FLEX_START => "flex-start",
123            AlignFlags::FLEX_END => "flex-end",
124            AlignFlags::CENTER => "center",
125            AlignFlags::LEFT => "left",
126            AlignFlags::RIGHT => "right",
127            AlignFlags::BASELINE => "baseline",
128            AlignFlags::LAST_BASELINE => "last baseline",
129            AlignFlags::STRETCH => "stretch",
130            AlignFlags::SELF_START => "self-start",
131            AlignFlags::SELF_END => "self-end",
132            AlignFlags::SPACE_BETWEEN => "space-between",
133            AlignFlags::SPACE_AROUND => "space-around",
134            AlignFlags::SPACE_EVENLY => "space-evenly",
135            AlignFlags::ANCHOR_CENTER => "anchor-center",
136            _ => unreachable!(),
137        })
138    }
139}
140
141/// An axis direction, either inline (for the `justify` properties) or block,
142/// (for the `align` properties).
143#[derive(Clone, Copy, PartialEq)]
144pub enum AxisDirection {
145    /// Block direction.
146    Block,
147    /// Inline direction.
148    Inline,
149}
150
151/// Shared value for the `align-content` and `justify-content` properties.
152///
153/// <https://drafts.csswg.org/css-align/#content-distribution>
154/// <https://drafts.csswg.org/css-align/#propdef-align-content>
155#[derive(
156    Clone,
157    Copy,
158    Debug,
159    Eq,
160    MallocSizeOf,
161    PartialEq,
162    ToComputedValue,
163    ToCss,
164    ToResolvedValue,
165    ToShmem,
166    ToTyped,
167)]
168#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
169#[repr(C)]
170pub struct ContentDistribution {
171    primary: AlignFlags,
172    // FIXME(https://github.com/w3c/csswg-drafts/issues/1002): This will need to
173    // accept fallback alignment, eventually.
174}
175
176impl ContentDistribution {
177    /// The initial value 'normal'
178    #[inline]
179    pub fn normal() -> Self {
180        Self::new(AlignFlags::NORMAL)
181    }
182
183    /// `start`
184    #[inline]
185    pub fn start() -> Self {
186        Self::new(AlignFlags::START)
187    }
188
189    /// The initial value 'normal'
190    #[inline]
191    pub fn new(primary: AlignFlags) -> Self {
192        Self { primary }
193    }
194
195    /// Returns whether this value is a <baseline-position>.
196    pub fn is_baseline_position(&self) -> bool {
197        matches!(
198            self.primary.value(),
199            AlignFlags::BASELINE | AlignFlags::LAST_BASELINE
200        )
201    }
202
203    /// The primary alignment
204    #[inline]
205    pub fn primary(self) -> AlignFlags {
206        self.primary
207    }
208
209    /// Parse a value for align-content
210    pub fn parse_block<'i>(
211        _: &ParserContext,
212        input: &mut Parser<'i, '_>,
213    ) -> Result<Self, ParseError<'i>> {
214        Self::parse(input, AxisDirection::Block)
215    }
216
217    /// Parse a value for justify-content
218    pub fn parse_inline<'i>(
219        _: &ParserContext,
220        input: &mut Parser<'i, '_>,
221    ) -> Result<Self, ParseError<'i>> {
222        Self::parse(input, AxisDirection::Inline)
223    }
224
225    fn parse<'i, 't>(
226        input: &mut Parser<'i, 't>,
227        axis: AxisDirection,
228    ) -> Result<Self, ParseError<'i>> {
229        // NOTE Please also update the `list_keywords` function below
230        //      when this function is updated.
231
232        // Try to parse normal first
233        if input
234            .try_parse(|i| i.expect_ident_matching("normal"))
235            .is_ok()
236        {
237            return Ok(ContentDistribution::normal());
238        }
239
240        // Parse <baseline-position>, but only on the block axis.
241        if axis == AxisDirection::Block {
242            if let Ok(value) = input.try_parse(parse_baseline) {
243                return Ok(ContentDistribution::new(value));
244            }
245        }
246
247        // <content-distribution>
248        if let Ok(value) = input.try_parse(parse_content_distribution) {
249            return Ok(ContentDistribution::new(value));
250        }
251
252        // <overflow-position>? <content-position>
253        let overflow_position = input
254            .try_parse(parse_overflow_position)
255            .unwrap_or(AlignFlags::empty());
256
257        let content_position = try_match_ident_ignore_ascii_case! { input,
258            "start" => AlignFlags::START,
259            "end" => AlignFlags::END,
260            "flex-start" => AlignFlags::FLEX_START,
261            "flex-end" => AlignFlags::FLEX_END,
262            "center" => AlignFlags::CENTER,
263            "left" if axis == AxisDirection::Inline => AlignFlags::LEFT,
264            "right" if axis == AxisDirection::Inline => AlignFlags::RIGHT,
265        };
266
267        Ok(ContentDistribution::new(
268            content_position | overflow_position,
269        ))
270    }
271}
272
273impl SpecifiedValueInfo for ContentDistribution {
274    fn collect_completion_keywords(f: KeywordsCollectFn) {
275        f(&["normal"]);
276        list_baseline_keywords(f); // block-axis only
277        list_content_distribution_keywords(f);
278        list_overflow_position_keywords(f);
279        f(&["start", "end", "flex-start", "flex-end", "center"]);
280        f(&["left", "right"]); // inline-axis only
281    }
282}
283
284/// The specified value of the {align,justify}-self properties.
285///
286/// <https://drafts.csswg.org/css-align/#self-alignment>
287/// <https://drafts.csswg.org/css-align/#propdef-align-self>
288#[derive(
289    Clone,
290    Copy,
291    Debug,
292    Deref,
293    Eq,
294    MallocSizeOf,
295    PartialEq,
296    ToComputedValue,
297    ToCss,
298    ToResolvedValue,
299    ToShmem,
300    ToTyped,
301)]
302#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
303#[repr(C)]
304pub struct SelfAlignment(pub AlignFlags);
305
306impl SelfAlignment {
307    /// The initial value 'auto'
308    #[inline]
309    pub fn auto() -> Self {
310        SelfAlignment(AlignFlags::AUTO)
311    }
312
313    /// Returns whether this value is valid for both axis directions.
314    pub fn is_valid_on_both_axes(&self) -> bool {
315        match self.0.value() {
316            // left | right are only allowed on the inline axis.
317            AlignFlags::LEFT | AlignFlags::RIGHT => false,
318
319            _ => true,
320        }
321    }
322
323    /// Parse self-alignment on the block axis (for align-self)
324    pub fn parse_block<'i, 't>(
325        _: &ParserContext,
326        input: &mut Parser<'i, 't>,
327    ) -> Result<Self, ParseError<'i>> {
328        Self::parse(input, AxisDirection::Block)
329    }
330
331    /// Parse self-alignment on the block axis (for align-self)
332    pub fn parse_inline<'i, 't>(
333        _: &ParserContext,
334        input: &mut Parser<'i, 't>,
335    ) -> Result<Self, ParseError<'i>> {
336        Self::parse(input, AxisDirection::Inline)
337    }
338
339    /// Parse a self-alignment value on one of the axes.
340    fn parse<'i, 't>(
341        input: &mut Parser<'i, 't>,
342        axis: AxisDirection,
343    ) -> Result<Self, ParseError<'i>> {
344        // NOTE Please also update the `list_keywords` function below
345        //      when this function is updated.
346
347        // <baseline-position>
348        //
349        // It's weird that this accepts <baseline-position>, but not
350        // justify-content...
351        if let Ok(value) = input.try_parse(parse_baseline) {
352            return Ok(SelfAlignment(value));
353        }
354
355        // auto | normal | stretch
356        if let Ok(value) = input.try_parse(parse_auto_normal_stretch) {
357            return Ok(SelfAlignment(value));
358        }
359
360        // <overflow-position>? <self-position>
361        let overflow_position = input
362            .try_parse(parse_overflow_position)
363            .unwrap_or(AlignFlags::empty());
364        let self_position = parse_self_position(input, axis)?;
365        Ok(SelfAlignment(overflow_position | self_position))
366    }
367
368    fn list_keywords(f: KeywordsCollectFn, axis: AxisDirection) {
369        list_baseline_keywords(f);
370        list_auto_normal_stretch(f);
371        list_overflow_position_keywords(f);
372        list_self_position_keywords(f, axis);
373    }
374
375    /// Performs a flip of the position, that is, for self-start we return self-end, for left
376    /// we return right, etc.
377    pub fn flip_position(self) -> Self {
378        let flipped_value = match self.0.value() {
379            AlignFlags::START => AlignFlags::END,
380            AlignFlags::END => AlignFlags::START,
381            AlignFlags::FLEX_START => AlignFlags::FLEX_END,
382            AlignFlags::FLEX_END => AlignFlags::FLEX_START,
383            AlignFlags::LEFT => AlignFlags::RIGHT,
384            AlignFlags::RIGHT => AlignFlags::LEFT,
385            AlignFlags::SELF_START => AlignFlags::SELF_END,
386            AlignFlags::SELF_END => AlignFlags::SELF_START,
387
388            AlignFlags::AUTO |
389            AlignFlags::NORMAL |
390            AlignFlags::BASELINE |
391            AlignFlags::LAST_BASELINE |
392            AlignFlags::STRETCH |
393            AlignFlags::CENTER |
394            AlignFlags::SPACE_BETWEEN |
395            AlignFlags::SPACE_AROUND |
396            AlignFlags::SPACE_EVENLY |
397            AlignFlags::ANCHOR_CENTER => return self,
398            _ => {
399                debug_assert!(false, "Unexpected alignment enumeration value");
400                return self;
401            }
402        };
403        self.with_value(flipped_value)
404    }
405
406    /// Returns a fixed-up alignment value.
407    #[inline]
408    pub fn with_value(self, value: AlignFlags) -> Self {
409        Self(self.0.with_value(value))
410    }
411}
412
413impl SpecifiedValueInfo for SelfAlignment {
414    fn collect_completion_keywords(f: KeywordsCollectFn) {
415        // TODO: This technically lists left/right for align-self. Not amazing but also not sure
416        // worth fixing here, could be special-cased on the caller.
417        Self::list_keywords(f, AxisDirection::Block);
418    }
419}
420
421/// Value of the `align-items` and `justify-items` properties
422///
423/// <https://drafts.csswg.org/css-align/#propdef-align-items>
424/// <https://drafts.csswg.org/css-align/#propdef-justify-items>
425#[derive(
426    Clone,
427    Copy,
428    Debug,
429    Deref,
430    Eq,
431    MallocSizeOf,
432    PartialEq,
433    ToComputedValue,
434    ToCss,
435    ToResolvedValue,
436    ToShmem,
437    ToTyped,
438)]
439#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
440#[repr(C)]
441pub struct ItemPlacement(pub AlignFlags);
442
443impl ItemPlacement {
444    /// The value 'normal'
445    #[inline]
446    pub fn normal() -> Self {
447        Self(AlignFlags::NORMAL)
448    }
449}
450
451impl ItemPlacement {
452    /// Parse a value for align-items
453    pub fn parse_block<'i>(
454        _: &ParserContext,
455        input: &mut Parser<'i, '_>,
456    ) -> Result<Self, ParseError<'i>> {
457        Self::parse(input, AxisDirection::Block)
458    }
459
460    /// Parse a value for justify-items
461    pub fn parse_inline<'i>(
462        _: &ParserContext,
463        input: &mut Parser<'i, '_>,
464    ) -> Result<Self, ParseError<'i>> {
465        Self::parse(input, AxisDirection::Inline)
466    }
467
468    fn parse<'i, 't>(
469        input: &mut Parser<'i, 't>,
470        axis: AxisDirection,
471    ) -> Result<Self, ParseError<'i>> {
472        // NOTE Please also update `impl SpecifiedValueInfo` below when
473        //      this function is updated.
474
475        // <baseline-position>
476        if let Ok(baseline) = input.try_parse(parse_baseline) {
477            return Ok(Self(baseline));
478        }
479
480        // normal | stretch
481        if let Ok(value) = input.try_parse(parse_normal_stretch) {
482            return Ok(Self(value));
483        }
484
485        if axis == AxisDirection::Inline {
486            // legacy | [ legacy && [ left | right | center ] ]
487            if let Ok(value) = input.try_parse(parse_legacy) {
488                return Ok(Self(value));
489            }
490        }
491
492        // <overflow-position>? <self-position>
493        let overflow = input
494            .try_parse(parse_overflow_position)
495            .unwrap_or(AlignFlags::empty());
496        let self_position = parse_self_position(input, axis)?;
497        Ok(ItemPlacement(self_position | overflow))
498    }
499}
500
501impl SpecifiedValueInfo for ItemPlacement {
502    fn collect_completion_keywords(f: KeywordsCollectFn) {
503        list_baseline_keywords(f);
504        list_normal_stretch(f);
505        list_overflow_position_keywords(f);
506        list_self_position_keywords(f, AxisDirection::Block);
507    }
508}
509
510/// Value of the `justify-items` property
511///
512/// <https://drafts.csswg.org/css-align/#justify-items-property>
513#[derive(
514    Clone, Copy, Debug, Deref, Eq, MallocSizeOf, PartialEq, ToCss, ToResolvedValue, ToShmem, ToTyped,
515)]
516#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
517#[repr(C)]
518pub struct JustifyItems(pub ItemPlacement);
519
520impl JustifyItems {
521    /// The initial value 'legacy'
522    #[inline]
523    pub fn legacy() -> Self {
524        Self(ItemPlacement(AlignFlags::LEGACY))
525    }
526
527    /// The value 'normal'
528    #[inline]
529    pub fn normal() -> Self {
530        Self(ItemPlacement::normal())
531    }
532}
533
534impl Parse for JustifyItems {
535    fn parse<'i, 't>(
536        context: &ParserContext,
537        input: &mut Parser<'i, 't>,
538    ) -> Result<Self, ParseError<'i>> {
539        ItemPlacement::parse_inline(context, input).map(Self)
540    }
541}
542
543impl SpecifiedValueInfo for JustifyItems {
544    fn collect_completion_keywords(f: KeywordsCollectFn) {
545        ItemPlacement::collect_completion_keywords(f);
546        list_legacy_keywords(f); // Inline axis only
547    }
548}
549
550// auto | normal | stretch
551fn parse_auto_normal_stretch<'i, 't>(
552    input: &mut Parser<'i, 't>,
553) -> Result<AlignFlags, ParseError<'i>> {
554    // NOTE Please also update the `list_auto_normal_stretch` function
555    //      below when this function is updated.
556    try_match_ident_ignore_ascii_case! { input,
557        "auto" => Ok(AlignFlags::AUTO),
558        "normal" => Ok(AlignFlags::NORMAL),
559        "stretch" => Ok(AlignFlags::STRETCH),
560    }
561}
562
563fn list_auto_normal_stretch(f: KeywordsCollectFn) {
564    f(&["auto", "normal", "stretch"]);
565}
566
567// normal | stretch
568fn parse_normal_stretch<'i, 't>(input: &mut Parser<'i, 't>) -> Result<AlignFlags, ParseError<'i>> {
569    // NOTE Please also update the `list_normal_stretch` function below
570    //      when this function is updated.
571    try_match_ident_ignore_ascii_case! { input,
572        "normal" => Ok(AlignFlags::NORMAL),
573        "stretch" => Ok(AlignFlags::STRETCH),
574    }
575}
576
577fn list_normal_stretch(f: KeywordsCollectFn) {
578    f(&["normal", "stretch"]);
579}
580
581// <baseline-position>
582fn parse_baseline<'i, 't>(input: &mut Parser<'i, 't>) -> Result<AlignFlags, ParseError<'i>> {
583    // NOTE Please also update the `list_baseline_keywords` function
584    //      below when this function is updated.
585    try_match_ident_ignore_ascii_case! { input,
586        "baseline" => Ok(AlignFlags::BASELINE),
587        "first" => {
588            input.expect_ident_matching("baseline")?;
589            Ok(AlignFlags::BASELINE)
590        },
591        "last" => {
592            input.expect_ident_matching("baseline")?;
593            Ok(AlignFlags::LAST_BASELINE)
594        },
595    }
596}
597
598fn list_baseline_keywords(f: KeywordsCollectFn) {
599    f(&["baseline", "first baseline", "last baseline"]);
600}
601
602// <content-distribution>
603fn parse_content_distribution<'i, 't>(
604    input: &mut Parser<'i, 't>,
605) -> Result<AlignFlags, ParseError<'i>> {
606    // NOTE Please also update the `list_content_distribution_keywords`
607    //      function below when this function is updated.
608    try_match_ident_ignore_ascii_case! { input,
609        "stretch" => Ok(AlignFlags::STRETCH),
610        "space-between" => Ok(AlignFlags::SPACE_BETWEEN),
611        "space-around" => Ok(AlignFlags::SPACE_AROUND),
612        "space-evenly" => Ok(AlignFlags::SPACE_EVENLY),
613    }
614}
615
616fn list_content_distribution_keywords(f: KeywordsCollectFn) {
617    f(&["stretch", "space-between", "space-around", "space-evenly"]);
618}
619
620// <overflow-position>
621fn parse_overflow_position<'i, 't>(
622    input: &mut Parser<'i, 't>,
623) -> Result<AlignFlags, ParseError<'i>> {
624    // NOTE Please also update the `list_overflow_position_keywords`
625    //      function below when this function is updated.
626    try_match_ident_ignore_ascii_case! { input,
627        "safe" => Ok(AlignFlags::SAFE),
628        "unsafe" => Ok(AlignFlags::UNSAFE),
629    }
630}
631
632fn list_overflow_position_keywords(f: KeywordsCollectFn) {
633    f(&["safe", "unsafe"]);
634}
635
636// <self-position> | left | right in the inline axis.
637fn parse_self_position<'i, 't>(
638    input: &mut Parser<'i, 't>,
639    axis: AxisDirection,
640) -> Result<AlignFlags, ParseError<'i>> {
641    // NOTE Please also update the `list_self_position_keywords`
642    //      function below when this function is updated.
643    Ok(try_match_ident_ignore_ascii_case! { input,
644        "start" => AlignFlags::START,
645        "end" => AlignFlags::END,
646        "flex-start" => AlignFlags::FLEX_START,
647        "flex-end" => AlignFlags::FLEX_END,
648        "center" => AlignFlags::CENTER,
649        "self-start" => AlignFlags::SELF_START,
650        "self-end" => AlignFlags::SELF_END,
651        "left" if axis == AxisDirection::Inline => AlignFlags::LEFT,
652        "right" if axis == AxisDirection::Inline => AlignFlags::RIGHT,
653        "anchor-center" if static_prefs::pref!("layout.css.anchor-positioning.enabled") => AlignFlags::ANCHOR_CENTER,
654    })
655}
656
657fn list_self_position_keywords(f: KeywordsCollectFn, axis: AxisDirection) {
658    f(&[
659        "start",
660        "end",
661        "flex-start",
662        "flex-end",
663        "center",
664        "self-start",
665        "self-end",
666    ]);
667
668    if static_prefs::pref!("layout.css.anchor-positioning.enabled") {
669        f(&["anchor-center"]);
670    }
671
672    if axis == AxisDirection::Inline {
673        f(&["left", "right"]);
674    }
675}
676
677fn parse_left_right_center<'i, 't>(
678    input: &mut Parser<'i, 't>,
679) -> Result<AlignFlags, ParseError<'i>> {
680    // NOTE Please also update the `list_legacy_keywords` function below
681    //      when this function is updated.
682    Ok(try_match_ident_ignore_ascii_case! { input,
683        "left" => AlignFlags::LEFT,
684        "right" => AlignFlags::RIGHT,
685        "center" => AlignFlags::CENTER,
686    })
687}
688
689// legacy | [ legacy && [ left | right | center ] ]
690fn parse_legacy<'i, 't>(input: &mut Parser<'i, 't>) -> Result<AlignFlags, ParseError<'i>> {
691    // NOTE Please also update the `list_legacy_keywords` function below
692    //      when this function is updated.
693    let flags = try_match_ident_ignore_ascii_case! { input,
694        "legacy" => {
695            let flags = input.try_parse(parse_left_right_center)
696                .unwrap_or(AlignFlags::empty());
697
698            return Ok(AlignFlags::LEGACY | flags)
699        },
700        "left" => AlignFlags::LEFT,
701        "right" => AlignFlags::RIGHT,
702        "center" => AlignFlags::CENTER,
703    };
704
705    input.expect_ident_matching("legacy")?;
706    Ok(AlignFlags::LEGACY | flags)
707}
708
709fn list_legacy_keywords(f: KeywordsCollectFn) {
710    f(&["legacy", "left", "right", "center"]);
711}