Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/add-use-media-caption-html.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@biomejs/biome": patch
Comment thread
rahuld109 marked this conversation as resolved.
Outdated
---

Added the HTML-specific lint rule [`useMediaCaption`](https://biomejs.dev/linter/rules/use-media-caption/). Enforces that `audio` and `video` elements have a `track` element with `kind="captions"` for accessibility. Muted videos are allowed without captions.
Comment thread
rahuld109 marked this conversation as resolved.
Outdated
3 changes: 2 additions & 1 deletion crates/biome_html_analyze/src/lint/a11y.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub mod use_aria_props_for_role;
pub mod use_button_type;
pub mod use_html_lang;
pub mod use_iframe_title;
pub mod use_media_caption;
pub mod use_valid_aria_role;
pub mod use_valid_lang;
declare_lint_group! { pub A11y { name : "a11y" , rules : [self :: no_access_key :: NoAccessKey , self :: no_autofocus :: NoAutofocus , self :: no_distracting_elements :: NoDistractingElements , self :: no_header_scope :: NoHeaderScope , self :: no_positive_tabindex :: NoPositiveTabindex , self :: no_redundant_alt :: NoRedundantAlt , self :: no_svg_without_title :: NoSvgWithoutTitle , self :: use_alt_text :: UseAltText , self :: use_aria_props_for_role :: UseAriaPropsForRole , self :: use_button_type :: UseButtonType , self :: use_html_lang :: UseHtmlLang , self :: use_iframe_title :: UseIframeTitle , self :: use_valid_aria_role :: UseValidAriaRole , self :: use_valid_lang :: UseValidLang ,] } }
declare_lint_group! { pub A11y { name : "a11y" , rules : [self :: no_access_key :: NoAccessKey , self :: no_autofocus :: NoAutofocus , self :: no_distracting_elements :: NoDistractingElements , self :: no_header_scope :: NoHeaderScope , self :: no_positive_tabindex :: NoPositiveTabindex , self :: no_redundant_alt :: NoRedundantAlt , self :: no_svg_without_title :: NoSvgWithoutTitle , self :: use_alt_text :: UseAltText , self :: use_aria_props_for_role :: UseAriaPropsForRole , self :: use_button_type :: UseButtonType , self :: use_html_lang :: UseHtmlLang , self :: use_iframe_title :: UseIframeTitle , self :: use_media_caption :: UseMediaCaption , self :: use_valid_aria_role :: UseValidAriaRole , self :: use_valid_lang :: UseValidLang ,] } }
139 changes: 139 additions & 0 deletions crates/biome_html_analyze/src/lint/a11y/use_media_caption.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
use biome_analyze::{
Ast, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule,
};
use biome_console::markup;
use biome_diagnostics::Severity;
use biome_html_syntax::AnyHtmlElement;
use biome_rowan::AstNode;

declare_lint_rule! {
/// Enforces that `audio` and `video` elements must have a `track` for captions.
///
/// Captions support users with hearing-impairments. They should be a transcription
/// or translation of the dialogue, sound effects, musical cues, and other relevant
/// audio information.
///
/// ## Examples
///
/// ### Invalid
///
/// ```html,expect_diagnostic
/// <video src="video.mp4"></video>
/// ```
///
/// ```html,expect_diagnostic
/// <audio src="audio.mp3">
/// <source src="audio.ogg" type="audio/ogg" />
/// </audio>
/// ```
///
/// ### Valid
///
/// ```html
/// <video src="video.mp4">
/// <track kind="captions" src="captions.vtt" />
/// </video>
/// ```
///
/// ```html
/// <audio src="audio.mp3">
/// <track kind="captions" src="captions.vtt" />
/// </audio>
/// ```
///
/// ```html
/// <video muted src="video.mp4"></video>
/// ```
///
/// ## Accessibility guidelines
///
/// - [WCAG 1.2.2](https://www.w3.org/WAI/WCAG21/Understanding/captions-prerecorded)
/// - [WCAG 1.2.3](https://www.w3.org/WAI/WCAG21/Understanding/audio-description-or-media-alternative-prerecorded)
///
pub UseMediaCaption {
version: "next",
name: "useMediaCaption",
language: "html",
sources: &[RuleSource::EslintJsxA11y("media-has-caption").same()],
recommended: true,
severity: Severity::Error,
}
}

impl Rule for UseMediaCaption {
type Query = Ast<AnyHtmlElement>;
type State = ();
type Signals = Option<Self::State>;
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let node = ctx.query();

// Check if element is audio or video
let element_name = node.name()?;
if element_name != "audio" && element_name != "video" {
return None;
}
Comment thread
rahuld109 marked this conversation as resolved.
Outdated

// If element has muted attribute, it's valid (no caption needed)
if node.find_attribute_by_name("muted").is_some() {
return None;
}
Comment thread
rahuld109 marked this conversation as resolved.
Outdated

// Check for track element with kind="captions" in children
let html_element = node.as_html_element()?;
if html_element.opening_element().is_ok() && has_caption_track(&html_element.children()) {
return None;
}

// No muted attribute and no caption track found - emit diagnostic
Some(())
}

fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
let node = ctx.query();
let diagnostic = RuleDiagnostic::new(
rule_category!(),
node.syntax().text_trimmed_range(),
markup! {
"Provide a "<Emphasis>"track"</Emphasis>" for captions when using "<Emphasis>"audio"</Emphasis>" or "<Emphasis>"video"</Emphasis>" elements."
},
)
.note(markup! {
"Captions support users with hearing-impairments. They should be a transcription or translation of the dialogue, sound effects, musical cues, and other relevant audio information."
});

Some(diagnostic)
}
}

