#![feature(vec_peek_mut)]
#![feature(error_generic_member_access)]
pub mod github;
pub mod issue;
pub mod local;
pub mod mocks;
pub mod remote;
pub mod sink;
pub mod current_user {
use std::sync::RwLock;
static CURRENT_USER: RwLock<Option<String>> = RwLock::new(None);
pub fn set(user: String) {
*CURRENT_USER.write().unwrap() = Some(user);
}
pub fn get() -> Option<String> {
CURRENT_USER.read().unwrap().clone()
}
pub fn is(user: &str) -> bool {
CURRENT_USER.read().unwrap().as_deref() == Some(user)
}
}
pub use issue::{
BlockerItem, BlockerSequence, BlockerSetState, CloseState, Comment, CommentIdentity, Comments, Events, HollowIssue, Issue, IssueContents, IssueError, IssueIdentity, IssueIndex,
IssueLink, IssueMarker, IssueRef, IssueSelector, IssueTimestamps, LazyIssue, LinkedIssueMeta, MAX_INDEX_DEPTH, MAX_LINEAGE_DEPTH, MAX_TITLE_LENGTH, Marker, MilestoneBlockerCache,
MilestoneDoc, OwnedCodeBlockKind, OwnedEvent, OwnedTag, OwnedTagEnd, ParseError, RepoInfo, TitleInGitPathError, VirtualIssue, join_with_blockers, parse_blockers_from_embedded,
serialize_blockers_view, split_blockers,
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Header {
pub level: usize,
pub content: String,
}
impl Header {
pub fn new(level: usize, content: impl Into<String>) -> Self {
debug_assert!(level >= 1, "Header level must be >= 1");
Self {
level: level.max(1),
content: content.into(),
}
}
pub fn decode(s: &str) -> Option<Self> {
let trimmed = s.trim();
if !trimmed.starts_with('#') {
return None;
}
let mut level = 0;
for ch in trimmed.chars() {
if ch == '#' {
level += 1;
} else {
break;
}
}
if level > 0 && trimmed.len() > level {
let rest = &trimmed[level..];
if let Some(stripped) = rest.strip_prefix(' ') {
return Some(Self {
level,
content: stripped.to_string(),
});
}
}
None
}
pub fn encode(&self) -> String {
format!("{} {}", "#".repeat(self.level), self.content)
}
pub fn content_eq_ignore_case(&self, text: &str) -> bool {
self.content.eq_ignore_ascii_case(text)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_header_new() {
let header = Header::new(2, "Test Content");
assert_eq!(header.level, 2);
assert_eq!(header.content, "Test Content");
}
#[test]
fn test_header_decode() {
assert_eq!(
Header::decode("# Heading 1"),
Some(Header {
level: 1,
content: "Heading 1".to_string()
})
);
assert_eq!(
Header::decode("## Heading 2"),
Some(Header {
level: 2,
content: "Heading 2".to_string()
})
);
assert_eq!(
Header::decode("### Heading 3"),
Some(Header {
level: 3,
content: "Heading 3".to_string()
})
);
assert_eq!(
Header::decode(" # Trimmed "),
Some(Header {
level: 1,
content: "Trimmed".to_string()
})
);
assert_eq!(Header::decode("#NoSpace"), None);
assert_eq!(Header::decode("Just text"), None);
assert_eq!(Header::decode("- List item"), None);
}
#[test]
fn test_header_encode() {
assert_eq!(Header::new(1, "Test").encode(), "# Test");
assert_eq!(Header::new(2, "Test").encode(), "## Test");
assert_eq!(Header::new(3, "Test").encode(), "### Test");
}
#[test]
fn test_header_roundtrip() {
for level in 1..=6 {
let original = Header::new(level, "Content");
let encoded = original.encode();
let decoded = Header::decode(&encoded).unwrap();
assert_eq!(original, decoded);
}
}
#[test]
fn test_header_content_eq_ignore_case() {
let header = Header::new(1, "Blockers");
assert!(header.content_eq_ignore_case("blockers"));
assert!(header.content_eq_ignore_case("BLOCKERS"));
assert!(header.content_eq_ignore_case("Blockers"));
assert!(!header.content_eq_ignore_case("Blocker"));
}
}