1pub mod issue;
2pub mod local;
3pub mod sink;
4
5pub mod current_user {
6 use std::cell::RefCell;
7
8 thread_local! {
9 static CURRENT_USER: RefCell<Option<String>> = const { RefCell::new(None) };
10 }
11
12 pub fn set(user: String) {
15 CURRENT_USER.with(|u| *u.borrow_mut() = Some(user));
16 }
17
18 pub fn get() -> Option<String> {
21 CURRENT_USER.with(|u| u.borrow().clone())
22 }
23
24 pub fn is(user: &str) -> bool {
26 CURRENT_USER.with(|u| u.borrow().as_deref() == Some(user))
27 }
28}
29
30pub use issue::{
32 Ancestry, BlockerItem, BlockerSequence, CloseState, Comment, CommentIdentity, DisplayFormat, Events, FetchedIssue, HeaderLevel, Issue, IssueContents, IssueIdentity, IssueIndex,
33 IssueLink, IssueSelector, IssueTimestamps, LazyIssue, Line, LinkedIssueMeta, MAX_LINEAGE_DEPTH, Marker, OwnedCodeBlockKind, OwnedEvent, OwnedTag, OwnedTagEnd, ParseError, RepoInfo,
34 classify_line, is_blockers_marker, join_with_blockers, normalize_issue_indentation, split_blockers,
35};
36
37#[derive(Clone, Debug, Eq, PartialEq)]
41pub struct Header {
42 pub level: usize,
43 pub content: String,
44}
45
46impl Header {
47 pub fn new(level: usize, content: impl Into<String>) -> Self {
50 debug_assert!(level >= 1, "Header level must be >= 1");
51 Self {
52 level: level.max(1),
53 content: content.into(),
54 }
55 }
56
57 pub fn decode(s: &str) -> Option<Self> {
60 let trimmed = s.trim();
61
62 if !trimmed.starts_with('#') {
64 return None;
65 }
66 let mut level = 0;
67 for ch in trimmed.chars() {
68 if ch == '#' {
69 level += 1;
70 } else {
71 break;
72 }
73 }
74 if level > 0 && trimmed.len() > level {
76 let rest = &trimmed[level..];
77 if let Some(stripped) = rest.strip_prefix(' ') {
78 return Some(Self {
79 level,
80 content: stripped.to_string(),
81 });
82 }
83 }
84 None
85 }
86
87 pub fn encode(&self) -> String {
89 format!("{} {}", "#".repeat(self.level), self.content)
90 }
91
92 pub fn content_eq_ignore_case(&self, text: &str) -> bool {
94 self.content.eq_ignore_ascii_case(text)
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn test_header_new() {
104 let header = Header::new(2, "Test Content");
105 assert_eq!(header.level, 2);
106 assert_eq!(header.content, "Test Content");
107 }
108
109 #[test]
110 fn test_header_decode() {
111 assert_eq!(
113 Header::decode("# Heading 1"),
114 Some(Header {
115 level: 1,
116 content: "Heading 1".to_string()
117 })
118 );
119 assert_eq!(
120 Header::decode("## Heading 2"),
121 Some(Header {
122 level: 2,
123 content: "Heading 2".to_string()
124 })
125 );
126 assert_eq!(
127 Header::decode("### Heading 3"),
128 Some(Header {
129 level: 3,
130 content: "Heading 3".to_string()
131 })
132 );
133
134 assert_eq!(
136 Header::decode(" # Trimmed "),
137 Some(Header {
138 level: 1,
139 content: "Trimmed".to_string()
140 })
141 );
142
143 assert_eq!(Header::decode("#NoSpace"), None);
145
146 assert_eq!(Header::decode("Just text"), None);
148 assert_eq!(Header::decode("- List item"), None);
149 }
150
151 #[test]
152 fn test_header_encode() {
153 assert_eq!(Header::new(1, "Test").encode(), "# Test");
154 assert_eq!(Header::new(2, "Test").encode(), "## Test");
155 assert_eq!(Header::new(3, "Test").encode(), "### Test");
156 }
157
158 #[test]
159 fn test_header_roundtrip() {
160 for level in 1..=6 {
161 let original = Header::new(level, "Content");
162 let encoded = original.encode();
163 let decoded = Header::decode(&encoded).unwrap();
164 assert_eq!(original, decoded);
165 }
166 }
167
168 #[test]
169 fn test_header_content_eq_ignore_case() {
170 let header = Header::new(1, "Blockers");
171 assert!(header.content_eq_ignore_case("blockers"));
172 assert!(header.content_eq_ignore_case("BLOCKERS"));
173 assert!(header.content_eq_ignore_case("Blockers"));
174 assert!(!header.content_eq_ignore_case("Blocker"));
175 }
176}