#![deny(unsafe_code)]
#![warn(rust_2018_idioms)]
#![warn(missing_docs)]
#![warn(clippy::all)]
use std::collections::HashMap;
use std::io;
use std::path::Path;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PodDoc {
pub name: Option<String>,
pub synopsis: Option<String>,
pub description: Option<String>,
pub methods: HashMap<String, String>,
}
impl PodDoc {
#[must_use]
pub fn is_empty(&self) -> bool {
self.name.is_none()
&& self.synopsis.is_none()
&& self.description.is_none()
&& self.methods.is_empty()
}
}
pub fn extract_pod_from_file(path: &Path) -> io::Result<PodDoc> {
let content = std::fs::read_to_string(path)?;
Ok(extract_pod(&content))
}
#[must_use]
pub fn extract_pod(source: &str) -> PodDoc {
let mut doc = PodDoc::default();
let mut current_section: Option<Section> = None;
let mut body = String::new();
let mut in_pod = false;
let mut in_over = false;
for line in source.lines() {
if line.starts_with("=head")
|| line.starts_with("=pod")
|| line.starts_with("=over")
|| line.starts_with("=begin")
|| line.starts_with("=for")
|| line.starts_with("=encoding")
|| line.starts_with("=item")
{
in_pod = true;
}
if !in_pod {
continue;
}
if line.starts_with("=cut") {
flush_section(&mut doc, ¤t_section, &body, in_over);
current_section = None;
body.clear();
in_pod = false;
in_over = false;
continue;
}
if line.starts_with("=over") {
in_over = true;
body.push('\n');
continue;
}
if line.starts_with("=back") {
in_over = false;
body.push('\n');
continue;
}
if line.starts_with("=item") {
let item_text = line.strip_prefix("=item").map(str::trim).unwrap_or("");
if !body.is_empty() {
body.push('\n');
}
body.push_str("- ");
body.push_str(&strip_pod_formatting(item_text));
body.push('\n');
continue;
}
if let Some(heading) = line.strip_prefix("=head1") {
flush_section(&mut doc, ¤t_section, &body, false);
body.clear();
let heading = heading.trim();
current_section = Some(match heading {
"NAME" => Section::Name,
"SYNOPSIS" => Section::Synopsis,
"DESCRIPTION" => Section::Description,
_ => Section::Other(()),
});
continue;
}
if let Some(heading) = line.strip_prefix("=head2") {
flush_section(&mut doc, ¤t_section, &body, false);
body.clear();
let heading = heading.trim().to_string();
current_section = Some(Section::Method(heading));
continue;
}
if line.starts_with("=pod")
|| line.starts_with("=encoding")
|| line.starts_with("=begin")
|| line.starts_with("=end")
|| line.starts_with("=for")
{
continue;
}
if current_section.is_some() && (!body.is_empty() || !line.is_empty()) {
if !body.is_empty() {
body.push('\n');
}
body.push_str(line);
}
}
flush_section(&mut doc, ¤t_section, &body, in_over);
doc
}
#[derive(Debug)]
enum Section {
Name,
Synopsis,
Description,
Method(String),
Other(()),
}
fn flush_section(doc: &mut PodDoc, section: &Option<Section>, body: &str, _in_over: bool) {
let section = match section {
Some(s) => s,
None => return,
};
let trimmed = body.trim();
if trimmed.is_empty() {
return;
}
let cleaned = strip_pod_formatting(trimmed);
match section {
Section::Name => {
doc.name = Some(cleaned);
}
Section::Synopsis => {
doc.synopsis = Some(cleaned);
}
Section::Description => {
let first_para = first_paragraph(&cleaned);
doc.description = Some(first_para);
}
Section::Method(name) => {
doc.methods.insert(name.clone(), cleaned);
}
Section::Other(_) => {
}
}
}
fn first_paragraph(text: &str) -> String {
let mut result = String::new();
for line in text.lines() {
if line.trim().is_empty() && !result.is_empty() {
break;
}
if !result.is_empty() {
result.push('\n');
}
result.push_str(line);
}
result
}
fn strip_pod_formatting(text: &str) -> String {
let mut result = String::with_capacity(text.len());
let chars: Vec<char> = text.chars().collect();
let len = chars.len();
let mut i = 0;
while i < len {
if i + 2 < len
&& chars[i].is_ascii_alphabetic()
&& chars[i + 1] == '<'
&& is_pod_format_code(chars[i])
{
let code_char = chars[i];
i += 2;
let mut depth = 1;
let start = i;
while i < len && depth > 0 {
if chars[i] == '<' {
depth += 1;
} else if chars[i] == '>' {
depth -= 1;
}
if depth > 0 {
i += 1;
}
}
let inner = &chars[start..i];
let inner_str: String = inner.iter().collect();
let display = if code_char == 'L' {
extract_link_display(&inner_str)
} else {
strip_pod_formatting(&inner_str)
};
result.push_str(&display);
if i < len {
i += 1; }
} else {
result.push(chars[i]);
i += 1;
}
}
result
}
fn extract_link_display(link: &str) -> String {
if let Some(pipe_pos) = link.find('|') {
return strip_pod_formatting(&link[..pipe_pos]);
}
if let Some(slash_pos) = link.find('/') {
return strip_pod_formatting(&link[..slash_pos]);
}
strip_pod_formatting(link)
}
fn is_pod_format_code(c: char) -> bool {
matches!(c, 'B' | 'I' | 'C' | 'L' | 'F' | 'S' | 'E' | 'X' | 'Z')
}