diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..93a6444 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,21 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "apidoc" +version = "0.1.0" +dependencies = [ + "anyhow", + "apidoc_parser", +] + +[[package]] +name = "apidoc_parser" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..fdb0731 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[workspace] +resolver = "3" +members = ["src/apidoc", "src/apidoc_parser"] + +[workspace.package] +version = "0.1.0" +edition = "2024" +public = false + +[workspace.clippy] +strict = "warn" diff --git a/src/apidoc/Cargo.toml b/src/apidoc/Cargo.toml new file mode 100644 index 0000000..0c26f42 --- /dev/null +++ b/src/apidoc/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "apidoc" +version.workspace = true +edition.workspace = true + +[dependencies] +anyhow = "1.0.100" +apidoc_parser = { version = "0.1.0", path = "../apidoc_parser" } diff --git a/src/apidoc/src/main.rs b/src/apidoc/src/main.rs new file mode 100644 index 0000000..a966bdd --- /dev/null +++ b/src/apidoc/src/main.rs @@ -0,0 +1,17 @@ +use std::{env::args, fs}; + +use anyhow::Context; +use apidoc_parser::parse; + +fn main() -> anyhow::Result<()> { + let mut args = args().skip(1); + + let file = fs::read_to_string(args.next().context("expected a file path argument")?) + .context("opening file")?; + + println!("{file}"); + + parse(file); + + Ok(()) +} diff --git a/src/apidoc_parser/Cargo.toml b/src/apidoc_parser/Cargo.toml new file mode 100644 index 0000000..1f962c6 --- /dev/null +++ b/src/apidoc_parser/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "apidoc_parser" +version.workspace = true +edition.workspace = true +public.workspace = true + +[dependencies] diff --git a/src/apidoc_parser/src/comment.rs b/src/apidoc_parser/src/comment.rs new file mode 100644 index 0000000..dae010c --- /dev/null +++ b/src/apidoc_parser/src/comment.rs @@ -0,0 +1,20 @@ +#[derive(Debug, Clone, Copy)] +pub struct Span { + lo: usize, + hi: usize, +} + +impl Span { + pub const fn new(range: std::ops::Range) -> Self { + Self { + lo: range.start, + hi: range.end, + } + } +} + +#[derive(Debug, Clone)] +pub struct Comment { + pub(crate) prefix: String, + pub(crate) span: Span, +} diff --git a/src/apidoc_parser/src/cursor.rs b/src/apidoc_parser/src/cursor.rs new file mode 100644 index 0000000..fbcbe11 --- /dev/null +++ b/src/apidoc_parser/src/cursor.rs @@ -0,0 +1,75 @@ +#[derive(Clone)] +pub(crate) struct Cursor { + source: String, + offset: usize, + byte_offset: usize, +} + +impl Cursor { + pub const fn new(source: String) -> Self { + Self { + source, + offset: 0, + byte_offset: 0, + } + } + + pub fn slice(&self) -> &'_ str { + self.source + .get(self.byte_offset..) + .expect("str should be built out of bounded chars") + } + + pub fn bump(&mut self) -> &mut Self { + self.offset += 1; + self.byte_offset += self.slice().chars().next().map_or(0, char::len_utf8); + + self + } + + pub fn bump_newline(&mut self) -> bool { + while !self.slice().starts_with('\n') && !self.bump().is_finished() {} + + if self.slice().starts_with('\n') { + self.bump(); + return true; + } + false + } + + pub fn skip(&mut self, chars: usize) { + let len: usize = self.slice().chars().take(chars).map(char::len_utf8).sum(); + + self.offset += chars; + self.byte_offset += len; + } + + pub fn skip_whitespace(&mut self) { + let mut offset = 0; + let mut byte_offset = 0; + + for char in self.slice().chars() { + if !char.is_ascii_whitespace() || char == '\n' { + break; + } + + offset += 1; + byte_offset += char.len_utf8(); + } + + self.offset += offset; + self.byte_offset += byte_offset; + } + + pub const fn is_finished(&self) -> bool { + self.byte_offset >= self.source.len() + } + + pub const fn offset(&self) -> usize { + self.offset + } + + pub fn fmt_offset(&self) -> String { + format!("{} (byte {})", self.offset, self.byte_offset) + } +} diff --git a/src/apidoc_parser/src/lib.rs b/src/apidoc_parser/src/lib.rs new file mode 100644 index 0000000..4f2c2b6 --- /dev/null +++ b/src/apidoc_parser/src/lib.rs @@ -0,0 +1,55 @@ +use crate::comment::{Comment, Span}; + +pub mod comment; +pub(crate) mod cursor; + +use cursor::Cursor; + +// TODO: (issue #3) Move these into some variable configuration +const APIDOC_PRAGMA: &str = "@apidoc"; +const COMMENT_PREFIX: &str = "//"; + +pub fn parse(source: String) -> Vec { + let mut comments = Vec::new(); + + let mut cursor = Cursor::new(source); + + while !cursor.bump().is_finished() { + let slice = cursor.slice(); + + if slice.starts_with(COMMENT_PREFIX) { + println!("comment: at offset {}", cursor.fmt_offset()); + + let start = cursor.offset(); + + cursor.skip(COMMENT_PREFIX.len()); + cursor.skip_whitespace(); + if !cursor.slice().starts_with(APIDOC_PRAGMA) { + continue; + } + + let prefix = COMMENT_PREFIX; + + println!(" pragma! {}", cursor.fmt_offset()); + + cursor.bump_newline(); + let mut end = cursor.offset() - 1; + + loop { + cursor.skip_whitespace(); + if !cursor.slice().starts_with(prefix) { + break; + } + cursor.bump_newline(); + end = cursor.offset() - 1; + } + + comments.push(dbg!(Comment { + prefix: COMMENT_PREFIX.to_owned(), + span: Span::new(start..end) + })); + } + } + + comments +}