atproto blogging
1use crate::NotebookContext;
2
3use super::*;
4use std::path::PathBuf;
5use weaver_common::jacquard::client::{
6 AtpSession, MemorySessionStore,
7 credential_session::{CredentialSession, SessionKey},
8};
9
10/// Type alias for the session used in tests
11type TestSession = CredentialSession<
12 MemorySessionStore<SessionKey, AtpSession>,
13 weaver_common::jacquard::identity::JacquardResolver,
14>;
15
16/// Helper: Create test context without network capabilities
17fn test_context() -> StaticSiteContext<TestSession> {
18 let root = PathBuf::from("/tmp/test");
19 let destination = PathBuf::from("/tmp/output");
20 let mut ctx = StaticSiteContext::new(root, destination, None);
21 ctx.client = None; // Explicitly disable network
22 ctx
23}
24
25/// Helper: Render markdown to HTML using test context
26async fn render_markdown(input: &str) -> String {
27 let context = test_context();
28 export_page(input, context).await.unwrap()
29}
30
31#[tokio::test]
32async fn test_smoke() {
33 let output = render_markdown("Hello world").await;
34 assert!(output.contains("Hello world"));
35}
36
37#[tokio::test]
38async fn test_paragraph_rendering() {
39 let input = "This is a paragraph.\n\nThis is another paragraph.";
40 let output = render_markdown(input).await;
41 insta::assert_snapshot!(output);
42}
43
44#[tokio::test]
45async fn test_heading_rendering() {
46 let input = "# Heading 1\n\n## Heading 2\n\n### Heading 3";
47 let output = render_markdown(input).await;
48 insta::assert_snapshot!(output);
49}
50
51#[tokio::test]
52async fn test_list_rendering() {
53 let input = "- Item 1\n- Item 2\n - Nested\n\n1. Ordered 1\n2. Ordered 2";
54 let output = render_markdown(input).await;
55 insta::assert_snapshot!(output);
56}
57
58#[tokio::test]
59async fn test_code_block_rendering() {
60 let input = "```rust\nfn main() {\n println!(\"Hello\");\n}\n```";
61 let output = render_markdown(input).await;
62 insta::assert_snapshot!(output);
63}
64
65#[tokio::test]
66async fn test_table_rendering() {
67 let input = "| Left | Center | Right |\n|:-----|:------:|------:|\n| A | B | C |";
68 let output = render_markdown(input).await;
69 insta::assert_snapshot!(output);
70}
71
72#[tokio::test]
73async fn test_blockquote_rendering() {
74 let input = "> This is a quote\n>\n> With multiple lines";
75 let output = render_markdown(input).await;
76 insta::assert_snapshot!(output);
77}
78
79#[tokio::test]
80async fn test_math_rendering() {
81 let input = "Inline $x^2$ and display:\n\n$$\ny = mx + b\n$$";
82 let output = render_markdown(input).await;
83 insta::assert_snapshot!(output);
84}
85
86#[tokio::test]
87async fn test_wikilink_resolution() {
88 let vault_contents = vec![
89 PathBuf::from("notes/First Note.md"),
90 PathBuf::from("notes/Second Note.md"),
91 ];
92
93 let mut context = test_context();
94 context.dir_contents = Some(vault_contents.into());
95
96 let input = "[[First Note]] and [[Second Note]]";
97 let output = export_page(input, context).await.unwrap();
98 println!("{output}");
99 assert!(output.contains("./First%20Note.html"));
100 assert!(output.contains("./Second%20Note.html"));
101}
102
103#[tokio::test]
104async fn test_broken_wikilink() {
105 let vault_contents = vec![PathBuf::from("notes/Exists.md")];
106
107 let mut context = test_context();
108 context.dir_contents = Some(vault_contents.into());
109
110 let input = "[[Does Not Exist]]";
111 let output = export_page(input, context).await.unwrap();
112
113 // Broken wikilinks become links (they just don't point anywhere valid)
114 // This is acceptable - static site will show 404 on click
115 assert!(output.contains("<a href="));
116 assert!(output.contains("Does Not Exist</a>") || output.contains("Does%20Not%20Exist"));
117}
118
119#[tokio::test]
120async fn test_wikilink_with_section() {
121 let vault_contents = vec![PathBuf::from("Note.md")];
122
123 let mut context = test_context();
124 context.dir_contents = Some(vault_contents.into());
125
126 let input = "[[Note#Section]]";
127 let output = export_page(input, context).await.unwrap();
128 println!("{output}");
129 assert!(output.contains("Note#Section"));
130}
131
132#[tokio::test]
133async fn test_link_flattening_enabled() {
134 let mut context = test_context();
135 context.options = StaticSiteOptions::FLATTEN_STRUCTURE;
136
137 let input = "[Link](path/to/nested/file.md)";
138 let output = export_page(input, context).await.unwrap();
139 println!("{output}");
140 // Should flatten to single parent directory
141 assert!(output.contains("./entry/file.html"));
142}
143
144#[tokio::test]
145async fn test_link_flattening_disabled() {
146 let mut context = test_context();
147 context.options = StaticSiteOptions::empty();
148
149 let input = "[Link](path/to/nested/file.md)";
150 let output = export_page(input, context).await.unwrap();
151 println!("{output}");
152 // Should preserve original path
153 assert!(output.contains("path/to/nested/file.html"));
154}
155
156#[tokio::test]
157async fn test_frontmatter_parsing() {
158 let input = "---\ntitle: Test Page\nauthor: Test Author\n---\n\nContent here";
159 let context = test_context();
160 let output = export_page(input, context.clone()).await.unwrap();
161
162 // Frontmatter should be parsed but not rendered
163 assert!(!output.contains("title: Test Page"));
164 assert!(output.contains("Content here"));
165
166 // Verify frontmatter was captured
167 let frontmatter = context.frontmatter();
168 let yaml = frontmatter.contents();
169 let yaml_guard = yaml.read().unwrap();
170 assert!(yaml_guard.len() > 0);
171}
172
173#[tokio::test]
174async fn test_empty_frontmatter() {
175 let input = "---\n---\n\nContent";
176 let output = render_markdown(input).await;
177
178 assert!(output.contains("Content"));
179 assert!(!output.contains("---"));
180}
181
182#[tokio::test]
183async fn test_empty_input() {
184 let output = render_markdown("").await;
185 assert_eq!(output, "");
186}
187
188#[tokio::test]
189async fn test_html_and_special_characters() {
190 // Test that markdown correctly handles HTML and special chars per CommonMark spec
191 let input =
192 "Text with <special> & some text. Valid tags: <em>emphasis</em> and <strong>bold</strong>";
193 let output = render_markdown(input).await;
194
195 // & must be escaped for valid HTML
196 assert!(output.contains("&"));
197
198 // Inline HTML tags pass through (CommonMark behavior)
199 assert!(output.contains("<special>"));
200 assert!(output.contains("<em>emphasis</em>"));
201 assert!(output.contains("<strong>bold</strong>"));
202}
203
204#[tokio::test]
205async fn test_unicode_content() {
206 let input = "Unicode: 你好 🎉 café";
207 let output = render_markdown(input).await;
208
209 assert!(output.contains("你好"));
210 assert!(output.contains("🎉"));
211 assert!(output.contains("café"));
212}
213
214// =============================================================================
215// WeaverBlock Prefix Tests
216// =============================================================================
217
218#[tokio::test]
219async fn test_weaver_block_aside_class() {
220 let input = "\n\n{.aside}\nThis paragraph should be in an aside.";
221 let output = render_markdown(input).await;
222 insta::assert_snapshot!(output);
223}
224
225#[tokio::test]
226async fn test_weaver_block_custom_class() {
227 let input = "\n\n{.highlight}\nThis paragraph has a custom class.";
228 let output = render_markdown(input).await;
229 insta::assert_snapshot!(output);
230}
231
232#[tokio::test]
233async fn test_weaver_block_custom_attributes() {
234 let input = "\n\n{.foo, width: 300px, data-test: value}\nParagraph with class and attributes.";
235 let output = render_markdown(input).await;
236 insta::assert_snapshot!(output);
237}
238
239#[tokio::test]
240async fn test_weaver_block_before_heading() {
241 let input = "\n\n{.aside}\n## Heading in aside\n\nParagraph also in aside.";
242 let output = render_markdown(input).await;
243 insta::assert_snapshot!(output);
244}
245
246#[tokio::test]
247async fn test_weaver_block_before_blockquote() {
248 let input = "\n\n{.aside}\n\n> This blockquote is in an aside.";
249 let output = render_markdown(input).await;
250 insta::assert_snapshot!(output);
251}
252
253#[tokio::test]
254async fn test_weaver_block_before_list() {
255 let input = "\n\n{.aside}\n\n- Item 1\n- Item 2";
256 let output = render_markdown(input).await;
257 insta::assert_snapshot!(output);
258}
259
260#[tokio::test]
261async fn test_weaver_block_before_code_block() {
262 let input = "\n\n{.aside}\n\n```rust\nfn main() {}\n```";
263 let output = render_markdown(input).await;
264 insta::assert_snapshot!(output);
265}
266
267#[tokio::test]
268async fn test_weaver_block_multiple_classes() {
269 let input = "\n\n{.aside, .highlight, .important}\nMultiple classes applied.";
270 let output = render_markdown(input).await;
271 insta::assert_snapshot!(output);
272}
273
274#[tokio::test]
275async fn test_weaver_block_no_effect_on_following() {
276 let input = "\n\n{.aside}\nFirst paragraph in aside.\n\nSecond paragraph NOT in aside.";
277 let output = render_markdown(input).await;
278 insta::assert_snapshot!(output);
279}
280
281// =============================================================================
282// Footnote / Sidenote Tests
283// =============================================================================
284
285#[tokio::test]
286async fn test_footnote_traditional() {
287 let input = "Here is some text[^1].\n[^1]: This is the footnote definition.";
288 let output = render_markdown(input).await;
289 insta::assert_snapshot!(output);
290}
291
292#[tokio::test]
293async fn test_footnote_sidenote_inline() {
294 // When definition immediately follows reference in the same paragraph flow
295 let input = "Here is text[^note]\n[^note]: Sidenote content.";
296 let output = render_markdown(input).await;
297 insta::assert_snapshot!(output);
298}
299
300#[tokio::test]
301async fn test_footnote_multiple() {
302 let input = "First[^1] and second[^2] footnotes.\n[^1]: First note.\n[^2]: Second note.";
303 let output = render_markdown(input).await;
304 insta::assert_snapshot!(output);
305}
306
307#[tokio::test]
308async fn test_footnote_with_inline_formatting() {
309 let input = "Text with footnote[^fmt].\n[^fmt]: Note with **bold** and *italic*.";
310 let output = render_markdown(input).await;
311 insta::assert_snapshot!(output);
312}
313
314#[tokio::test]
315async fn test_footnote_named() {
316 let input = "Reference[^my-note].\n[^my-note]: Named footnote content.";
317 let output = render_markdown(input).await;
318 insta::assert_snapshot!(output);
319}
320
321#[tokio::test]
322async fn test_footnote_in_blockquote() {
323 let input = "> Quote with footnote[^q].\n[^q]: Footnote for quote.";
324 let output = render_markdown(input).await;
325 insta::assert_snapshot!(output);
326}
327
328// =============================================================================
329// Combined WeaverBlock + Footnote Tests
330// =============================================================================
331
332#[tokio::test]
333async fn test_weaver_block_with_footnote() {
334 let input = "{.aside}\nAside with a footnote[^aside].\n\n[^aside]: Footnote in aside context.";
335 let output = render_markdown(input).await;
336 insta::assert_snapshot!(output);
337}