use std::collections::HashSet;
use std::path::Path;
use crate::config::Config;
use crate::salsa::SymbolUsageIndex;
use crate::syntax::{
AstNode, AttributeNode, Citation, FootnoteReference, ImageLink, Link, SyntaxKind, SyntaxNode,
UnresolvedReference,
};
use crate::utils::{implicit_heading_ids, normalize_label};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct DefinitionLabels {
pub reference_labels: HashSet<String>,
pub footnote_ids: HashSet<String>,
pub crossref_labels: HashSet<String>,
pub heading_text_labels: HashSet<String>,
}
impl DefinitionLabels {
pub fn merge(&mut self, other: &DefinitionLabels) {
self.reference_labels
.extend(other.reference_labels.iter().cloned());
self.footnote_ids.extend(other.footnote_ids.iter().cloned());
self.crossref_labels
.extend(other.crossref_labels.iter().cloned());
self.heading_text_labels
.extend(other.heading_text_labels.iter().cloned());
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct UsageLabels {
pub reference_labels: HashSet<String>,
pub footnote_ids: HashSet<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ProjectSymbolIndex {
pub definitions: DefinitionLabels,
pub anchors: HashSet<String>,
pub usage: UsageLabels,
}
impl ProjectSymbolIndex {
pub fn fold_document(
&mut self,
tree: &SyntaxNode,
symbol_index: &SymbolUsageIndex,
config: &Config,
) {
extend_labels_from_tree(&mut self.definitions, tree, config, symbol_index);
extend_anchors(&mut self.anchors, tree, config, symbol_index);
let usage = collect_usage_labels(tree.descendants());
self.usage.reference_labels.extend(usage.reference_labels);
self.usage.footnote_ids.extend(usage.footnote_ids);
}
pub fn extend(&mut self, other: &ProjectSymbolIndex) {
self.definitions.merge(&other.definitions);
self.anchors.extend(other.anchors.iter().cloned());
self.usage
.reference_labels
.extend(other.usage.reference_labels.iter().cloned());
self.usage
.footnote_ids
.extend(other.usage.footnote_ids.iter().cloned());
}
}
pub fn build_from_fs(
project_root: &Path,
doc_path: &Path,
config: &Config,
is_bookdown: bool,
) -> ProjectSymbolIndex {
let mut aggregate = ProjectSymbolIndex::default();
let db = crate::salsa::SalsaDb::default();
for path in crate::includes::find_project_documents(project_root, config, is_bookdown) {
if path == doc_path {
continue;
}
if let Ok(other_input) = std::fs::read_to_string(&path) {
let tree = crate::parser::parse(&other_input, Some(config.clone()));
let index = crate::salsa::symbol_usage_index_from_tree(&db, &tree, &config.extensions);
aggregate.fold_document(&tree, &index, config);
}
}
aggregate
}
pub fn extend_labels_from_tree(
labels: &mut DefinitionLabels,
tree: &SyntaxNode,
config: &Config,
symbol_index: &SymbolUsageIndex,
) {
labels.reference_labels.extend(
symbol_index
.reference_definition_entries()
.map(|(label, _)| label.clone())
.filter(|label| !label.is_empty()),
);
labels.footnote_ids.extend(
symbol_index
.footnote_definition_entries()
.map(|(id, _)| id.clone())
.filter(|id| !id.is_empty()),
);
labels.crossref_labels.extend(
symbol_index
.crossref_declaration_entries()
.map(|(label, _)| label.clone())
.filter(|label| !label.is_empty()),
);
if config.extensions.implicit_header_references && config.extensions.auto_identifiers {
labels.heading_text_labels.extend(
symbol_index
.heading_label_entries()
.map(|(label, _)| label.clone())
.filter(|label| !label.is_empty()),
);
}
if config.extensions.bookdown_references && config.extensions.auto_identifiers {
labels
.crossref_labels
.extend(collect_implicit_heading_ids(tree, &config.extensions));
}
}
fn collect_implicit_heading_ids(
tree: &SyntaxNode,
extensions: &crate::config::Extensions,
) -> HashSet<String> {
implicit_heading_ids(tree, extensions)
.into_iter()
.map(|entry| entry.id)
.collect()
}
pub fn extend_anchors(
anchors: &mut HashSet<String>,
tree: &SyntaxNode,
config: &Config,
symbol_index: &SymbolUsageIndex,
) {
anchors.extend(
symbol_index
.crossref_declaration_entries()
.map(|(label, _)| label.clone())
.filter(|label| !label.is_empty()),
);
if config.extensions.auto_identifiers {
for entry in implicit_heading_ids(tree, &config.extensions) {
if heading_has_explicit_id(&entry.heading) {
continue;
}
if entry.id.is_empty() {
continue;
}
anchors.insert(entry.id);
}
}
if config.extensions.citations {
for citation in tree.descendants().filter_map(Citation::cast) {
for key in citation.key_texts() {
if key.is_empty() {
continue;
}
anchors.insert(format!("ref-{key}"));
}
}
}
}
fn heading_has_explicit_id(heading: &SyntaxNode) -> bool {
heading
.children()
.filter_map(AttributeNode::cast)
.any(|attribute| attribute.id().is_some())
}
pub fn collect_usage_labels(nodes: impl Iterator<Item = SyntaxNode>) -> UsageLabels {
let mut reference_labels: HashSet<String> = HashSet::new();
let mut footnote_ids: HashSet<String> = HashSet::new();
for node in nodes {
match node.kind() {
SyntaxKind::LINK => {
if let Some(label) = Link::cast(node).and_then(usage_label_from_link) {
reference_labels.insert(label);
}
}
SyntaxKind::IMAGE_LINK => {
if let Some(label) = ImageLink::cast(node).and_then(usage_label_from_image) {
reference_labels.insert(label);
}
}
SyntaxKind::UNRESOLVED_REFERENCE => {
if let Some(label) =
UnresolvedReference::cast(node).and_then(usage_label_from_unresolved)
{
reference_labels.insert(label);
}
}
SyntaxKind::FOOTNOTE_REFERENCE => {
if let Some(footnote) = FootnoteReference::cast(node) {
let id = normalize_label(&footnote.id());
if !id.is_empty() {
footnote_ids.insert(id);
}
}
}
_ => {}
}
}
UsageLabels {
reference_labels,
footnote_ids,
}
}
fn usage_label_from_link(link: Link) -> Option<String> {
if link
.syntax()
.ancestors()
.any(|ancestor| ancestor.kind() == SyntaxKind::REFERENCE_DEFINITION)
{
return None;
}
if link.dest().is_some() {
return None;
}
if let Some(link_ref) = link.reference() {
let label = normalize_label(&link_ref.label());
if !label.is_empty() {
return Some(label);
}
}
link.text()
.map(|text| normalize_label(&text.raw_label()))
.filter(|label| !label.is_empty())
}
fn usage_label_from_image(image: ImageLink) -> Option<String> {
if image
.syntax()
.ancestors()
.any(|ancestor| ancestor.kind() == SyntaxKind::REFERENCE_DEFINITION)
{
return None;
}
if image.dest().is_some() {
return None;
}
if let Some(link_ref) = image.reference() {
let label = normalize_label(&link_ref.label());
if !label.is_empty() {
return Some(label);
}
}
image
.alt()
.map(|alt| normalize_label(&alt.text()))
.filter(|label| !label.is_empty())
}
fn usage_label_from_unresolved(unresolved: UnresolvedReference) -> Option<String> {
if let Some(label) = unresolved.label() {
let normalized = normalize_label(&label);
if !normalized.is_empty() {
return Some(normalized);
}
}
let normalized = normalize_label(&unresolved.text());
if normalized.is_empty() {
None
} else {
Some(normalized)
}
}