engine_observables_api/semantic.rs
1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! Common semantic facts every lane that has a document publishes —
6//! plus engine-specific extensions for richer protocol-native shape.
7//!
8//! Hekate's indexing / search / preview pipeline consumes
9//! [`SemanticQuery`] only — uniform across lanes. Apparatus's
10//! inspector pane can downcast to a lane-specific extension when
11//! present (e.g., `if let Some(html) = doc.as_html_ext()`).
12//!
13//! Cf. Hekate doc §"Semantic Plane". Per Mark's correction: don't
14//! force one fake tree model across lanes — use common-minimum +
15//! engine-specific extensions instead.
16
17use malloc_size_of_derive::MallocSizeOf;
18use serde::{Deserialize, Serialize};
19
20use crate::types::{Lang, SourceNodeId, SourceRange};
21
22/// Common-minimum semantic queries every lane with a document
23/// publishes. The trait is generic over `NodeId` so consumers that
24/// hold a concrete impl can index without boxing; engine-agnostic
25/// dispatch (Hekate) reads `SourceNodeId` via the inferred
26/// `as_source_id` helper each impl defines.
27pub trait SemanticQuery {
28 type NodeId: Copy + Eq + std::hash::Hash;
29
30 /// Document title (HTML `<title>`, Atom `<title>`, RSS channel
31 /// title, Gemini frontmatter title, Scroll frontmatter, etc.).
32 fn title(&self) -> Option<&str>;
33
34 /// Document language (HTML `lang` attribute, Atom `xml:lang`,
35 /// etc.). BCP 47 tag.
36 fn language(&self) -> Option<&Lang>;
37
38 /// Heading hierarchy. Lane impls pick their own ordering — most
39 /// will iterate in document order.
40 fn headings<'a>(&'a self) -> Box<dyn Iterator<Item = HeadingInfo> + 'a>;
41
42 /// Outbound links.
43 fn links<'a>(&'a self) -> Box<dyn Iterator<Item = LinkInfo> + 'a>;
44
45 /// Named anchors (`<a name>`, `#fragment`-target headings, etc.).
46 fn anchors<'a>(&'a self) -> Box<dyn Iterator<Item = AnchorInfo> + 'a>;
47
48 /// Nodes matching a generic semantic role (e.g., `Main`,
49 /// `Navigation`). Useful for reader-mode extraction.
50 fn nodes_by_role<'a>(
51 &'a self,
52 role: SemanticRole,
53 ) -> Box<dyn Iterator<Item = Self::NodeId> + 'a>;
54
55 /// Text content of a node, if it has any (text leaf, headings,
56 /// link text).
57 fn text_range(&self, node: Self::NodeId) -> Option<&str>;
58
59 /// Source-byte range a node corresponds to. Lanes that don't
60 /// track source spans return `None`.
61 fn source_range(&self, node: Self::NodeId) -> Option<SourceRange>;
62}
63
64/// One heading and its level.
65#[derive(Clone, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)]
66pub struct HeadingInfo {
67 pub source_node: SourceNodeId,
68 /// 1..=6 for HTML, lane-specific for others (Markdown allows
69 /// 1..=6 too; Scroll's headings flatten; Gemini has one level).
70 pub level: u8,
71 pub text: String,
72}
73
74/// One outbound link.
75#[derive(Clone, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)]
76pub struct LinkInfo {
77 pub source_node: SourceNodeId,
78 pub href: String,
79 /// Visible link text (may be empty for image links).
80 pub text: String,
81}
82
83/// One named anchor (target of `#fragment` navigation).
84#[derive(Clone, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)]
85pub struct AnchorInfo {
86 pub source_node: SourceNodeId,
87 pub name: String,
88}
89
90/// Generic semantic role consumers can ask about, independent of any
91/// specific markup language. Map onto each lane's native vocabulary.
92#[derive(
93 Clone, Copy, Debug, Default, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize,
94)]
95pub enum SemanticRole {
96 Main,
97 Navigation,
98 Article,
99 Section,
100 Aside,
101 Header,
102 Footer,
103 Heading,
104 Paragraph,
105 List,
106 ListItem,
107 Quote,
108 Code,
109 Form,
110 #[default]
111 Generic,
112}