diff --git a/src/main.rs b/src/main.rs
index 3790416..6890bcd 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -16,6 +16,71 @@
// You should have received a copy of the GNU Affero General Public License
// along with Saul. If not, see .
+use std::{
+ borrow::Cow,
+ io::{self, BufRead},
+};
+
+/// Configuration of a particular language. Affects parsing and output.
+pub struct Language {
+ /// A string that appears before all commented lines.
+ pub comment: Cow<'static, str>,
+}
+
+impl Language {
+ /// The default Rust language config.
+ pub const RUST: Self = Self {
+ comment: Cow::Borrowed("// "),
+ };
+
+ /// Extracts a header (as a list of lines) from an input in this language.
+ ///
+ /// Don't reply on the state of the reader being consistent after this function
+ /// is called.
+ pub fn read_header(&self, f: &mut impl BufRead) -> io::Result> {
+ let mut header = Vec::new();
+ let mut is_first_line = true;
+
+ for line in f.lines() {
+ let line = line?;
+
+ if is_first_line && line.starts_with("#!") {
+ is_first_line = false;
+ continue;
+ }
+
+ is_first_line = false;
+
+ let Some(content) = line.strip_prefix(self.comment.as_ref()) else {
+ break;
+ };
+
+ header.push(content.trim().to_string());
+ }
+
+ Ok(header)
+ }
+}
+
fn main() {
println!("Hello, world!");
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn header() {
+ let src = b"// header content\n// header content 2\nbody";
+ let lines = Language::RUST.read_header(&mut src.as_slice()).unwrap();
+ assert_eq!(lines, vec!["header content", "header content 2"]);
+ }
+
+ #[test]
+ fn skip_header_shebang() {
+ let src = b"#!/bin/sh\n// header content\nbody goes here";
+ let lines = Language::RUST.read_header(&mut src.as_slice()).unwrap();
+ assert_eq!(lines, vec!["header content"]);
+ }
+}