use std::{
borrow::Cow,
collections::BTreeMap,
+ iter::once,
mem::take,
str::FromStr,
sync::{Arc, OnceLock},
};
use anyhow::anyhow;
+use bit_vec::BitVec;
use clap::{ArgAction, ArgMatches, Args, FromArgMatches, value_parser};
use enum_map::EnumMap;
use enumset::{EnumSet, EnumSetType};
}
}
+ /// Returns a new group item suitable as the root node of an output document.
+ ///
+ /// A root node is a group whose own properties are mostly disregarded.
+ /// Instead of having root nodes, it would make just as much sense to just
+ /// keep around arrays of nodes that would serve as the top level of an
+ /// output document, but we'd need more special cases instead of just using
+ /// the existing support for group items.
+ pub fn new_root() -> Self {
+ Self::new(Details::Group(Vec::new())).with_label(Some(String::from("Output")))
+ }
+
pub fn label(&self) -> Cow<'static, str> {
match &self.label {
Some(label) => Cow::from(label.clone()),
Self { show, ..self }
}
+ pub fn with_label(self, label: Option<String>) -> Self {
+ Self { label, ..self }
+ }
+
pub fn with_command_name(self, command_name: Option<String>) -> Self {
Self {
command_name,
}
}
+ pub fn as_mut_group(&mut self) -> Option<&mut Vec<Arc<Item>>> {
+ match self {
+ Self::Group(children) => Some(children),
+ _ => None,
+ }
+ }
+
+ pub fn children(&self) -> impl Iterator<Item = &Arc<Item>> {
+ match self {
+ Self::Group(children) => Some(children.iter()),
+ _ => None,
+ }
+ .into_iter()
+ .flatten()
+ }
+
+ pub fn as_message(&self) -> Option<&Diagnostic> {
+ match self {
+ Self::Message(diagnostic) => Some(diagnostic),
+ _ => None,
+ }
+ }
+
+ pub fn as_table(&self) -> Option<&PivotTable> {
+ match self {
+ Self::Table(table) => Some(table),
+ _ => None,
+ }
+ }
+
+ pub fn as_text(&self) -> Option<&Text> {
+ match self {
+ Self::Text(text) => Some(text),
+ _ => None,
+ }
+ }
+
pub fn command_name(&self) -> Option<&String> {
match self {
Details::Chart
pub fn label(&self) -> Cow<'static, str> {
match self {
Details::Chart => todo!(),
- Details::Image => todo!(),
+ Details::Image => Cow::from("Image"),
Details::Group(_) => Cow::from("Group"),
Details::Message(diagnostic) => Cow::from(diagnostic.severity.as_title_str()),
Details::PageBreak => Cow::from("Page Break"),
}
}
+ pub fn is_group(&self) -> bool {
+ matches!(self, Self::Group(_))
+ }
+
+ pub fn is_message(&self) -> bool {
+ matches!(self, Self::Message(_))
+ }
+
pub fn is_page_break(&self) -> bool {
matches!(self, Self::PageBreak)
}
+
+ pub fn is_table(&self) -> bool {
+ matches!(self, Self::Table(_))
+ }
+
+ pub fn is_text(&self) -> bool {
+ matches!(self, Self::Text(_))
+ }
}
impl<A> FromIterator<A> for Details
..self
}
}
+
+ pub fn member_names(&self) -> Vec<&str> {
+ let mut member_names = vec![self.structure_member.as_str()];
+ if let Some(members) = &self.members {
+ member_names.extend(members.iter());
+ }
+ member_names
+ }
}
/// Identifies ZIP file members for one kind of output item in an SPV file.
),
}
+impl SpvMembers {
+ pub fn iter(&self) -> impl Iterator<Item = &str> {
+ let (a, b) = match self {
+ SpvMembers::Light(a) => (a.as_str(), None),
+ SpvMembers::Legacy { xml: a, binary: b } => (a.as_str(), Some(b.as_str())),
+ SpvMembers::Image(a) => (a.as_str(), None),
+ };
+ once(a).chain(once(b).flatten())
+ }
+}
+
/// Classifications for output items. These only roughly correspond to the
/// output item types; for example, "warnings" are a subset of text items.
#[derive(Debug, EnumSetType)]
pub nth_commands: Vec<usize>,
/// Include the objects with the given 1-based indexes within each of the
- /// commands that are included. Indexes are 1-based. Negative indexes
- /// count backward from the last object in a command.
- pub instances: Vec<isize>,
+ /// commands that are included. Indexes are 1-based. Index 0 represents
+ /// the last instance in a command.
+ pub instances: Vec<usize>,
/// Include only XML and binary member names that match. Without any member
/// names, include all objects.
impl Selection {
pub fn parse_nth_commands(s: &str) -> Result<Vec<usize>, anyhow::Error> {
s.split(',')
- .map(|s| match s.parse::<usize>() {
- Ok(0) => Err(anyhow!("--nth-commmands values must be positive")),
- Ok(n) => Ok(n),
+ .map(|s| match s.trim().parse::<usize>() {
+ Ok(n) if n > 0 => Ok(n),
+ Ok(_) => Err(anyhow!("--nth-commmands values must be positive")),
Err(error) => Err(error.into()),
})
.collect()
}
- pub fn parse_instances(s: &str) -> Result<Vec<isize>, anyhow::Error> {
+ pub fn parse_instances(s: &str) -> Result<Vec<usize>, anyhow::Error> {
s.split(',')
.map(|s| {
+ let s = s.trim();
if s == "last" {
- Ok(-1)
+ Ok(0)
} else {
- match s.parse::<isize>() {
- Ok(0) => Err(anyhow!("--instances values must be nonzero")),
- Ok(n) => Ok(n),
+ match s.parse::<usize>() {
+ Ok(n) if n > 0 => Ok(n),
+ Ok(_) => Err(anyhow!("--instances values must be positive")),
Err(error) => Err(error.into()),
}
}
}
impl StringMatch {
- fn matches(&self, s: &str) -> bool {
+ pub fn is_default(&self) -> bool {
+ if let Self::Exclude(strings) = self
+ && strings.is_empty()
+ {
+ true
+ } else {
+ false
+ }
+ }
+ pub fn matches(&self, s: &str) -> bool {
fn inner(items: &[String], s: &str) -> bool {
items.iter().any(|item| match item.strip_suffix('*') {
Some(prefix) => s.starts_with(prefix),
impl Criteria {
/// Returns a new output item whose children are all the (direct and
/// indirect) children of `item` that meet the criteria.
- fn apply(&self, mut item: Item) -> Item {
- fn take_children(item: &mut Item) -> Vec<Arc<Item>> {
- match &mut item.details {
- Details::Group(items) => take(items),
- _ => Vec::new(),
- }
+ fn apply(&self, item: Item) -> Item {
+ fn take_children(item: &Item) -> Vec<&Item> {
+ item.details.children().map(|item| &**item).collect()
}
- fn flatten_children(
- children: Vec<Arc<Item>>,
+ fn flatten_children<'a>(
+ children: Vec<&'a Item>,
depth: usize,
- items: &mut Vec<Item>,
+ items: &mut Vec<&'a Item>,
depths: &mut Vec<usize>,
) {
for child in children {
- flatten(Arc::unwrap_or_clone(child), depth, items, depths);
+ flatten(child, depth, items, depths);
}
}
- fn flatten(mut item: Item, depth: usize, items: &mut Vec<Item>, depths: &mut Vec<usize>) {
- let children = take_children(&mut item);
+ fn flatten<'a>(
+ item: &'a Item,
+ depth: usize,
+ items: &mut Vec<&'a Item>,
+ depths: &mut Vec<usize>,
+ ) {
+ let children = take_children(item);
items.push(item);
depths.push(depth);
flatten_children(children, depth + 1, items, depths);
}
+ fn select_matches(
+ items: &[&Item],
+ depths: &[usize],
+ selection: &Selection,
+ include: &mut BitVec,
+ ) {
+ let mut instance_within_command = 0;
+ let mut last_instance = None;
+ let mut command_item = None;
+ let mut command_command_item = None;
+ let mut nth_command = 0;
+ for (index, (item, depth)) in std::iter::zip(items, depths).enumerate() {
+ if *depth == 0 {
+ command_item = Some(index);
+ if let Some(last_instance) = last_instance.take() {
+ include.set(last_instance, true);
+ }
+ instance_within_command = 0;
+ }
+ if !selection.classes.contains(item.class()) {
+ continue;
+ }
+ if let Some(visible) = selection.visible
+ && !item.details.is_group()
+ && visible != item.show
+ {
+ continue;
+ }
+ if let Some(error) = selection.error
+ && error
+ != item
+ .spv_info
+ .as_ref()
+ .map_or(false, |spv_info| spv_info.error)
+ {
+ continue;
+ }
+ if !selection
+ .commands
+ .matches(item.command_name.as_ref().map_or("", |name| name.as_str()))
+ {
+ continue;
+ }
+ if !selection.nth_commands.is_empty() {
+ if command_item != command_command_item {
+ command_command_item = command_command_item;
+ nth_command += 1;
+ }
+ if !selection.nth_commands.contains(&nth_command) {
+ continue;
+ }
+ }
+ if !selection.subtypes.is_default() {
+ let Some(table) = item.details.as_table() else {
+ continue;
+ };
+ let subtype = table.subtype().display(table).to_string();
+ if !selection.subtypes.matches(&subtype) {
+ continue;
+ }
+ }
+ if !selection.labels.matches(&item.label()) {
+ continue;
+ }
+ if !selection.members.is_empty() {
+ let Some(spv_info) = item.spv_info.as_ref() else {
+ continue;
+ };
+ let member_names = spv_info.member_names();
+ if !selection
+ .members
+ .iter()
+ .any(|name| member_names.contains(&name.as_str()))
+ {
+ continue;
+ }
+ }
+ if !selection.instances.is_empty() {
+ if *depth == 0 {
+ continue;
+ }
+ instance_within_command += 1;
+ if !selection.instances.contains(&instance_within_command) {
+ if selection.instances.contains(&0) {
+ last_instance = Some(index);
+ }
+ continue;
+ }
+ }
+
+ include.set(index, true);
+ }
+ }
+ fn unflatten_items(
+ items: Vec<Arc<Item>>,
+ mut index: usize,
+ include: &BitVec,
+ out: &mut Vec<Arc<Item>>,
+ ) {
+ for item in items {
+ unflatten_item(Arc::unwrap_or_clone(item), index, include, out);
+ index += 1;
+ }
+ }
+ fn unflatten_item(
+ mut item: Item,
+ mut index: usize,
+ include: &BitVec,
+ out: &mut Vec<Arc<Item>>,
+ ) {
+ let include_item = include[index];
+ index += 1;
+ match item.details {
+ Details::Group(ref mut children) => {
+ let in_children = take(children);
+ if !include_item {
+ unflatten_items(in_children, index, include, out);
+ return;
+ }
+ unflatten_items(in_children, index, include, children);
+ }
+ _ => {}
+ }
+ if include_item {
+ out.push(Arc::new(item));
+ }
+ todo!()
+ }
+
let mut items = Vec::new();
let mut depths = Vec::new();
- flatten_children(take_children(&mut item), 0, &mut items, &mut depths);
+ flatten_children(take_children(&item), 0, &mut items, &mut depths);
+
+ let mut include = BitVec::from_elem(items.len(), false);
+ for selection in &self.0 {
+ select_matches(&items, &depths, selection, &mut include);
+ }
+
+ let mut output = Item::new_root();
+ unflatten_item(item, 0, &include, output.details.as_mut_group().unwrap());
todo!()
}
}
Subtypes(StringMatch),
Labels(StringMatch),
NthCommands(Vec<usize>),
- Instances(Vec<isize>),
+ Instances(Vec<usize>),
ShowHidden(bool),
Errors(bool),
}
#[arg(long, required = false, value_parser = StringMatch::from_str, action = ArgAction::Append)]
labels: StringMatch,
- /// Include only the Nth (1-based) instance of the selected commands.
+ /// Include only objects from the Nth (1-based) command that matches
+ /// `--command`.
#[arg(long, required = false, value_parser = Selection::parse_nth_commands, action = ArgAction::Append)]
nth_commands: Vec<usize>,
+ /// Include only the given instances of an object that matches the other
+ /// criteria within a single command. Each instance may be a number (1 for
+ /// the first, and so on), or `last` for the last instance.
+ #[arg(long, required = false, value_parser = Selection::parse_instances, action = ArgAction::Append)]
+ instances: Vec<usize>,
+
/// Include hidden objects in the output (by default, they are excluded)
#[arg(long, required = false, action = ArgAction::Append)]
show_hidden: bool,
#[test]
fn parse_instances() {
assert_eq!(Selection::parse_instances("1").unwrap(), vec![1]);
- assert_eq!(
- Selection::parse_instances("2,3,-2,-3").unwrap(),
- vec![2, 3, -2, -3]
- );
- assert_eq!(Selection::parse_instances("last,1").unwrap(), vec![-1, 1]);
+ assert_eq!(Selection::parse_instances("2,3").unwrap(), vec![2, 3]);
+ assert_eq!(Selection::parse_instances("last,1").unwrap(), vec![0, 1]);
assert!(Selection::parse_instances("0").is_err());
}