Files
moli/moli-script/src/classify.rs
T
2026-08-11 00:10:12 +08:00

177 lines
5.9 KiB
Rust

use moli_page_types::{ScriptKind, ScriptMode, ScriptSourceKind};
use super::scheduling::{
ScriptElementClassification, ScriptElementClassificationInput, ScriptPreparationClassification,
ScriptPreparationClassificationInput, ScriptPreparationDisposition, ScriptSchedulingInput,
};
pub fn classify_script_preparation(
input: ScriptPreparationClassificationInput<'_>,
) -> ScriptPreparationClassification {
let element = classify_script_element(input.element);
let scheduling = ScriptSchedulingInput {
parser_inserted: input.parser_inserted,
allow_parser_blocking_modes: input.allow_parser_blocking_modes,
force_async: input.force_async,
async_attribute_present: input.async_attribute_present,
defer_attribute_present: input.defer_attribute_present,
kind: element.kind,
source_kind: input.source_kind,
};
let disposition = match element.kind {
ScriptKind::Classic => ScriptPreparationDisposition::Classic(
classify_script_mode(scheduling).expect("classic scripts have an execution mode"),
),
ScriptKind::Module => ScriptPreparationDisposition::Module(
classify_script_mode(scheduling).expect("module scripts have an execution mode"),
),
ScriptKind::ImportMap => ScriptPreparationDisposition::ImportMap,
ScriptKind::DataBlock => ScriptPreparationDisposition::DataBlock,
};
ScriptPreparationClassification {
disposition,
legacy_event_for_mismatch: element.legacy_event_for_mismatch,
}
}
pub fn classify_script_mode(input: ScriptSchedulingInput) -> Option<ScriptMode> {
let force_async = input.force_async && !input.parser_inserted;
let mode = match input.kind {
ScriptKind::Module => {
if input.async_attribute_present || force_async {
ScriptMode::Async
} else if input.parser_inserted && input.allow_parser_blocking_modes {
ScriptMode::ModuleDefer
} else {
ScriptMode::ModuleInOrder
}
}
ScriptKind::Classic => match input.source_kind {
ScriptSourceKind::Inline => ScriptMode::Normal,
ScriptSourceKind::External => {
if input.async_attribute_present || force_async {
ScriptMode::Async
} else if input.parser_inserted && input.allow_parser_blocking_modes {
if input.defer_attribute_present {
ScriptMode::Defer
} else {
ScriptMode::Normal
}
} else {
ScriptMode::InOrder
}
}
},
ScriptKind::ImportMap | ScriptKind::DataBlock => return None,
};
Some(mode)
}
pub fn classify_script_element(
input: ScriptElementClassificationInput<'_>,
) -> ScriptElementClassification {
let kind = classify_script_kind_from_attributes(input.script_type, input.language);
let legacy_event_for_mismatch = kind == ScriptKind::Classic
&& legacy_event_for_attributes_mismatch(input.for_attribute, input.event);
ScriptElementClassification {
kind,
legacy_event_for_mismatch,
}
}
pub fn classify_script_kind(script_type: Option<&str>) -> ScriptKind {
classify_script_kind_from_type(script_type)
}
pub fn html_script_element_supports_type(script_type: &str) -> bool {
matches!(script_type, "classic" | "module" | "importmap")
}
fn classify_script_kind_from_attributes(
script_type: Option<&str>,
language: Option<&str>,
) -> ScriptKind {
if script_type.is_some() {
return classify_script_kind_from_type(script_type);
}
classify_script_kind_from_language(language)
}
fn classify_script_kind_from_type(script_type: Option<&str>) -> ScriptKind {
let Some(raw_script_type) = script_type else {
return ScriptKind::Classic;
};
if raw_script_type.is_empty() {
return ScriptKind::Classic;
}
let script_type = trim_ascii_whitespace(raw_script_type);
if script_type.is_empty() {
// Current WPT script-type-and-language-js.html expects a whitespace-only
// type attribute not to execute, while a truly empty type remains classic.
return ScriptKind::DataBlock;
}
if is_javascript_mime_essence_for_script_type(script_type) {
return ScriptKind::Classic;
}
if script_type.eq_ignore_ascii_case("module") {
return ScriptKind::Module;
}
if script_type.eq_ignore_ascii_case("importmap") {
return ScriptKind::ImportMap;
}
ScriptKind::DataBlock
}
fn classify_script_kind_from_language(language: Option<&str>) -> ScriptKind {
let Some(language) = language else {
return ScriptKind::Classic;
};
if language.is_empty() {
return ScriptKind::Classic;
}
let mime = format!("text/{language}");
if is_javascript_mime_essence_exact(&mime) {
ScriptKind::Classic
} else {
ScriptKind::DataBlock
}
}
fn is_javascript_mime_essence_for_script_type(input: &str) -> bool {
let essence = trim_ascii_whitespace(input).to_ascii_lowercase();
moli_web_mime::is_javascript_mime_essence(&essence)
}
fn is_javascript_mime_essence_exact(input: &str) -> bool {
moli_web_mime::is_javascript_mime_essence(&input.to_ascii_lowercase())
}
fn legacy_event_for_attributes_mismatch(for_attribute: Option<&str>, event: Option<&str>) -> bool {
let (Some(for_attribute), Some(event)) = (for_attribute, event) else {
return false;
};
let for_attribute = trim_ascii_whitespace(for_attribute);
if !for_attribute.eq_ignore_ascii_case("window") {
return true;
}
!matches!(
trim_ascii_whitespace(event).to_ascii_lowercase().as_str(),
"onload" | "onload()"
)
}
fn trim_ascii_whitespace(value: &str) -> &str {
value.trim_matches(|ch: char| matches!(ch, '\t' | '\n' | '\u{000C}' | '\r' | ' '))
}