Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
52 changes: 46 additions & 6 deletions rust/contracts/rust-core-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,8 @@ or removing a command fails until the table changes in the same commit.
| `processLifecycle.resolve` | Decide how each process or resource of one Lithe instance ends |
| `processLifecycle.inventory` | Group a process snapshot of one Lithe instance into terminals, Agent tasks and background services |
| `processLifecycle.settle` | Turn confirmation answers into what ends, what is kept and the updated settings |
| `markdown.render` | Render and sanitizes shared Markdown |
| `markdown.render` | Renders and sanitizes shared Markdown |
| `markdown.resolveReference` | Decides what a link or an image of a rendered Markdown document points at |
| `keymap.parseContext` | Compile a bounded boolean shortcut context (from `lithe-core-rules`) |
| `keymap.evaluateContext` | Evaluate a precompiled shortcut context (from `lithe-core-rules`) |
| `keymap.analyzeConflicts` | Find conflicts among the final layered shortcut bindings (from `lithe-core-rules`) |
Expand Down Expand Up @@ -5467,11 +5468,50 @@ embedding. Inline `<svg>` keeps its drawing elements (shapes, text, `use`,
primitives) with their geometry and presentation attributes; `script`,
`foreignObject`, `style`, the animation elements, `feImage` and the `style`
attribute are removed, and `use`/`textPath` may only reference `#id` in the
same document. Windows asynchronously requests this existing command via its
central dispatcher, discards obsolete responses and falls back to the sanitized
fragment if syntax coloring fails. Render failures are shown as localized text;
there is no frontend Markdown parser fallback. This does not promise Windows
math or diagram rendering parity.
same document. Frontends request the command off their window thread, discard
responses to an older revision of the source and show the sanitized fragment
unchanged if syntax coloring, math or diagram rendering fails. Render failures
are shown as localized text; there is no frontend Markdown parser fallback.

`markdown.resolveReference` decides what one link destination or image `src` of
a rendered document points at, so every preview follows and loads the same
things. It accepts:

```json
{
"documentPath": "/workspace/docs/guide.md",
"workspaceRoot": "/workspace",
"reference": "../assets/logo.svg?v=2",
"purpose": "resource"
}
```

`workspaceRoot` is optional. A document inside it has the workspace as its
scope; without it, or for a document outside it, the scope is the document's
folder. `reference` is the attribute exactly as the HTML holds it. `purpose` is
`link` for a click and `resource` for an image the page loads. The response is
tagged by `kind`:

| kind | purpose | fields | meaning |
| --- | --- | --- | --- |
| `external` | link | `url` | An http, https or mailto address for the system's browser or mail client |
| `remote` | resource | `url` | An http or https address with a host; the frontend downloads it under its proxy policy |
| `anchor` | link | `anchor` | A `#fragment` of the document, without `#` and percent-decoded |
| `file` | both | `path`, `mime` for a resource | A file of the machine |
| `none` | both | — | Nothing is followed or loaded |

Addresses come back normalized as a browser reads them, and a scheme-relative
`//host` address gets `https:`. Any other scheme, including `file:`, `data:`,
`javascript:` and a one-letter Windows drive, is `none`. A path drops its query
and fragment and is percent-decoded; `/path` starts at the scope's root and any
other path at the document's folder, with `.` and `..` resolved lexically. A
link's `file` is that lexical path, which need not exist. A resource's `file`
must stay inside the scope both lexically and after Core resolves symbolic
links, must be an existing regular file, and must have an image extension
(`apng`, `avif`, `bmp`, `gif`, `ico`, `jpeg`, `jpg`, `png`, `svg`, `webp`); its
`path` is canonical and `mime` names the type. Otherwise the resource is
`none`. Frontends read a resource file without following another link and do not
load what this command refuses.

