Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move unindent implementation into indoc to flatten dependency #49

Merged
merged 1 commit into from May 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 1 addition & 3 deletions Cargo.toml
Expand Up @@ -14,12 +14,10 @@ rust-version = "1.42"
[lib]
proc-macro = true

[dependencies]
unindent = { version = "0.1.8", path = "unindent" }

[dev-dependencies]
rustversion = "1.0"
trybuild = { version = "1.0.49", features = ["diff"] }
unindent = { version = "0.1.8", path = "unindent" }

[workspace]
members = ["unindent"]
Expand Down
11 changes: 9 additions & 2 deletions src/lib.rs
Expand Up @@ -107,18 +107,25 @@
//! the first line.
//! 4. Remove the computed number of spaces from the beginning of each line.

#![allow(clippy::needless_doctest_main, clippy::needless_pass_by_value)]
#![allow(
clippy::module_name_repetitions,
clippy::needless_doctest_main,
clippy::needless_pass_by_value,
clippy::trivially_copy_pass_by_ref,
clippy::type_complexity
)]

mod error;
mod expr;
mod unindent;

use crate::error::{Error, Result};
use crate::expr::Expr;
use crate::unindent::unindent;
use proc_macro::token_stream::IntoIter as TokenIter;
use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree};
use std::iter::{self, FromIterator};
use std::str::FromStr;
use unindent::unindent;

#[derive(Copy, Clone, PartialEq)]
enum Macro {
Expand Down
131 changes: 131 additions & 0 deletions src/unindent.rs
@@ -0,0 +1,131 @@
use std::iter::Peekable;
use std::slice::Split;

pub fn unindent(s: &str) -> String {
let bytes = s.as_bytes();
let unindented = unindent_bytes(bytes);
String::from_utf8(unindented).unwrap()
}

// Compute the maximal number of spaces that can be removed from every line, and
// remove them.
pub fn unindent_bytes(s: &[u8]) -> Vec<u8> {
// Document may start either on the same line as opening quote or
// on the next line
let ignore_first_line = s.starts_with(b"\n") || s.starts_with(b"\r\n");

// Largest number of spaces that can be removed from every
// non-whitespace-only line after the first
let spaces = s
.lines()
.skip(1)
.filter_map(count_spaces)
.min()
.unwrap_or(0);

let mut result = Vec::with_capacity(s.len());
for (i, line) in s.lines().enumerate() {
if i > 1 || (i == 1 && !ignore_first_line) {
result.push(b'\n');
}
if i == 0 {
// Do not un-indent anything on same line as opening quote
result.extend_from_slice(line);
} else if line.len() > spaces {
// Whitespace-only lines may have fewer than the number of spaces
// being removed
result.extend_from_slice(&line[spaces..]);
}
}
result
}

pub trait Unindent {
type Output;

fn unindent(&self) -> Self::Output;
}

impl Unindent for str {
type Output = String;

fn unindent(&self) -> Self::Output {
unindent(self)
}
}

impl Unindent for String {
type Output = String;

fn unindent(&self) -> Self::Output {
unindent(self)
}
}

impl Unindent for [u8] {
type Output = Vec<u8>;

fn unindent(&self) -> Self::Output {
unindent_bytes(self)
}
}

impl<'a, T: ?Sized + Unindent> Unindent for &'a T {
type Output = T::Output;

fn unindent(&self) -> Self::Output {
(**self).unindent()
}
}

// Number of leading spaces in the line, or None if the line is entirely spaces.
fn count_spaces(line: &[u8]) -> Option<usize> {
for (i, ch) in line.iter().enumerate() {
if *ch != b' ' && *ch != b'\t' {
return Some(i);
}
}
None
}

// Based on core::str::StrExt.
trait BytesExt {
fn lines(&self) -> Lines;
}

impl BytesExt for [u8] {
fn lines(&self) -> Lines {
fn is_newline(b: &u8) -> bool {
*b == b'\n'
}
let bytestring = if self.starts_with(b"\r\n") {
&self[1..]
} else {
self
};
Lines {
split: bytestring.split(is_newline as fn(&u8) -> bool).peekable(),
}
}
}

struct Lines<'a> {
split: Peekable<Split<'a, u8, fn(&u8) -> bool>>,
}

