修复视觉门 HTML 与 classic 脚本时序
隔离惰性与原始文本容器中的 Canvas 和样式诱饵 按解析阻塞与 defer 阶段组合 classic 脚本并拒绝 async 不确定顺序 补充视觉回归、权威技术方案与长期决策记录
This commit is contained in:
+235
-76
@@ -563,23 +563,49 @@ fn strip_art_reference_comments(content: &str) -> String {
|
||||
output
|
||||
}
|
||||
|
||||
fn strip_script_blocks(content: &str) -> String {
|
||||
let mut output = String::with_capacity(content.len());
|
||||
let mut cursor = 0;
|
||||
while let Some(start_offset) = content[cursor..].find("<script") {
|
||||
let start = cursor + start_offset;
|
||||
output.push_str(&content[cursor..start]);
|
||||
let Some(open_end_offset) = content[start..].find('>') else {
|
||||
return output;
|
||||
fn rendered_html_markup_and_stylesheets(content: &str) -> (String, String) {
|
||||
let lower = content.to_ascii_lowercase();
|
||||
let mut markup = String::with_capacity(content.len());
|
||||
let mut stylesheets = String::new();
|
||||
let mut cursor = 0usize;
|
||||
while let Some(offset) = lower[cursor..].find('<') {
|
||||
let tag_start = cursor + offset;
|
||||
if lower[tag_start..].starts_with("<!--") {
|
||||
cursor = lower[tag_start + 4..]
|
||||
.find("-->")
|
||||
.map(|end| tag_start + 4 + end + 3)
|
||||
.unwrap_or(lower.len());
|
||||
continue;
|
||||
}
|
||||
let Some(tag_after) = html_tag_end(&lower, tag_start) else {
|
||||
break;
|
||||
};
|
||||
let body_start = start + open_end_offset + 1;
|
||||
let Some(end_offset) = content[body_start..].find("</script>") else {
|
||||
return output;
|
||||
};
|
||||
cursor = body_start + end_offset + "</script>".len();
|
||||
let tag_end = tag_after - 1;
|
||||
let tag = &lower[tag_start..=tag_end];
|
||||
if html_tag_starts_element(tag, "style") {
|
||||
let Some((close_start, close_end)) =
|
||||
raw_text_html_element_close(&lower, tag_after, "style")
|
||||
else {
|
||||
break;
|
||||
};
|
||||
stylesheets.push_str(&lower[tag_after..close_start]);
|
||||
stylesheets.push('\n');
|
||||
cursor = close_end;
|
||||
continue;
|
||||
}
|
||||
if html_tag_starts_element(tag, "script") {
|
||||
cursor = raw_text_html_element_end(&lower, tag_after, "script");
|
||||
continue;
|
||||
}
|
||||
if let Some(end) = html_non_executable_container_end(&lower, tag, tag_end) {
|
||||
cursor = end;
|
||||
continue;
|
||||
}
|
||||
markup.push_str(tag);
|
||||
markup.push('\n');
|
||||
cursor = tag_after;
|
||||
}
|
||||
output.push_str(&content[cursor..]);
|
||||
output
|
||||
(markup, stylesheets)
|
||||
}
|
||||
|
||||
fn relative_visual_url_resolves_to_asset(value: &str, asset_path: &str) -> bool {
|
||||
@@ -824,25 +850,19 @@ fn selector_matches_tag(selector: &str, tag: &str) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
fn tag_is_hidden_by_stylesheet(tag: &str, markup: &str, default_dimensions: (u32, u32)) -> bool {
|
||||
markup.split("<style").skip(1).any(|tail| {
|
||||
let Some(body_start) = tail.find('>').map(|offset| offset + 1) else {
|
||||
return false;
|
||||
};
|
||||
let Some(body_end) = tail[body_start..].find("</style>") else {
|
||||
return false;
|
||||
};
|
||||
tail[body_start..body_start + body_end]
|
||||
.split('}')
|
||||
.any(|rule| {
|
||||
rule.rsplit_once('{')
|
||||
.is_some_and(|(selector, declarations)| {
|
||||
selector_matches_tag(selector, tag)
|
||||
&& tag_is_obviously_hidden_or_tiny(
|
||||
&format!("<div style=\"{declarations}\">"),
|
||||
default_dimensions,
|
||||
)
|
||||
})
|
||||
fn tag_is_hidden_by_stylesheet(
|
||||
tag: &str,
|
||||
stylesheets: &str,
|
||||
default_dimensions: (u32, u32),
|
||||
) -> bool {
|
||||
stylesheets.split('}').any(|rule| {
|
||||
rule.rsplit_once('{')
|
||||
.is_some_and(|(selector, declarations)| {
|
||||
selector_matches_tag(selector, tag)
|
||||
&& tag_is_obviously_hidden_or_tiny(
|
||||
&format!("<div style=\"{declarations}\">"),
|
||||
default_dimensions,
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -3205,7 +3225,7 @@ fn game_index_visibly_uses_visual_asset(
|
||||
};
|
||||
let original_content = strip_art_reference_comments(html);
|
||||
let heuristic_content = original_content.to_ascii_lowercase();
|
||||
let markup = strip_script_blocks(&heuristic_content);
|
||||
let (markup, stylesheets) = rendered_html_markup_and_stylesheets(&heuristic_content);
|
||||
|
||||
let mut tag_cursor = 0;
|
||||
while let Some(start_offset) = markup[tag_cursor..].find('<') {
|
||||
@@ -3217,41 +3237,24 @@ fn game_index_visibly_uses_visual_asset(
|
||||
let tag = &markup[start..end];
|
||||
if requirement == VisualAssetUsageRequirement::AnyVisible
|
||||
&& tag_visibly_uses_visual_asset(tag, &asset_path, asset_dimensions)
|
||||
&& !tag_is_hidden_by_stylesheet(tag, &markup, asset_dimensions)
|
||||
&& !tag_is_hidden_by_stylesheet(tag, &stylesheets, asset_dimensions)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
tag_cursor = end;
|
||||
}
|
||||
|
||||
let mut style_cursor = 0;
|
||||
while let Some(start_offset) = markup[style_cursor..].find("<style") {
|
||||
let start = style_cursor + start_offset;
|
||||
let Some(open_end_offset) = markup[start..].find('>') else {
|
||||
break;
|
||||
for rule in stylesheets.split('}') {
|
||||
let Some((selector, declarations)) = rule.rsplit_once('{') else {
|
||||
continue;
|
||||
};
|
||||
let body_start = start + open_end_offset + 1;
|
||||
let Some(end_offset) = markup[body_start..].find("</style>") else {
|
||||
break;
|
||||
};
|
||||
let body_end = body_start + end_offset;
|
||||
let style = &markup[body_start..body_end];
|
||||
for rule in style.split('}') {
|
||||
let Some((selector, declarations)) = rule.rsplit_once('{') else {
|
||||
continue;
|
||||
};
|
||||
if requirement == VisualAssetUsageRequirement::AnyVisible
|
||||
&& css_contains_resolving_url(declarations, &asset_path)
|
||||
&& selector_is_bound_to_markup(selector, &markup)
|
||||
&& !tag_is_obviously_hidden_or_tiny(
|
||||
&format!("<div style=\"{declarations}\">"),
|
||||
(0, 0),
|
||||
)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if requirement == VisualAssetUsageRequirement::AnyVisible
|
||||
&& css_contains_resolving_url(declarations, &asset_path)
|
||||
&& selector_is_bound_to_markup(selector, &markup)
|
||||
&& !tag_is_obviously_hidden_or_tiny(&format!("<div style=\"{declarations}\">"), (0, 0))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
style_cursor = body_end + "</style>".len();
|
||||
}
|
||||
|
||||
let Some(canvas_dimensions) = markup.split('>').find_map(|tag| {
|
||||
@@ -3260,7 +3263,7 @@ fn game_index_visibly_uses_visual_asset(
|
||||
.starts_with("canvas");
|
||||
(is_canvas
|
||||
&& !tag_is_obviously_hidden_or_tiny(tag, (300, 150))
|
||||
&& !tag_is_hidden_by_stylesheet(tag, &markup, (300, 150)))
|
||||
&& !tag_is_hidden_by_stylesheet(tag, &stylesheets, (300, 150)))
|
||||
.then(|| {
|
||||
(
|
||||
tag_dimension(tag, "width", "width").unwrap_or(300.0),
|
||||
@@ -4548,7 +4551,7 @@ fn executable_javascript_from_html(content: &str) -> String {
|
||||
.into_iter()
|
||||
.filter_map(|source| match source {
|
||||
ExecutableClassicScriptSource::Inline(body) => Some(body),
|
||||
ExecutableClassicScriptSource::External(_) => None,
|
||||
ExecutableClassicScriptSource::External { .. } => None,
|
||||
})
|
||||
.fold(String::new(), |mut executable, body| {
|
||||
executable.push_str(&body);
|
||||
@@ -4560,7 +4563,17 @@ fn executable_javascript_from_html(content: &str) -> String {
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
enum ExecutableClassicScriptSource {
|
||||
Inline(String),
|
||||
External(String),
|
||||
External {
|
||||
source: String,
|
||||
timing: ClassicExternalScriptTiming,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum ClassicExternalScriptTiming {
|
||||
ParserBlocking,
|
||||
Deferred,
|
||||
Async,
|
||||
}
|
||||
|
||||
fn executable_classic_script_sources_from_html(
|
||||
@@ -4605,9 +4618,17 @@ fn executable_classic_script_sources_from_html(
|
||||
if let Some(source) = html_attribute_value(tag, "src") {
|
||||
let value_offset = source.as_ptr() as usize - tag.as_ptr() as usize;
|
||||
let original_tag = &content[tag_start..=tag_end];
|
||||
executable.push(ExecutableClassicScriptSource::External(
|
||||
original_tag[value_offset..value_offset + source.len()].to_string(),
|
||||
));
|
||||
let timing = if html_has_attribute(tag, "async") {
|
||||
ClassicExternalScriptTiming::Async
|
||||
} else if html_has_attribute(tag, "defer") {
|
||||
ClassicExternalScriptTiming::Deferred
|
||||
} else {
|
||||
ClassicExternalScriptTiming::ParserBlocking
|
||||
};
|
||||
executable.push(ExecutableClassicScriptSource::External {
|
||||
source: original_tag[value_offset..value_offset + source.len()].to_string(),
|
||||
timing,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let body = &content[body_start..close_start];
|
||||
@@ -8218,6 +8239,19 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at(
|
||||
let mut total_bytes = 0_u64;
|
||||
let mut pending = std::collections::VecDeque::new();
|
||||
let classic_script_sources = executable_classic_script_sources_from_html(html);
|
||||
if classic_script_sources.iter().any(|source| {
|
||||
matches!(
|
||||
source,
|
||||
ExecutableClassicScriptSource::External {
|
||||
timing: ClassicExternalScriptTiming::Async,
|
||||
..
|
||||
}
|
||||
)
|
||||
}) {
|
||||
return Err(
|
||||
"自主构建 classic async 外部脚本执行顺序不可静态证明,视觉门失败关闭".to_string(),
|
||||
);
|
||||
}
|
||||
let mut classic_external_contents = BTreeMap::<String, String>::new();
|
||||
let mut module_contents = std::collections::BTreeMap::<String, String>::new();
|
||||
let mut module_analyses = BTreeMap::<String, JavascriptModuleAnalysis>::new();
|
||||
@@ -8574,11 +8608,14 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at(
|
||||
break;
|
||||
}
|
||||
}
|
||||
let mut classic_ordered_unit = String::new();
|
||||
let mut parser_blocking_classic_unit = String::new();
|
||||
let mut deferred_classic_unit = String::new();
|
||||
for source in classic_script_sources {
|
||||
let script = match source {
|
||||
ExecutableClassicScriptSource::Inline(script) => script,
|
||||
ExecutableClassicScriptSource::External(source) => {
|
||||
let (script, timing) = match source {
|
||||
ExecutableClassicScriptSource::Inline(script) => {
|
||||
(script, ClassicExternalScriptTiming::ParserBlocking)
|
||||
}
|
||||
ExecutableClassicScriptSource::External { source, timing } => {
|
||||
let local_path = local_gameplay_script_path_from(None, &source)
|
||||
.ok_or_else(|| format!("自主构建外部脚本路径不受支持:{source}"))?;
|
||||
let Some(script) = classic_external_contents.get(&local_path) else {
|
||||
@@ -8586,13 +8623,19 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at(
|
||||
};
|
||||
output.classic_global.push_str(script);
|
||||
output.classic_global.push('\n');
|
||||
script.clone()
|
||||
(script.clone(), timing)
|
||||
}
|
||||
};
|
||||
classic_ordered_unit.push_str(&script);
|
||||
classic_ordered_unit.push('\n');
|
||||
let unit = match timing {
|
||||
ClassicExternalScriptTiming::ParserBlocking => &mut parser_blocking_classic_unit,
|
||||
ClassicExternalScriptTiming::Deferred => &mut deferred_classic_unit,
|
||||
ClassicExternalScriptTiming::Async => unreachable!("async rejected before loading"),
|
||||
};
|
||||
unit.push_str(&script);
|
||||
unit.push('\n');
|
||||
}
|
||||
output.classic_ordered_unit = Some(classic_ordered_unit);
|
||||
parser_blocking_classic_unit.push_str(&deferred_classic_unit);
|
||||
output.classic_ordered_unit = Some(parser_blocking_classic_unit);
|
||||
validate_javascript_module_links(&module_analyses)?;
|
||||
output
|
||||
.module_units
|
||||
@@ -11062,6 +11105,122 @@ mod visible_destination_tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canvas_scan_ignores_inert_and_raw_text_container_decoys() {
|
||||
let root = tempfile::tempdir().expect("create inert canvas fixture");
|
||||
let drawing_script = r#"<script>
|
||||
const context=document.querySelector('canvas').getContext('2d');
|
||||
const art=new Image();
|
||||
art.src='../assets/art-spec.png';
|
||||
context.drawImage(art,0,0,64,64);
|
||||
</script>"#;
|
||||
for (open, close) in [
|
||||
("<template>", "</template>"),
|
||||
("<textarea>", "</textarea>"),
|
||||
("<noscript>", "</noscript>"),
|
||||
("<style>", "</style>"),
|
||||
("<title>", "</title>"),
|
||||
("<xmp>", "</xmp>"),
|
||||
("<iframe>", "</iframe>"),
|
||||
("<noembed>", "</noembed>"),
|
||||
] {
|
||||
let html = format!(
|
||||
"<!doctype html><html><body>{open}<canvas width=320 height=180></canvas>{close}{drawing_script}</body></html>"
|
||||
);
|
||||
assert!(
|
||||
!game_index_visibly_uses_visual_asset(
|
||||
root.path(),
|
||||
html.as_bytes(),
|
||||
"assets/art-spec.png",
|
||||
(64, 64),
|
||||
VisualAssetUsageRequirement::CanvasDraw,
|
||||
),
|
||||
"{open} content must not provide a visible canvas",
|
||||
);
|
||||
}
|
||||
|
||||
let plaintext = format!(
|
||||
"<!doctype html><html><body>{drawing_script}<plaintext><canvas width=320 height=180></canvas></body></html>"
|
||||
);
|
||||
assert!(!game_index_visibly_uses_visual_asset(
|
||||
root.path(),
|
||||
plaintext.as_bytes(),
|
||||
"assets/art-spec.png",
|
||||
(64, 64),
|
||||
VisualAssetUsageRequirement::CanvasDraw,
|
||||
));
|
||||
|
||||
let inert_stylesheet = format!(
|
||||
"<!doctype html><html><body><template><style>canvas{{display:none}}</style></template><canvas width=320 height=180></canvas>{drawing_script}</body></html>"
|
||||
);
|
||||
assert!(game_index_visibly_uses_visual_asset(
|
||||
root.path(),
|
||||
inert_stylesheet.as_bytes(),
|
||||
"assets/art-spec.png",
|
||||
(64, 64),
|
||||
VisualAssetUsageRequirement::CanvasDraw,
|
||||
));
|
||||
|
||||
let active_stylesheet = format!(
|
||||
"<!doctype html><html><head><style>canvas{{display:none}}</style></head><body><canvas width=320 height=180></canvas>{drawing_script}</body></html>"
|
||||
);
|
||||
assert!(!game_index_visibly_uses_visual_asset(
|
||||
root.path(),
|
||||
active_stylesheet.as_bytes(),
|
||||
"assets/art-spec.png",
|
||||
(64, 64),
|
||||
VisualAssetUsageRequirement::CanvasDraw,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deferred_classic_external_script_does_not_execute_before_following_inline_script() {
|
||||
let root = tempfile::tempdir().expect("create deferred classic fixture");
|
||||
fs::create_dir_all(root.path().join("game")).expect("create game directory");
|
||||
fs::write(
|
||||
root.path().join("game/render.js"),
|
||||
"renderFrame=()=>context.drawImage(art,0,0,64,64);",
|
||||
)
|
||||
.expect("write deferred classic script");
|
||||
let html = br#"<!doctype html><html><body>
|
||||
<canvas width=320 height=180></canvas>
|
||||
<script>const context=document.querySelector('canvas').getContext('2d');const art=new Image();art.src='../assets/art-spec.png';let renderFrame;</script>
|
||||
<script defer src="./render.js"></script>
|
||||
<script>renderFrame();</script>
|
||||
</body></html>"#;
|
||||
assert!(!game_index_visibly_uses_visual_asset(
|
||||
root.path(),
|
||||
html,
|
||||
"assets/art-spec.png",
|
||||
(64, 64),
|
||||
VisualAssetUsageRequirement::CanvasDraw,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn async_classic_external_script_fails_closed_when_order_is_not_provable() {
|
||||
let root = tempfile::tempdir().expect("create async classic fixture");
|
||||
fs::create_dir_all(root.path().join("game")).expect("create game directory");
|
||||
fs::write(
|
||||
root.path().join("game/render.js"),
|
||||
"renderFrame=()=>context.drawImage(art,0,0,64,64);",
|
||||
)
|
||||
.expect("write async classic script");
|
||||
let html = br#"<!doctype html><html><body>
|
||||
<canvas width=320 height=180></canvas>
|
||||
<script>const context=document.querySelector('canvas').getContext('2d');const art=new Image();art.src='../assets/art-spec.png';let renderFrame;</script>
|
||||
<script async src="./render.js"></script>
|
||||
<script>renderFrame();</script>
|
||||
</body></html>"#;
|
||||
assert!(!game_index_visibly_uses_visual_asset(
|
||||
root.path(),
|
||||
html,
|
||||
"assets/art-spec.png",
|
||||
(64, 64),
|
||||
VisualAssetUsageRequirement::CanvasDraw,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn esm_dependency_initialization_precedes_importer_evaluation() {
|
||||
let root = tempfile::tempdir().expect("create ESM evaluation order fixture");
|
||||
|
||||
@@ -5999,5 +5999,6 @@
|
||||
## 2026-08-04 静态视觉门脚本与 ESM 求值顺序
|
||||
|
||||
- HTML 输入只在非 raw-text 区域屏蔽真正的 `<!-- -->` 注释;`script / style` 等 raw-text 原文保持逐字不变,再交给各自 parser / semantic 处理。禁止在整份 HTML 上按文本删除 `//` 或 `/* */`,否则字符串中的 `https://`、路径和注释形状会被破坏;HTML 注释内的标签、脚本和素材路径仍不得形成视觉证据。
|
||||
- classic script 分析单元按 `game/index.html` 的真实标签顺序交错组合 inline 与本地 external 正文;带 `src` 标签的 inline body 继续忽略。外部文件仍执行可信普通文件、`game/` 边界、文件数与累计体积门禁,重复标签按浏览器出现次数保留求值位置。
|
||||
- classic script 分析单元把 inline 与无 `defer / async` 的本地 external 正文按 `game/index.html` 标签顺序交错组成 parser-blocking 段,再把 classic external `defer` 按文档顺序放到解析完成后的 deferred 段;不得把 defer-before-inline 误投影为外链先执行。classic external `async` 的下载完成顺序不可静态证明,当前静态门直接失败关闭。带 `src` 标签的 inline body 继续忽略;外部文件仍执行可信普通文件、`game/` 边界、文件数与累计体积门禁,重复标签按浏览器出现次数保留求值位置。
|
||||
- Canvas 尺寸、可见性、元素绑定和 stylesheet 选择器扫描只消费浏览器可渲染标记;`template / textarea / noscript / title / style / xmp / iframe / noembed / plaintext` 内的 Canvas、标签和样式诱饵全部跳过。活动顶层 stylesheet 与可见标记分开提取,既允许真实 CSS 参与隐藏/尺寸判断,也不把 CSS raw-text 中的伪标签当作 DOM。
|
||||
- ESM 组合单元按 dependency 初始化先于 importer 顶层求值排列。import reference 的 span replacement 仍基于原 importer 完成,随后把已闭包的 dependency projection 放在 importer 前并对最终单元重跑 parser、semantic、单元 `2 MiB` 与累计投影 `32 MiB` 门禁;循环模块继续按 `(origin module, original root binding)` canonical identity 去重并要求有界固定点收敛。
|
||||
|
||||
@@ -842,7 +842,7 @@ game-project/
|
||||
- tool-plan 成功响应落账前,对内置 Runtime 原生函数与 legacy wrapper 的合法、无重复 key JSON arguments 按工具 schema 的精确位置做项目路径 canonicalization:`file.*.path`、`project.patchset.changes[*].path`、`project.git_commit.paths[*]`、`command.*.cwd`、`image.inspect.paths[*]` 与 `canvas.asset_generate.outputPath` 若是当前项目根目录内的完整绝对路径,转换为 `/` 分隔的项目相对路径后再校验、持久化并执行;源码/叙述字段、任务产物描述、动态 MCP arguments 和项目外绝对路径不得改写,后两者继续由绝对路径门禁失败关闭。项目根只允许搜索/列举范围与命令 cwd 规范化为 `.`,不能成为文件目标。当前进程与重启恢复都必须从同一份规范化 handoff 重放,禁止分别执行原响应和持久响应。
|
||||
|
||||
- 循环 ESM 的投影身份固定为 `(origin module, original root binding)`;canonical 重命名只改变组合单元中的展示名,身份随投影闭包传播。删除已存在的回流声明后仍必须把 import 引用改接到 importer 中同一身份的既有 canonical;固定点未在“模块数 + 1”轮内收敛时失败关闭,不能返回最后一轮仍变化的部分结果。inline module 必须先提取原文并计入与外部脚本共享的累计源码 `2 MiB` 上限,再执行语法和语义校验;超限源码不能因无效语法被过滤,小型无效模块也必须明确失败关闭。
|
||||
- HTML 静态门只屏蔽 raw-text 容器之外的真正 HTML 注释,`script / style` 原文必须逐字交给对应 parser;禁止对整份 HTML 做非词法 `//`、`/* */` 删除,字符串中的 `https://`、路径和注释形状不能被截断,HTML 注释诱饵仍不得进入标签或脚本证据。classic inline 与 external 脚本按 HTML 标签出现顺序组成同一个 global 分析单元,带 `src` 的标签忽略 inline body,不能把所有 inline 提前到 external 之前。
|
||||
- HTML 静态门只屏蔽 raw-text 容器之外的真正 HTML 注释,`script / style` 原文必须逐字交给对应 parser;禁止对整份 HTML 做非词法 `//`、`/* */` 删除,字符串中的 `https://`、路径和注释形状不能被截断,HTML 注释诱饵仍不得进入标签或脚本证据。Canvas 尺寸、可见性、元素绑定和 stylesheet 选择器扫描只读取浏览器可渲染标记,必须完整跳过 `template / textarea / noscript / title / style / xmp / iframe / noembed / plaintext` 的标签诱饵;活动顶层 stylesheet 独立提取,不能借用 inert 容器中的样式或把 CSS raw-text 当 HTML 标签。classic inline 与无 `defer / async` 的 external 脚本按 HTML 标签出现顺序组成 parser-blocking global 段;classic external `defer` 必须在解析阻塞段之后按文档顺序求值,不能提前到标签之后的 inline 前,`async` 因下载完成顺序不可静态证明而失败关闭。带 `src` 的标签忽略 inline body,普通脚本顺序不得退化。
|
||||
- ESM 投影的最终组合顺序必须反映 dependency 先初始化、importer 后求值:先在原 importer 坐标上完成 import reference span replacement,再把依赖闭包投影放到 importer 顶层正文之前,使 dependency 在声明后安装的 function-valued export 能被 importer 调用看见。最终单元继续重跑 parser / semantic 和单元 `2 MiB`、累计投影 `32 MiB` 门禁;canonical identity、循环回接去重与有界固定点规则不因顺序调整而放宽。
|
||||
|
||||
## 2026-07-31 长耗时与恢复收口
|
||||
|
||||
Reference in New Issue
Block a user