use biome_html_syntax::HtmlElementList;

/// Checks if the given `HtmlElementList` has a `track` element with `kind="captions"`.
fn has_caption_track(html_child_list: &HtmlElementList) -> bool {
html_child_list.into_iter().any(|child| {
Comment thread
rahuld109 marked this conversation as resolved.
Outdated
// Check if element is a track element (works for both HtmlElement and HtmlSelfClosingElement)
let Some(name) = child.name() else {
return false;
};

if name.text() != "track" {
Comment thread
rahuld109 marked this conversation as resolved.
Outdated
return false;
}

// Check if track has kind="captions"
let Some(kind_attr) = child.find_attribute_by_name("kind") else {
return false;
};
let Some(initializer) = kind_attr.initializer() else {
return false;
};
let Ok(value) = initializer.value() else {
return false;
};
let Some(string_value) = value.string_value() else {
return false;
};
string_value.eq_ignore_ascii_case("captions")
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- should generate diagnostics -->
<video src="video.mp4"></video>
<audio src="audio.mp3"></audio>
<video>
<source src="video.webm" type="video/webm" />
</video>
<audio>
<source src="audio.ogg" type="audio/ogg" />
</audio>
<!-- track without kind="captions" -->
<video>
<track kind="subtitles" src="subtitles.vtt" />
</video>
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
---
source: crates/biome_html_analyze/tests/spec_tests.rs
expression: invalid.html
---
# Input
```html
<!-- should generate diagnostics -->
<video src="video.mp4"></video>
<audio src="audio.mp3"></audio>
<video>
<source src="video.webm" type="video/webm" />
</video>
<audio>
<source src="audio.ogg" type="audio/ogg" />
</audio>
<!-- track without kind="captions" -->
<video>
<track kind="subtitles" src="subtitles.vtt" />
</video>

```

# Diagnostics
```
invalid.html:2:1 lint/a11y/useMediaCaption ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Provide a track for captions when using audio or video elements.

1 │ <!-- should generate diagnostics -->
> 2 │ <video src="video.mp4"></video>
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3 │ <audio src="audio.mp3"></audio>
4 │ <video>

i Captions support users with hearing-impairments. They should be a transcription or translation of the dialogue, sound effects, musical cues, and other relevant audio information.


```

```
invalid.html:3:1 lint/a11y/useMediaCaption ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Provide a track for captions when using audio or video elements.

1 │ <!-- should generate diagnostics -->
2 │ <video src="video.mp4"></video>
> 3 │ <audio src="audio.mp3"></audio>
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4 │ <video>
5 │ <source src="video.webm" type="video/webm" />

i Captions support users with hearing-impairments. They should be a transcription or translation of the dialogue, sound effects, musical cues, and other relevant audio information.


```

```
invalid.html:4:1 lint/a11y/useMediaCaption ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Provide a track for captions when using audio or video elements.

2 │ <video src="video.mp4"></video>
3 │ <audio src="audio.mp3"></audio>
> 4 │ <video>
│ ^^^^^^^
> 5 │ <source src="video.webm" type="video/webm" />
> 6 │ </video>
│ ^^^^^^^^
7 │ <audio>
8 │ <source src="audio.ogg" type="audio/ogg" />

i Captions support users with hearing-impairments. They should be a transcription or translation of the dialogue, sound effects, musical cues, and other relevant audio information.


```

```
invalid.html:7:1 lint/a11y/useMediaCaption ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Provide a track for captions when using audio or video elements.

5 │ <source src="video.webm" type="video/webm" />
6 │ </video>
> 7 │ <audio>
│ ^^^^^^^
> 8 │ <source src="audio.ogg" type="audio/ogg" />
> 9 │ </audio>
│ ^^^^^^^^
10 │ <!-- track without kind="captions" -->
11 │ <video>

i Captions support users with hearing-impairments. They should be a transcription or translation of the dialogue, sound effects, musical cues, and other relevant audio information.


```

```
invalid.html:11:1 lint/a11y/useMediaCaption ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Provide a track for captions when using audio or video elements.

9 │ </audio>
10 │ <!-- track without kind="captions" -->
> 11 │ <video>
│ ^^^^^^^
> 12 │ <track kind="subtitles" src="subtitles.vtt" />
> 13 │ </video>
│ ^^^^^^^^
14 │

i Captions support users with hearing-impairments. They should be a transcription or translation of the dialogue, sound effects, musical cues, and other relevant audio information.


```
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!-- should not generate diagnostics -->
<video src="video.mp4">
<track kind="captions" src="captions.vtt" />
</video>
<audio src="audio.mp3">
<track kind="captions" src="captions.vtt" />
</audio>
<!-- muted videos don't need captions -->
<video muted src="video.mp4"></video>
<video muted>
<source src="video.webm" type="video/webm" />
</video>
<!-- case insensitive kind check -->
<video>
<track kind="Captions" src="captions.vtt" />
</video>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
source: crates/biome_html_analyze/tests/spec_tests.rs
expression: valid.html
---
# Input
```html
<!-- should not generate diagnostics -->
<video src="video.mp4">
<track kind="captions" src="captions.vtt" />
</video>
<audio src="audio.mp3">
<track kind="captions" src="captions.vtt" />
</audio>
<!-- muted videos don't need captions -->
<video muted src="video.mp4"></video>
<video muted>
<source src="video.webm" type="video/webm" />
</video>
<!-- case insensitive kind check -->
<video>
<track kind="Captions" src="captions.vtt" />
</video>

```