impl<'a> Iterator for Lines<'a> {
type Item = &'a [u8];

fn next(&mut self) -> Option<Self::Item> {
match self.split.next() {
None => None,
Some(fragment) => {
if fragment.is_empty() && self.split.peek().is_none() {
None
} else {
Some(fragment)
}
}
}
}
}
133 changes: 3 additions & 130 deletions unindent/src/lib.rs
Expand Up @@ -48,139 +48,12 @@
#![doc(html_root_url = "https://docs.rs/unindent/0.1.8")]
#![allow(
clippy::missing_panics_doc,
clippy::module_name_repetitions,
clippy::must_use_candidate,
clippy::trivially_copy_pass_by_ref,
clippy::type_complexity
)]

use std::iter::Peekable;
use std::slice::Split;
mod unindent;

pub fn unindent(s: &str) -> String {
let bytes = s.as_bytes();
let unindented = unindent_bytes(bytes);
String::from_utf8(unindented).unwrap()
}

// Compute the maximal number of spaces that can be removed from every line, and
// remove them.
pub fn unindent_bytes(s: &[u8]) -> Vec<u8> {
// Document may start either on the same line as opening quote or
// on the next line
let ignore_first_line = s.starts_with(b"\n") || s.starts_with(b"\r\n");

// Largest number of spaces that can be removed from every
// non-whitespace-only line after the first
let spaces = s
.lines()
.skip(1)
.filter_map(count_spaces)
.min()
.unwrap_or(0);

let mut result = Vec::with_capacity(s.len());
for (i, line) in s.lines().enumerate() {
if i > 1 || (i == 1 && !ignore_first_line) {
result.push(b'\n');
}
if i == 0 {
// Do not un-indent anything on same line as opening quote
result.extend_from_slice(line);
} else if line.len() > spaces {
// Whitespace-only lines may have fewer than the number of spaces
// being removed
result.extend_from_slice(&line[spaces..]);
}
}
result
}

pub trait Unindent {
type Output;

fn unindent(&self) -> Self::Output;
}

impl Unindent for str {
type Output = String;

fn unindent(&self) -> Self::Output {
unindent(self)
}
}

impl Unindent for String {
type Output = String;

fn unindent(&self) -> Self::Output {
unindent(self)
}
}

impl Unindent for [u8] {
type Output = Vec<u8>;

fn unindent(&self) -> Self::Output {
unindent_bytes(self)
}
}

impl<'a, T: ?Sized + Unindent> Unindent for &'a T {
type Output = T::Output;

fn unindent(&self) -> Self::Output {
(**self).unindent()
}
}

// Number of leading spaces in the line, or None if the line is entirely spaces.
fn count_spaces(line: &[u8]) -> Option<usize> {
for (i, ch) in line.iter().enumerate() {
if *ch != b' ' && *ch != b'\t' {
return Some(i);
}
}
None
}

// Based on core::str::StrExt.
trait BytesExt {
fn lines(&self) -> Lines;
}

impl BytesExt for [u8] {
fn lines(&self) -> Lines {
fn is_newline(b: &u8) -> bool {
*b == b'\n'
}
let bytestring = if self.starts_with(b"\r\n") {
&self[1..]
} else {
self
};
Lines {
split: bytestring.split(is_newline as fn(&u8) -> bool).peekable(),
}
}
}

struct Lines<'a> {
split: Peekable<Split<'a, u8, fn(&u8) -> bool>>,
}

impl<'a> Iterator for Lines<'a> {
type Item = &'a [u8];

fn next(&mut self) -> Option<Self::Item> {
match self.split.next() {
None => None,
Some(fragment) => {
if fragment.is_empty() && self.split.peek().is_none() {
None
} else {
Some(fragment)
}
}
}
}
}
pub use unindent::*;
1 change: 1 addition & 0 deletions unindent/src/unindent.rs