2026 03 08_simple_document_engine_target_spec - mark-ik/graphshell GitHub Wiki
Date: 2026-03-08 Status: Canonical contract Priority: Required before Gemini resolver, Reader Mode, or markdown pipeline implementation
Related docs:
-
2026-02-24_universal_content_model_plan.mdโ UCM Steps 11โ12 (where these types originate) -
universal_content_model_spec.mdโ Viewer selection policy; O4 tracked as open concern -
2026-03-08_servo_text_editor_architecture_plan.mdโ text-editor short-circuit (exempted from this pipeline) -
../../technical_architecture/2026-03-08_graphshell_core_extraction_plan.mdโ core/host split; these types are host-only
SimpleDocument and EngineTarget are the intermediate types for the Servo-first content adaptation pipeline (UCM Step 12). They allow Graphshell to transform non-HTML content (Gemini text, reader-mode extracted HTML, Markdown docs, safe previews) into a form that Servo can render, without modifying Servo itself.
These types are host-only. They are not WASM-clean and do not belong in graphshell-core. The pipeline that uses them runs in the desktop host.
This spec defines backend/render-target contracts, not a replacement taxonomy
for viewer identity. EngineTarget answers "how should this adapted content be
realized by the runtime?" It does not answer "what canonical viewer class is
this?" The Viewer domain remains the authority for viewer identity and
selection semantics.
This spec closes open concern O4 from ../../../archive_docs/checkpoint_2026-03-27/graphshell_docs/technical_architecture/ARCHITECTURAL_CONCERNS.md.
SimpleDocument is a format-agnostic block-structured intermediate model. It is the canonical output of any content source that targets the adaptation pipeline.
// crates/graphshell-desktop/src/viewer/adaptation/simple_document.rs
pub enum SimpleDocument {
Blocks(Vec<SimpleBlock>),
}
pub enum SimpleBlock {
Heading { level: u8, text: String },
Paragraph(String),
Link { text: String, href: String },
Quote(String),
CodeFence { lang: Option<String>, text: String },
List { ordered: bool, items: Vec<String> },
Rule,
}The following content sources produce a SimpleDocument:
| Source | How |
|---|---|
text/gemini content |
GeminiRenderer::parse() โ line-by-line format; exact mapping in ยง5 |
| HTTP Reader Mode extraction | DOM-to-blocks pass on Servo's extracted readable content |
text/markdown via local File node (read-only) |
pulldown-cmark โ block mapping |
| Safe preview of untrusted content | Sanitized subset; all blocks allowed except Link with non-https href |
Not a producer: viewer:text-editor. Editable text/* + File nodes short-circuit directly to editor-core and never enter the adaptation pipeline. See 2026-03-08_servo_text_editor_architecture_plan.md ยง9.
| Consumer | How |
|---|---|
EngineTarget::ServoHtml compiler |
Renders blocks as constrained HTML fed to Servo |
EngineTarget::NativeReader |
Renders blocks directly via egui layout (fallback, low-surface) |
-
SimpleDocumentnever contains raw HTML. Block types are enumerated and exhaustive. -
Link.hrefvalues are validated before entering the document. Non-https/gemini/fileschemes are either rejected or stored as plain text. -
CodeFence.langis an advisory syntax hint; renderers must not fail on unknown languages. -
SimpleDocumentis not persisted. It is an ephemeral pipeline artifact computed from source content on each load.
EngineTarget is the output of the adaptation pipeline โ the final render package bound to a specific rendering path.
// crates/graphshell-desktop/src/viewer/adaptation/engine_target.rs
pub enum EngineTarget {
ServoHtml {
html: String,
base_url: Option<String>,
content_security_policy: String,
policy: RenderPolicy,
/// User-supplied CSS injected into the compiled document's <style> block.
/// Appended after the Graphshell default stylesheet; user rules win on specificity.
/// None = use Graphshell default only.
user_stylesheet: Option<String>,
},
WryWebview {
source_url: String,
},
NativeReader {
doc: SimpleDocument,
policy: RenderPolicy,
/// Which visual theme profile to apply. Defaults to the source-appropriate preset.
/// Users may select a different profile via the node's Render With menu.
theme: NativeReaderTheme,
},
}
/// Visual theme for NativeReader rendering.
/// The default for Gemini content is `Gemini` (minimal, respects capsule author conventions).
/// The default for Reader Mode and Markdown is `Readable`.
pub enum NativeReaderTheme {
/// Minimal presentation aligned with Gemini community conventions:
/// monospace or serif body, generous line height, no decorative chrome.
Gemini,
/// Clean readable prose layout โ Reader Mode and Markdown default.
Readable,
/// User-defined profile stored in GraphshellProfile. Loaded by name at render time.
Custom(String),
}
pub struct RenderPolicy {
pub scripts_allowed: bool,
pub remote_subresources_allowed: bool,
pub storage_allowed: bool,
pub cookies_allowed: bool,
pub intercept_links: bool,
}The pipeline selects an EngineTarget based on the following ordered rules:
| Priority | Condition | Target |
|---|---|---|
| 1 | Edit-intent text node (address_kind = File, mime_hint in text/*, edit intent set) |
Short-circuit โ skip pipeline, route to viewer:text-editor
|
| 2 | Source is text/gemini
|
NativeReader with theme: NativeReaderTheme::Gemini โ default for Gemini
|
| 3 | Source can be compiled to valid constrained HTML (Reader Mode, Markdown, safe preview) |
ServoHtml with restrictive RenderPolicy
|
| 4 | Source is a raw web URL and Servo fails or user selects Wry |
WryWebview (compatibility fallback) |
| 5 | Source is SimpleDocument and Servo compilation fails or is unavailable |
NativeReader with theme: NativeReaderTheme::Readable (fallback) |
Gemini default is NativeReader. Gemini content is presentation-agnostic by protocol design; the Gemini community expects minimal, text-first rendering without web-engine overhead. NativeReader with the Gemini theme is the correct default. ServoHtml remains available as an explicit user choice via Render With.
ServoHtml is the preferred target for HTTP Reader Mode, Markdown, and safe previews. WryWebview is not a general target for SimpleDocument sources.
Interpretation:
-
EngineTargetis a backend choice underneath viewer policy. -
ServoHtmlandNativeReaderare peer realization paths for adapted rich-document content, each preferred for different source types. -
WryWebviewis the compatibility backend for eligible raw web content, not a canonical answer for arbitrary adapted document sources. - A user-facing
Render Withcommand exposes backend choice (NativeReader / ServoHtml) and, for ServoHtml, a CSS editor. These choices still sit under the canonical viewer taxonomy defined inVIEWER.md.
| Source | Default target | scripts_allowed |
remote_subresources_allowed |
storage_allowed |
cookies_allowed |
intercept_links |
|---|---|---|---|---|---|---|
| Gemini capsule |
NativeReader (Gemini theme) |
n/a | n/a | n/a | n/a | true |
| Gemini capsule (user-selected ServoHtml) | ServoHtml |
false | false | false | false | true |
| HTTP Reader Mode | ServoHtml |
false | false | false | false | true |
| Markdown doc | ServoHtml |
false | false | false | false | true |
| Safe preview / untrusted content | ServoHtml |
false | false | false | false | true |
| Raw web URL | WryWebview |
true | true | true | true | false |
NativeReader does not use RenderPolicy fields โ it has no script or subresource execution model. Link interception is always active: clicks emit GraphIntent::NavigateNode.
intercept_links = true means all link navigations are captured by Graphshell and emitted as GraphIntent::NavigateNode rather than being followed by the renderer. This is the default for all pipeline-compiled targets.
For ServoHtml targets, the content_security_policy field is generated from RenderPolicy:
-
scripts_allowed = falseโscript-src 'none' -
remote_subresources_allowed = falseโdefault-src 'none'; style-src 'unsafe-inline'(inline styles allowed for block rendering) - The CSP string is injected as a
<meta http-equiv="Content-Security-Policy">tag in the compiled HTML. It is not a response header (Servo's in-process HTML load does not use HTTP response headers for injected documents).
The adaptation pipeline runs in the host crate on the I/O task pool (not the frame thread).
ProtocolResolver
โ bytes + MIME
ContentClassifier (AddressKind + MIME โ source type)
โ source type
[Short-circuit check: edit-intent text/File โ viewer:text-editor]
โ if not short-circuited
SimpleDocumentProducer (source bytes โ SimpleDocument)
โ SimpleDocument
EngineTargetCompiler (SimpleDocument โ EngineTarget)
โ EngineTarget
ViewerRegistry (bind EngineTarget to viewer lifecycle)
Each stage is a pure function or async task. No stage mutates graph state directly. Side effects (e.g., setting mime_hint after detection) are emitted as GraphIntent values from the pipeline coordinator.
text/gemini line-type to SimpleBlock mapping:
| Gemini line prefix | SimpleBlock |
|---|---|
# |
Heading { level: 1, text } |
## |
Heading { level: 2, text } |
### |
Heading { level: 3, text } |
=> URL [label] |
Link { text: label or URL, href: URL } |
> |
Quote(text) |
* (list item) |
List { ordered: false, items } (consecutive * lines grouped) |
``` / ``` lang (toggle) |
CodeFence { lang, text } (content between toggles) |
(blank line or ---) |
Rule |
| (any other line) | Paragraph(text) |
Grouping rules: consecutive same-type lines that can be grouped (list items, paragraph text) are merged before producing the block sequence. This is a producer-side responsibility.
SimpleDocument โ ServoHtml.html compilation rules:
- Output is a minimal
<!DOCTYPE html><html><head>...</head><body>...</body></html>document. - The
<head>contains the CSP<meta>tag and a minimal stylesheet for block layout (inline<style>; no external stylesheet). - Block type to HTML element mapping:
-
Heading { level, text }โ<h1>โ<h3> -
Paragraph(text)โ<p> -
Link { text, href }โ<a href="...">โ href is validated against allowed schemes before emission -
Quote(text)โ<blockquote><p> -
CodeFence { lang, text }โ<pre><code class="language-{lang}"> -
List { ordered: false, items }โ<ul><li>per item -
List { ordered: true, items }โ<ol><li>per item -
Ruleโ<hr>
-
- All text content is HTML-escaped (
<,>,&,") before insertion. -
hrefvalues: onlyhttps://,gemini://,file://, and relative paths are passed through. Other schemes are replaced withabout:blank.
NativeReader renders SimpleDocument blocks directly via egui without Servo. It is the default path for Gemini and the fallback for other adapted sources.
- Implemented in the same module as
PlaintextViewer, sharing block layout utilities. -
Headingblocks use eguiRichTextwith scaled font size. -
Linkblocks render as underlined text; click emitsGraphIntent::NavigateNode. -
CodeFenceblocks render inside aScrollAreawith monospace font and optional syntax hint label. -
NativeReaderdoes not support images, tables, or inline HTML.SimpleDocumentcannot produce these by construction.
Gemini theme โ minimal, in line with Gemini community conventions:
- Serif or monospace body font (user-configurable in
GraphshellProfile) - No decorative chrome, no borders around block elements
- Links rendered inline in body flow, not as a separate link list
- Generous line height (โฅ 1.6)
- No images rendered (Gemini has no inline image syntax; external image links remain as clickable links)
- Color scheme respects the app's active egui theme (light/dark)
Readable theme โ clean prose layout for Reader Mode and Markdown:
- Sans-serif body font
- Slightly narrower max-width column (readable line length)
- Subtle heading rules
Custom(name) theme โ user-defined profile stored in GraphshellProfile under native_reader_themes. Loaded by name at render time; falls back to Readable if not found.
Users can create, edit, and name custom themes via the node's Render With panel. Theme definitions are stored in the user profile, not per-node โ they are reusable across all NativeReader nodes.
When EngineTarget::ServoHtml is selected (either as default for non-Gemini sources, or as a user-selected override for Gemini), the compiled document's <style> block includes:
- Graphshell base stylesheet โ minimal block layout, font defaults, link color. Kept deliberately sparse.
-
Source-appropriate preset โ a small per-source-type stylesheet layer (e.g., Gemini preset applies the same minimal conventions as the
GeminiNativeReader theme, translated to CSS). -
User stylesheet (
user_stylesheet: Option<String>) โ appended last; user rules win via specificity. Empty by default.
The source-appropriate preset for Gemini (ServoHtml override case):
-
font-family: serifbody -
max-width: 70ch; margin: 0 autofor line length -
acolor matches the active egui link color - No
background-image, nobox-shadow, no decorative borders
CSS editor surface: accessible via the node's Render With panel โ Customize stylesheet. Edits are stored in GraphshellProfile keyed by (source_type, profile_name). The user can save named CSS profiles and switch between them, following the same pattern as NativeReaderTheme::Custom.
The following features depend on this spec being implemented before they can proceed:
| Feature | Dependency |
|---|---|
Gemini resolver (viewer:gemini, UCM Step 10) |
SimpleDocument Gemini producer (ยง5) |
HTTP Reader Mode (viewer.profile.reader_toggle) |
SimpleDocument reader-mode producer + ServoHtml compiler |
| Markdown docs pipeline |
SimpleDocument Markdown producer + ServoHtml or NativeReader compiler |
| Safe content preview (untrusted nodes) |
SimpleDocument sanitized producer + restrictive RenderPolicy
|
- A
gemini://URL resolves to aSimpleDocumentand renders viaEngineTarget::NativeReaderwiththeme: Geminiby default. No Servo process is started for a default Gemini load. - The
Render Withpanel on a Gemini node offers NativeReader (Gemini) and ServoHtml as backend choices. Selecting ServoHtml renders the capsule via Servo with the Gemini CSS preset and scripts/remote resources blocked. - HTTP Reader Mode extracts a readable
SimpleDocumentfrom anhttp://orhttps://page and renders viaEngineTarget::ServoHtmlwithout scripts or tracking. - A local
text/markdownfile opened read-only renders viaServoHtml(preferred) orNativeReader(fallback if Servo unavailable). -
viewer:text-editoris never selected by the pipeline for read-only opens. The short-circuit check (ยง3.1 priority 1) only fires on edit-intent opens. - All
Link.hrefvalues in compiled HTML are validated; non-https/gemini/fileschemes do not appear in the rendered output. - The pipeline does not block the frame thread; it runs on the I/O task pool with a
GraphIntent::UpdateNodeMimeHintemitted on classification completion. - Switching from
NativeReadertoServoHtml(or vice versa) viaRender Withdoes not recreate the sourceSimpleDocument; the already-computed document is re-compiled to the new target. - A user can open Customize stylesheet on a
ServoHtmlnode, enter CSS, save it as a named profile, and have it persist across sessions and apply to subsequent loads of the same source type. -
NativeReaderTheme::Customprofiles are stored inGraphshellProfileand survive app restart.