···46 pub is_read: bool,
47 #[serde(skip_serializing_if = "core::option::Option::is_none")]
48 pub labels: core::option::Option<Vec<crate::com::atproto::label::defs::Label>>,
49+ ///The reason why this notification was delivered - e.g. your post was liked, or you received a new follower.
50 pub reason: String,
51 #[serde(skip_serializing_if = "core::option::Option::is_none")]
52 pub reason_subject: core::option::Option<String>,
···1150 _ => Err(atrium_xrpc::Error::UnexpectedResponseType),
1151 }
1152 }
1153+ ///Find posts matching search criteria, returning views of those posts. Note that this API endpoint may require authentication (eg, not public) for some service providers and implementations.
1154 pub async fn search_posts(
1155 &self,
1156 params: crate::app::bsky::feed::search_posts::Parameters,
···1// @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT.
2//!Definitions for the `sh.weaver.actor.defs` namespace.
000000000003#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
4#[serde(rename_all = "camelCase")]
5pub struct ProfileDataViewData {
···1// @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT.
2//!Definitions for the `sh.weaver.actor.defs` namespace.
3+///A single author in a Weaver notebook.
4+#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
5+#[serde(rename_all = "camelCase")]
6+pub struct AuthorData {
7+ pub did: atrium_api::types::string::Did,
8+ #[serde(skip_serializing_if = "core::option::Option::is_none")]
9+ pub display_name: core::option::Option<String>,
10+ #[serde(skip_serializing_if = "core::option::Option::is_none")]
11+ pub handle: core::option::Option<atrium_api::types::string::Handle>,
12+}
13+pub type Author = atrium_api::types::Object<AuthorData>;
14#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
15#[serde(rename_all = "camelCase")]
16pub struct ProfileDataViewData {
···2//!Definitions for the `sh.weaver.notebook` namespace.
3pub mod authors;
4pub mod book;
05pub mod defs;
6pub mod entry;
7#[derive(Debug)]
···15impl atrium_api::types::Collection for Book {
16 const NSID: &'static str = "sh.weaver.notebook.book";
17 type Record = book::Record;
00000018}
19#[derive(Debug)]
20pub struct Entry;
···2//!Definitions for the `sh.weaver.notebook` namespace.
3pub mod authors;
4pub mod book;
5+pub mod chapter;
6pub mod defs;
7pub mod entry;
8#[derive(Debug)]
···16impl atrium_api::types::Collection for Book {
17 const NSID: &'static str = "sh.weaver.notebook.book";
18 type Record = book::Record;
19+}
20+#[derive(Debug)]
21+pub struct Chapter;
22+impl atrium_api::types::Collection for Chapter {
23+ const NSID: &'static str = "sh.weaver.notebook.chapter";
24+ type Record = chapter::Record;
25}
26#[derive(Debug)]
27pub struct Entry;
···30 pub prev: core::option::Option<BookEntryRef>,
31}
32pub type BookEntryView = atrium_api::types::Object<BookEntryViewData>;
33+///The format of the content. This is used to determine how to render the content.
34+#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
35+#[serde(rename_all = "camelCase")]
36+pub struct ContentFormatData {
37+ ///The format of the content. This is used to determine how to render the content.
38+ #[serde(skip_serializing_if = "core::option::Option::is_none")]
39+ pub markdown: core::option::Option<String>,
40+}
41+pub type ContentFormat = atrium_api::types::Object<ContentFormatData>;
42#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
43#[serde(rename_all = "camelCase")]
44pub struct EntryViewData {
···1+// @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT.
2+//!Definitions for the `sh.weaver.publish` namespace.
3+pub mod blob;
4+pub mod defs;
5+#[derive(Debug)]
6+pub struct Blob;
7+impl atrium_api::types::Collection for Blob {
8+ const NSID: &'static str = "sh.weaver.publish.blob";
9+ type Record = blob::Record;
10+}
···1+// @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT.
2+//!Definitions for the `sh.weaver.publish.defs` namespace.
+85-1
crates/weaver-common/src/lib.rs
···8pub mod oauth;
9pub mod resolver;
10pub mod xrpc_server;
11-use atrium_api::types::{BlobRef, TypedBlobRef, string::Did};
0012pub use lexicons::*;
001314pub use crate::error::{Error, IoError, ParseError, SerDeError};
15···56 mime_type.strip_prefix("image/").unwrap_or(mime_type)
57 )
58}
00000000000000000000000000000000000000000000000000000000000000000000000000000000
···8pub mod oauth;
9pub mod resolver;
10pub mod xrpc_server;
11+use std::sync::OnceLock;
12+13+pub use atrium_api::types::*;
14pub use lexicons::*;
15+use regex::Regex;
16+use string::Did;
1718pub use crate::error::{Error, IoError, ParseError, SerDeError};
19···60 mime_type.strip_prefix("image/").unwrap_or(mime_type)
61 )
62}
63+64+pub fn blob_url(did: &Did, pds: &str, blob_ref: &BlobRef) -> String {
65+ let cid = match blob_ref {
66+ BlobRef::Typed(TypedBlobRef::Blob(b)) => atrium_api::types::string::Cid::new(b.r#ref.0)
67+ .as_ref()
68+ .to_string(),
69+70+ BlobRef::Untyped(r) => r.cid.clone(),
71+ };
72+ format!(
73+ "https://{}/xrpc/com.atproto.repo.getBlob?did={}&cid={}",
74+ pds,
75+ did.as_str(),
76+ cid,
77+ )
78+}
79+80+pub fn match_identifier(maybe_identifier: &str) -> Option<&str> {
81+ static RE_HANDLE: OnceLock<Regex> = OnceLock::new();
82+ static RE_DID: OnceLock<Regex> = OnceLock::new();
83+ if maybe_identifier.len() > 253 {
84+ None
85+ } else if !RE_DID.get_or_init(|| Regex::new(r"^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$").unwrap())
86+ .is_match(&maybe_identifier) && !RE_HANDLE
87+ .get_or_init(|| Regex::new(r"^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$").unwrap())
88+ .is_match(&maybe_identifier)
89+ {
90+ None
91+ } else {
92+ Some(maybe_identifier)
93+ }
94+}
95+96+pub fn match_nsid(maybe_nsid: &str) -> Option<&str> {
97+ static RE_NSID: OnceLock<Regex> = OnceLock::new();
98+ if maybe_nsid.len() > 317 {
99+ None
100+ } else if !RE_NSID
101+ .get_or_init(|| Regex::new(r"^[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(\.[a-zA-Z]([a-zA-Z0-9]{0,62}[a-zA-Z0-9])?)$").unwrap())
102+ .is_match(&maybe_nsid)
103+ {
104+ None
105+ } else {
106+ Some(maybe_nsid)
107+ }
108+}
109+110+/// Convert an ATURI to a HTTP URL
111+/// Currently has some failure modes and should restrict the NSIDs to a known subset
112+pub fn aturi_to_http<'s>(aturi: &'s str, appview: &'s str) -> Option<markdown_weaver::CowStr<'s>> {
113+ use markdown_weaver::CowStr;
114+115+ if aturi.starts_with("at://") {
116+ let rest = aturi.strip_prefix("at:://").unwrap();
117+ let mut split = rest.splitn(2, '/');
118+ let maybe_identifier = split.next()?;
119+ let maybe_nsid = split.next()?;
120+ let maybe_rkey = split.next()?;
121+122+ // https://atproto.com/specs/handle#handle-identifier-syntax
123+ let identifier = match_identifier(maybe_identifier)?;
124+125+ let nsid = if let Some(nsid) = match_nsid(maybe_nsid) {
126+ // Last part of the nsid is generally the middle component of the URL
127+ // TODO: check for bsky ones specifically, because those are the ones where this is valid
128+ nsid.rsplitn(1, '.').next()?
129+ } else {
130+ return None;
131+ };
132+ Some(CowStr::Boxed(
133+ format!(
134+ "https://{}/profile/{}/{}/{}",
135+ appview, identifier, nsid, maybe_rkey
136+ )
137+ .into_boxed_str(),
138+ ))
139+ } else {
140+ Some(CowStr::Borrowed(aturi))
141+ }
142+}
···1+//! Atproto renderer
2+//!
3+//! This mode of the renderer renders either an entire notebook or entries in it to files suitable for inclusion
4+//! in a single-page app and uploads them to your Atproto PDS
5+//! It can be accessed via the appview at {your-handle}.weaver.sh/{notebook-name}.
6+//!
7+//! It can also be edited there.
8+//!
9+//! Link altering logic:
10+//! - Option 1: leave (non-embed) links the same as in the markdown, have the CSM deal with them via some means
11+//! such as adding "data-did" and "data-cid" attributes to the `<a/>` tag containing the DID and CID
12+//! Pushes toward having the SPA/Appview do a bit more, but makes this step MUCH simpler
13+//! - In this scenario, the rendering step can happen upon access (and then be cached) in the appview
14+//! - More flexible in some ways, less in others
15+//! - Option 2: alter links to point to other rendered blobs. Requires a certain amount of work to handle
16+//! scenarios with a complex mesh of internal links, as the CID is altered by the editing of the link.
17+//! Such cycles are handled in the simplest way, by rendering an absolute url which will make a call to the appview.
18+//!
···1+use markdown_weaver_escape::StrWrite;
2+// use syntect::highlighting::ThemeSet;
3+// use syntect::html::css_for_theme_with_class_style;
4+use syntect::html::{ClassStyle, ClassedHTMLGenerator};
5+use syntect::parsing::SyntaxSet;
6+use syntect::util::LinesWithEndings;
7+8+/// Perform syntax highlighting on a code block.
9+/// This requires an external stylesheet, also generated by syntect to be loaded by the page.
10+/// The syntect SyntaxSet is also provided, so that it is not re-created on every call.
11+pub fn highlight<M>(
12+ syn_set: SyntaxSet,
13+ lang: Option<&str>,
14+ code: impl AsRef<str>,
15+ writer: &mut M,
16+) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>>
17+where
18+ M: StrWrite,
19+ <M as StrWrite>::Error: std::error::Error + Send + Sync + 'static,
20+{
21+ let lang_syn = if let Some(lang) = lang {
22+ syn_set
23+ .find_syntax_by_token(lang)
24+ .unwrap_or_else(|| syn_set.find_syntax_plain_text())
25+ } else {
26+ syn_set
27+ .find_syntax_by_first_line(code.as_ref())
28+ .unwrap_or_else(|| syn_set.find_syntax_plain_text())
29+ };
30+ writer.write_str("<pre><code class=\"language-")?;
31+ writer.write_str(&lang_syn.name)?;
32+ writer.write_str("\">")?;
33+34+ let mut html_gen = ClassedHTMLGenerator::new_with_class_style(
35+ lang_syn,
36+ &syn_set,
37+ ClassStyle::SpacedPrefixed { prefix: CSS_PREFIX },
38+ );
39+ for line in LinesWithEndings::from(code.as_ref()) {
40+ html_gen
41+ .parse_html_for_line_which_includes_newline(line)
42+ .unwrap();
43+ }
44+ writer.write_str(&html_gen.finalize())?;
45+ writer.write_str("</code></pre>")?;
46+ Ok(())
47+}
48+49+pub const CSS_PREFIX: &str = "wvrcode-";
+323
crates/weaver-renderer/src/lib.rs
···3//! This crate works with the weaver-markdown crate to render and optionally upload markdown notebooks to your Atproto PDS.
4//!
500000000000000000000000006pub mod types;
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
···112 },
113 "maxLength": 10,
114 "description": "An array of tags associated with the notebook entry. Tags can help categorize and organize entries."
000000000000115 }
116 }
117}
···112 },
113 "maxLength": 10,
114 "description": "An array of tags associated with the notebook entry. Tags can help categorize and organize entries."
115+ },
116+ "contentFormat": {
117+ "type": "object",
118+ "description": "The format of the content. This is used to determine how to render the content.",
119+ "properties": {
120+ "markdown": {
121+ "type": "string",
122+ "description": "The format of the content. This is used to determine how to render the content.",
123+ "enum": ["commonmark", "gfm", "obsidian", "weaver"],
124+ "default": "weaver"
125+ }
126+ }
127 }
128 }
129}
+36
lexicons/sh/weaver/notebook/page.json
···000000000000000000000000000000000000
···1+{
2+ "lexicon": 1,
3+ "id": "sh.weaver.notebook.chapter",
4+ "defs": {
5+ "main": {
6+ "type": "record",
7+ "description": "A grouping of entries in a notebook, intended to be displayed as a single page.",
8+ "key": "tid",
9+ "record": {
10+ "type": "object",
11+ "required": ["notebook", "authors", "entryList"],
12+ "properties": {
13+ "title": { "type": "ref", "ref": "sh.weaver.notebook.defs#title" },
14+ "tags": { "type": "ref", "ref": "sh.weaver.notebook.defs#tags" },
15+ "notebook": {
16+ "type": "ref",
17+ "ref": "com.atproto.repo.strongRef",
18+ "description": "The notebook this page belongs to."
19+ },
20+ "entryList": {
21+ "type": "array",
22+ "items": {
23+ "type": "ref",
24+ "ref": "com.atproto.repo.strongRef"
25+ }
26+ },
27+ "createdAt": {
28+ "type": "string",
29+ "format": "datetime",
30+ "description": "Client-declared timestamp when this was originally created."
31+ }
32+ }
33+ }
34+ }
35+ }
36+}