Shared examples: `rust/fixtures/editor/markdown-render-v1.json`,
`markdown-html-v1.json` and `markdown-rich-content-v1.json`.
Expand Down
4 changes: 4 additions & 0 deletions rust/crates/core/src/protocol/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ pub enum CoreCommand {
ProcessLifecycleSettle,
/// Renders and sanitizes shared Markdown (`markdown.render`).
MarkdownRender,
/// Decides what a link or an image of a rendered Markdown document points at
/// (`markdown.resolveReference`).
MarkdownResolveReference,
/// Editor reducers: workbench state, text edits, find, navigation and preferences (`editor.*`).
Editor(EditorCommand),
/// Applies one transition to the rearrangeable workbench chrome: stripe, title bar and status bar
Expand Down Expand Up @@ -1184,6 +1187,7 @@ impl CoreCommand {
"processLifecycle.inventory" => Some(Self::ProcessLifecycleInventory),
"processLifecycle.settle" => Some(Self::ProcessLifecycleSettle),
"markdown.render" => Some(Self::MarkdownRender),
"markdown.resolveReference" => Some(Self::MarkdownResolveReference),
"keymap.parseContext" => Some(Self::Keymap(KeymapCommand::ParseContext)),
"keymap.evaluateContext" => Some(Self::Keymap(KeymapCommand::EvaluateContext)),
"keymap.analyzeConflicts" => Some(Self::Keymap(KeymapCommand::AnalyzeConflicts)),
Expand Down
21 changes: 20 additions & 1 deletion rust/crates/core/src/runtime/dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
//! child module named after it; the remaining small families dispatch here.

use crate::project::{
self, DocumentLifecycleRequest, FileReadRequest, FileWriteRequest, MarkdownRenderRequest,
self, DocumentLifecycleRequest, FileReadRequest, FileWriteRequest, MarkdownReferenceRequest,
MarkdownRenderRequest,
};
use crate::protocol::{CoreCommand, CoreError, CoreRequest, CoreResponse, ErrorCode, GitCommand};
use lithe_github::{NormalizeResponseRequest, ParseRemoteRequest, RequestPlanRequest};
Expand Down Expand Up @@ -455,6 +456,24 @@ fn execute_parsed(mut parsed: CoreRequest, document: Option<&str>) -> CoreRespon
Err(error) => CoreResponse::failure(id, error),
}
}
CoreCommand::MarkdownResolveReference => {
match serde_json::from_value::<MarkdownReferenceRequest>(parsed.payload).map_err(
|error| {
CoreError::new(
ErrorCode::InvalidRequest,
"Invalid Markdown reference request",
)
.with_details(error.to_string())
},
) {
Ok(request) => CoreResponse::success(
id,
serde_json::to_value(crate::project::resolve_reference(request))
.expect("Markdown reference response should encode"),
),
Err(error) => CoreResponse::failure(id, error),
}
}
CoreCommand::Lsp(command) => lsp::dispatch(command, id, parsed.payload),
CoreCommand::NetworkProxyPolicy => {
match serde_json::from_value::<lithe_network::NetworkProxyPolicyRequest>(parsed.payload)
Expand Down
1 change: 1 addition & 0 deletions rust/crates/core/src/runtime/request_trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ pub(crate) fn is_hot(method: &str) -> bool {
"document.classifyText"
| "document.lifecycle"
| "markdown.render"
| "markdown.resolveReference"
| "workspace.classifyVisibility"
| "workspace.visibilityDefaults"
| "lsp.syncDocument"
Expand Down
39 changes: 39 additions & 0 deletions rust/crates/core/src/tests/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,45 @@ fn markdown_render_command_returns_sanitized_html() {
assert!(!html.contains("alert(1)"));
}

#[test]
fn markdown_resolve_reference_command_answers_links_and_resources() {
let resolve = |reference: &str, purpose: &str| -> Value {
let request = serde_json::json!({
"id": "markdown-reference",
"command": "markdown.resolveReference",
"payload": {
"documentPath": "/workspace/docs/guide.md",
"workspaceRoot": "/workspace",
"reference": reference,
"purpose": purpose
}
});
let response: Value = serde_json::from_str(&execute_json(
&serde_json::to_string(&request).expect("Markdown reference request should encode"),
))
.expect("Markdown reference response should be JSON");
assert_eq!(response["ok"], true, "{response}");
response["data"].clone()
};

assert_eq!(
resolve("https://example.com", "link"),
serde_json::json!({"kind": "external", "url": "https://example.com/"})
);
assert_eq!(
resolve("#usage", "link"),
serde_json::json!({"kind": "anchor", "anchor": "usage"})
);
assert_eq!(
resolve("//example.com/a.png", "resource"),
serde_json::json!({"kind": "remote", "url": "https://example.com/a.png"})
);
assert_eq!(
resolve("javascript:alert(1)", "link"),
serde_json::json!({"kind": "none"})
);
}

#[test]
fn search_matches_shared_fixture_semantics() {
let fixture = fixture();
Expand Down
2 changes: 2 additions & 0 deletions rust/crates/project/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub mod files;
mod history;
pub mod indexing;
mod markdown;
mod markdown_reference;
mod name_matching;
pub mod name_ranking;
pub mod recent_projects;
Expand All @@ -26,6 +27,7 @@ pub use document_lifecycle::*;
pub use files::*;
pub use history::*;
pub use markdown::*;
pub use markdown_reference::*;
pub use search_paging::{search_page, SearchPageRequest};

#[cfg(test)]
Expand Down
Loading
Loading