feat: add highlighted protocol views and inline operation forms
Reference site / build (push) Canceled after 0s
Reference site / build (push) Canceled after 0s
Expose Rust Text View tokens and safely highlight signatures and JSON results. Add animated Interface and inline Run disclosures with a shared 10ms line cadence, typed argument inputs, and explicit execution. Update documentation and browser regressions for the new controls and explicit counter-value responses.
This commit is contained in:
@@ -161,10 +161,11 @@ impl BrowserSession {
|
||||
// Reconstruct requested coverage from fresh Client state, never from Host data.
|
||||
let observation = self.manual_observation(path, depth)?;
|
||||
let object = self.fresh_object(path)?;
|
||||
let text =
|
||||
wip_text_view::render_object(&object).map_err(|e| ManualError::new("render", e))?;
|
||||
let tokens =
|
||||
wip_text_view::tokenize_object(&object).map_err(|e| ManualError::new("render", e))?;
|
||||
let text = wip_text_view::to_plain_text(&tokens);
|
||||
Ok(
|
||||
json!({"path": path, "observation": observation, "textView": text, "cached": cached, "stats": self.manual_stats()}),
|
||||
json!({"path": path, "observation": observation, "textView": text, "textViewTokens": text_view_tokens(&tokens), "cached": cached, "stats": self.manual_stats()}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -221,13 +222,14 @@ impl BrowserSession {
|
||||
.descriptor
|
||||
.as_ref()
|
||||
.ok_or_else(missing_observation)?;
|
||||
let text = wip_text_view::render_interface(&wip_text_view::Interface {
|
||||
let tokens = wip_text_view::tokenize_interface(&wip_text_view::Interface {
|
||||
reference: &reference,
|
||||
descriptor,
|
||||
})
|
||||
.map_err(|e| ManualError::new("render", e))?;
|
||||
let text = wip_text_view::to_plain_text(&tokens);
|
||||
Ok(
|
||||
json!({"reference": reference_json(&reference), "descriptor": descriptor_json(descriptor), "textView": text, "cached": cached, "stats": self.manual_stats()}),
|
||||
json!({"reference": reference_json(&reference), "descriptor": descriptor_json(descriptor), "textView": text, "textViewTokens": text_view_tokens(&tokens), "cached": cached, "stats": self.manual_stats()}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -277,6 +279,34 @@ impl BrowserSession {
|
||||
}
|
||||
}
|
||||
|
||||
// Display-only DTOs: preserve WIP text verbatim, not HTML or protocol identity.
|
||||
fn text_view_tokens(tokens: &[wip_text_view::Token]) -> Vec<Json> {
|
||||
tokens
|
||||
.iter()
|
||||
.map(|token| json!({"kind": text_view_token_kind(token.kind()), "text": token.text()}))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn text_view_token_kind(kind: wip_text_view::TokenKind) -> &'static str {
|
||||
use wip_text_view::TokenKind;
|
||||
match kind {
|
||||
TokenKind::Keyword => "keyword",
|
||||
TokenKind::TypeName => "type-name",
|
||||
TokenKind::OperationName => "operation-name",
|
||||
TokenKind::ParameterName => "parameter-name",
|
||||
TokenKind::FieldName => "field-name",
|
||||
TokenKind::CaseName => "case-name",
|
||||
TokenKind::String => "string",
|
||||
TokenKind::Reference => "reference",
|
||||
TokenKind::Documentation => "documentation",
|
||||
TokenKind::Symbol => "symbol",
|
||||
TokenKind::Whitespace => "whitespace",
|
||||
TokenKind::Newline => "newline",
|
||||
TokenKind::Indentation => "indentation",
|
||||
_ => "plain",
|
||||
}
|
||||
}
|
||||
|
||||
fn missing_observation() -> ManualError {
|
||||
ManualError {
|
||||
kind: "client",
|
||||
|
||||
@@ -166,6 +166,9 @@ fn signatures_are_real_renderer_output_and_exclude_metadata() {
|
||||
observed["textView"],
|
||||
wip_text_view::render_object(&object).unwrap()
|
||||
);
|
||||
assert_display_tokens(&observed, &wip_text_view::tokenize_object(&object).unwrap());
|
||||
let cached = s.manual_observe("/quoted", 0.0).unwrap();
|
||||
assert_eq!(cached["textViewTokens"], observed["textViewTokens"]);
|
||||
let text = observed["textView"].as_str().unwrap();
|
||||
assert!(text.contains("引用 \\\"quote\\\" \\\\ space\\nnext\\tline "));
|
||||
assert!(text.ends_with("}\n"));
|
||||
@@ -187,6 +190,17 @@ fn signatures_are_real_renderer_output_and_exclude_metadata() {
|
||||
})
|
||||
.unwrap()
|
||||
);
|
||||
assert_display_tokens(
|
||||
&fetched,
|
||||
&wip_text_view::tokenize_interface(&wip_text_view::Interface {
|
||||
reference: &reference,
|
||||
descriptor,
|
||||
})
|
||||
.unwrap(),
|
||||
);
|
||||
let cached = s.manual_interface("/", "document").unwrap();
|
||||
assert_eq!(cached["textViewTokens"], fetched["textViewTokens"]);
|
||||
assert_eq!(fetched["reference"], reference_json(&reference));
|
||||
assert_eq!(fetched["descriptor"]["operations"][0]["name"], "read");
|
||||
assert_eq!(
|
||||
fetched["descriptor"]["operations"][0]["parameters"],
|
||||
@@ -194,6 +208,98 @@ fn signatures_are_real_renderer_output_and_exclude_metadata() {
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_display_tokens(response: &Json, tokens: &[wip_text_view::Token]) {
|
||||
let dto = response["textViewTokens"].as_array().unwrap();
|
||||
assert_eq!(dto.len(), tokens.len());
|
||||
for (entry, token) in dto.iter().zip(tokens) {
|
||||
assert_eq!(entry.as_object().unwrap().len(), 2);
|
||||
assert_eq!(entry["kind"], text_view_token_kind(token.kind()));
|
||||
assert_eq!(entry["text"], token.text());
|
||||
}
|
||||
let concatenated: String = dto
|
||||
.iter()
|
||||
.map(|entry| entry["text"].as_str().unwrap())
|
||||
.collect();
|
||||
assert_eq!(concatenated, response["textView"].as_str().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_token_kinds_use_the_explicit_kebab_case_whitelist() {
|
||||
use wip_text_view::TokenKind;
|
||||
for (kind, expected) in [
|
||||
(TokenKind::Keyword, "keyword"),
|
||||
(TokenKind::TypeName, "type-name"),
|
||||
(TokenKind::OperationName, "operation-name"),
|
||||
(TokenKind::ParameterName, "parameter-name"),
|
||||
(TokenKind::FieldName, "field-name"),
|
||||
(TokenKind::CaseName, "case-name"),
|
||||
(TokenKind::String, "string"),
|
||||
(TokenKind::Reference, "reference"),
|
||||
(TokenKind::Documentation, "documentation"),
|
||||
(TokenKind::Symbol, "symbol"),
|
||||
(TokenKind::Whitespace, "whitespace"),
|
||||
(TokenKind::Newline, "newline"),
|
||||
(TokenKind::Indentation, "indentation"),
|
||||
] {
|
||||
assert_eq!(text_view_token_kind(kind), expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_tokens_preserve_quoted_markup_and_unicode_as_raw_text() {
|
||||
let raw = r#"<script title="引用 🦀">& 'text'</script>"#;
|
||||
let quoted = r#""<script title=\"引用 🦀\">& 'text'</script>""#;
|
||||
let mut data: Json = serde_json::from_str(&manifest()).unwrap();
|
||||
data["nodes"][1]["title"] = json!(raw);
|
||||
let mut s = BrowserSession::new(&data.to_string()).unwrap();
|
||||
let observed = s.manual_observe("/quoted", 0.0).unwrap();
|
||||
let object = s.fresh_object("/quoted").unwrap();
|
||||
assert_eq!(
|
||||
observed["textView"],
|
||||
wip_text_view::render_object(&object).unwrap()
|
||||
);
|
||||
assert_display_tokens(&observed, &wip_text_view::tokenize_object(&object).unwrap());
|
||||
assert!(
|
||||
observed["textViewTokens"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.contains(&json!({"kind": "string", "text": quoted}))
|
||||
);
|
||||
|
||||
s.manual_interface("/", "document").unwrap();
|
||||
let reference = Worldspace::document_interface();
|
||||
let mut descriptor = s
|
||||
.client
|
||||
.interface(&s.session, &reference)
|
||||
.unwrap()
|
||||
.descriptor
|
||||
.clone()
|
||||
.unwrap();
|
||||
descriptor.documentation = Some(Documentation {
|
||||
summary: raw.into(),
|
||||
details: None,
|
||||
});
|
||||
descriptor.operations[0].name = raw.into();
|
||||
let interface = wip_text_view::Interface {
|
||||
reference: &reference,
|
||||
descriptor: &descriptor,
|
||||
};
|
||||
let tokens = wip_text_view::tokenize_interface(&interface).unwrap();
|
||||
let dto = json!({
|
||||
"textView": wip_text_view::render_interface(&interface).unwrap(),
|
||||
"textViewTokens": text_view_tokens(&tokens),
|
||||
});
|
||||
// JSON transport must retain literal text, never HTML entities or generated markup.
|
||||
let decoded: Json = serde_json::from_str(&dto.to_string()).unwrap();
|
||||
assert_display_tokens(&decoded, &tokens);
|
||||
let entries = decoded["textViewTokens"].as_array().unwrap();
|
||||
let quoted_identifier = r#"`<script title=\"引用 🦀\">& 'text'</script>`"#;
|
||||
assert!(entries.contains(&json!({"kind": "operation-name", "text": quoted_identifier})));
|
||||
assert!(entries.contains(&json!({"kind": "documentation", "text": format!("/// {quoted}")})));
|
||||
assert_eq!(descriptor.operations[0].name, raw);
|
||||
assert_eq!(reference, Worldspace::document_interface());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_tagged_values_round_trip_without_integer_or_bytes_coercion() {
|
||||
let values = vec![
|
||||
|
||||
Reference in New Issue
Block a user