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