use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::Mutex;
use tower_lsp_server::ls_types::{Location, Range, Uri};
use crate::Config;
use crate::lsp::DocumentState;
use crate::salsa::Db;
use crate::syntax::{
AstNode, AttributeNode, Citation, CodeBlock, CodeSpan, Crossref, FootnoteDefinition,
FootnoteReference, ImageLink, InlineMath, Link, LinkRef, ParsedYamlRegionSnapshot,
ReferenceDefinition, SyntaxKind, SyntaxNode, UnresolvedReference,
};
use crate::utils::{normalize_anchor_label, normalize_label};
use rowan::{NodeOrToken, TextRange, TextSize};
use super::config::load_config;
pub(crate) async fn get_document_content(
document_map: &Arc<Mutex<HashMap<String, DocumentState>>>,
salsa_db: &Arc<Mutex<crate::salsa::SalsaDb>>,
uri: &Uri,
) -> Option<String> {
let state = {
let doc_map = document_map.lock().await;
doc_map.get(&uri.to_string())?.clone()
};
let db = salsa_db.lock().await;
Some(state.salsa_file.text(&*db).clone())
}
pub(crate) async fn get_document_content_and_tree(
document_map: &Arc<Mutex<HashMap<String, DocumentState>>>,
salsa_db: &Arc<Mutex<crate::salsa::SalsaDb>>,
uri: &Uri,
) -> Option<(String, SyntaxNode)> {
let state = {
let doc_map = document_map.lock().await;
doc_map.get(&uri.to_string())?.clone()
};
let db = salsa_db.lock().await;
Some((
state.salsa_file.text(&*db).clone(),
SyntaxNode::new_root(state.tree.clone()),
))
}
pub(crate) async fn get_config(
client: &tower_lsp_server::Client,
workspace_root: &Arc<Mutex<Option<PathBuf>>>,
uri: &Uri,
) -> Config {
let workspace_root = workspace_root.lock().await.clone();
load_config(client, &workspace_root, Some(uri)).await
}
pub(crate) async fn get_document_and_config(
client: &tower_lsp_server::Client,
document_map: &Arc<Mutex<HashMap<String, DocumentState>>>,
salsa_db: &Arc<Mutex<crate::salsa::SalsaDb>>,
workspace_root: &Arc<Mutex<Option<PathBuf>>>,
uri: &Uri,
) -> Option<(String, Config)> {
let content = get_document_content(document_map, salsa_db, uri).await?;
let config = get_config(client, workspace_root, uri).await;
Some((content, config))
}
pub(crate) async fn get_definition_index_with_includes(
document_map: &Arc<Mutex<HashMap<String, DocumentState>>>,
salsa_db: &Arc<Mutex<crate::salsa::SalsaDb>>,
uri: &Uri,
) -> crate::salsa::DefinitionIndex {
let (salsa_file, salsa_config, root_path) = {
let doc_map = document_map.lock().await;
let Some(state) = doc_map.get(&uri.to_string()) else {
return crate::salsa::DefinitionIndex::default();
};
let root_path = state
.path
.clone()
.unwrap_or_else(|| PathBuf::from("<memory>"));
(state.salsa_file, state.salsa_config, root_path)
};
let db = salsa_db.lock().await;
let graph =
crate::salsa::project_graph(&*db, salsa_file, salsa_config, root_path.clone()).clone();
let mut index =
crate::salsa::definition_index(&*db, salsa_file, salsa_config, root_path).clone();
for path in graph.documents().iter() {
if let Some(include_file) = db.file_text(path.clone()) {
let include_index =
crate::salsa::definition_index(&*db, include_file, salsa_config, path.clone());
index.merge_from(include_index);
}
}
index
}
pub(crate) fn citation_definition_locations(
index: &crate::salsa::CitationDefinitionIndex,
key: &str,
default_uri: &Uri,
default_content: &str,
db: &dyn crate::salsa::Db,
) -> Vec<Location> {
let mut out = Vec::new();
let norm = normalize_label(key);
if let Some(entries) = index.by_key(&norm) {
for entry in entries {
let entry_uri = Uri::from_file_path(&entry.path).unwrap_or_else(|| default_uri.clone());
let text = if entry_uri == *default_uri {
default_content.to_string()
} else {
db.file_text(entry.path.clone())
.map(|file| file.text(db).clone())
.unwrap_or_default()
};
out.push(Location {
uri: entry_uri,
range: Range {
start: crate::lsp::conversions::offset_to_position(
&text,
entry.range.start().into(),
),
end: crate::lsp::conversions::offset_to_position(
&text,
entry.range.end().into(),
),
},
});
}
}
out.sort_by(|a, b| {
a.uri
.as_str()
.cmp(b.uri.as_str())
.then(a.range.start.line.cmp(&b.range.start.line))
.then(a.range.start.character.cmp(&b.range.start.character))
.then(a.range.end.line.cmp(&b.range.end.line))
.then(a.range.end.character.cmp(&b.range.end.character))
});
out.dedup_by(|a, b| a.uri == b.uri && a.range == b.range);
out
}
pub(crate) fn find_node_at_offset(root: &SyntaxNode, offset: usize) -> Option<SyntaxNode> {
let text_size = TextSize::from(offset as u32);
let range = TextRange::new(text_size, text_size);
match root.covering_element(range) {
NodeOrToken::Node(node) => Some(node),
NodeOrToken::Token(token) => token.parent(),
}
}
pub(crate) fn is_offset_in_yaml_frontmatter(
parsed_yaml_regions: &[ParsedYamlRegionSnapshot],
offset: usize,
) -> bool {
parsed_yaml_regions
.iter()
.find(|region| region.is_frontmatter())
.is_some_and(|frontmatter| {
let range = frontmatter.host_range();
range.start <= offset && offset < range.end
})
}
pub(crate) fn is_yaml_frontmatter_valid(parsed_yaml_regions: &[ParsedYamlRegionSnapshot]) -> bool {
parsed_yaml_regions
.iter()
.find(|region| region.is_frontmatter())
.is_none_or(ParsedYamlRegionSnapshot::is_valid)
}
pub(crate) fn extract_reference_label(node: &SyntaxNode) -> Option<(String, bool)> {
if let Some(link_ref) = LinkRef::cast(node.clone()) {
return Some((normalize_label(&link_ref.label()), false));
}
if let Some(footnote_ref) = FootnoteReference::cast(node.clone()) {
let id = footnote_ref.id();
if !id.is_empty() {
return Some((normalize_label(&id), true));
}
}
None
}
pub(crate) fn extract_reference_target(node: &SyntaxNode) -> Option<(String, bool)> {
if let Some(reference) = extract_reference_label(node) {
return Some(reference);
}
if let Some(link) = Link::cast(node.clone())
&& let Some(link_ref) = link.reference()
{
return extract_reference_label(link_ref.syntax());
}
if let Some(image) = ImageLink::cast(node.clone())
&& let Some(label) = image.reference_label()
{
return Some((normalize_label(&label), false));
}
None
}
pub(crate) fn extract_definition_target(node: &SyntaxNode) -> Option<(String, bool)> {
if let Some(reference_def) = ReferenceDefinition::cast(node.clone()) {
let label = normalize_label(&reference_def.label());
if !label.is_empty() {
return Some((label, false));
}
}
if let Some(footnote_def) = FootnoteDefinition::cast(node.clone()) {
let id = normalize_label(&footnote_def.id());
if !id.is_empty() {
return Some((id, true));
}
}
None
}
pub(crate) fn extract_definition_target_at_offset(
root: &SyntaxNode,
offset: usize,
) -> Option<(String, bool)> {
let mut node = find_node_at_offset(root, offset)?;
loop {
if let Some(target) = extract_definition_target(&node) {
return Some(target);
}
node = node.parent()?;
}
}
pub(crate) fn extract_citation_key(node: &SyntaxNode) -> Option<String> {
node_and_ancestors(node)
.find_map(Citation::cast)
.and_then(|citation| citation.keys().first().map(|key| key.text()))
}
pub(crate) fn extract_crossref_key(node: &SyntaxNode) -> Option<String> {
node_and_ancestors(node)
.find_map(Crossref::cast)
.and_then(|crossref| crossref.keys().first().map(|key| key.text()))
}
pub(crate) fn extract_chunk_label_key(node: &SyntaxNode) -> Option<String> {
chunk_label_entry_at_node(node).map(|entry| entry.value().to_string())
}
pub(crate) fn extract_attribute_id_key(node: &SyntaxNode) -> Option<String> {
if let Some(attribute) = AttributeNode::cast(node.clone())
&& let Some(id) = attribute.id()
{
return Some(id);
}
let mut current = node.clone();
while let Some(parent) = current.parent() {
if let Some(attribute) = AttributeNode::cast(parent.clone())
&& let Some(id) = attribute.id()
{
return Some(id);
}
current = parent;
}
None
}
pub(crate) fn extract_heading_id_key(node: &SyntaxNode) -> Option<String> {
if let Some(attribute) = AttributeNode::cast(node.clone())
&& let Some(id) = attribute.id()
&& attribute_has_heading_ancestor(attribute.syntax())
{
return Some(normalize_anchor_label(&id));
}
let mut current = node.clone();
while let Some(parent) = current.parent() {
if let Some(attribute) = AttributeNode::cast(parent.clone())
&& let Some(id) = attribute.id()
&& attribute_has_heading_ancestor(attribute.syntax())
{
return Some(normalize_anchor_label(&id));
}
current = parent;
}
None
}
pub(crate) fn extract_heading_link_target(node: &SyntaxNode) -> Option<String> {
if let Some(target) = heading_target_from_node(node) {
return Some(target);
}
let mut current = node.clone();
while let Some(parent) = current.parent() {
if let Some(target) = heading_target_from_node(&parent) {
return Some(target);
}
current = parent;
}
None
}
fn heading_target_from_node(node: &SyntaxNode) -> Option<String> {
if let Some(link) = Link::cast(node.clone()) {
return heading_target_from_link(&link);
}
if let Some(unresolved) = UnresolvedReference::cast(node.clone()) {
return heading_target_from_unresolved(&unresolved);
}
None
}
fn heading_target_from_unresolved(unresolved: &UnresolvedReference) -> Option<String> {
if unresolved.is_image() || unresolved.label().is_some() {
return None;
}
let label = normalize_label(&unresolved.text());
(!label.is_empty()).then_some(label)
}
pub(crate) fn extract_example_label_target_at_offset(
root: &SyntaxNode,
offset: usize,
) -> Option<String> {
let text = root.text().to_string();
if offset > text.len() {
return None;
}
example_label_at_offset(&text, offset).map(normalize_label)
}
pub(crate) fn extract_symbol_text_range(node: &SyntaxNode) -> Option<TextRange> {
if let Some(crossref) = Crossref::cast(node.clone()) {
return crossref.keys().first().map(|key| key.text_range());
}
if let Some(citation) = Citation::cast(node.clone()) {
return citation.keys().first().map(|key| key.text_range());
}
if let Some(entry) = chunk_label_entry_at_node(node) {
return Some(entry.value_range());
}
if let Some(attribute) = AttributeNode::cast(node.clone())
&& attribute.id().is_some()
{
return attribute.id_value_range();
}
if let Some(link) = Link::cast(node.clone()) {
if let Some(dest) = link.dest() {
return dest.hash_anchor_id_range();
}
if let Some(link_ref) = link.reference() {
return link_ref.label_value_range();
}
if link.reference().is_none() {
return link.text().map(|text| text.syntax().text_range());
}
}
if let Some(unresolved) = UnresolvedReference::cast(node.clone())
&& !unresolved.is_image()
{
if unresolved.label().is_some() {
if let Some(link_ref) = unresolved
.syntax()
.children()
.find(|c| c.kind() == SyntaxKind::LINK_REF)
.and_then(LinkRef::cast)
{
return link_ref.label_value_range();
}
} else {
return unresolved
.syntax()
.children()
.find(|c| c.kind() == SyntaxKind::LINK_TEXT)
.map(|n| n.text_range());
}
}
if let Some(math) = InlineMath::cast(node.clone())
&& let Some(range) = math.content_range()
{
return Some(range);
}
if let Some(code) = CodeSpan::cast(node.clone())
&& let Some(range) = code.content_range()
{
return Some(range);
}
if let Some(image) = ImageLink::cast(node.clone())
&& let Some(range) = image.reference_label_range()
{
return Some(range);
}
if let Some(footnote_ref) = FootnoteReference::cast(node.clone()) {
return footnote_ref
.id_value_range()
.or_else(|| Some(footnote_ref.id_range()));
}
None
}
pub(crate) fn example_label_range_at_offset(root: &SyntaxNode, offset: usize) -> Option<TextRange> {
let text = root.text().to_string();
if offset > text.len() {
return None;
}
let (start, label) = example_label_span_at_offset(&text, offset)?;
let label_start = rowan::TextSize::from((start + 2) as u32);
let label_end = rowan::TextSize::from((start + 2 + label.len()) as u32);
Some(TextRange::new(label_start, label_end))
}
pub(crate) fn find_symbol_text_range_at_offset(
root: &SyntaxNode,
offset: usize,
) -> Option<TextRange> {
let mut node = find_node_at_offset(root, offset)?;
loop {
if let Some(range) = extract_symbol_text_range(&node) {
return Some(range);
}
node = node.parent()?;
}
}
fn attribute_has_heading_ancestor(node: &SyntaxNode) -> bool {
node.ancestors()
.any(|ancestor| ancestor.kind() == SyntaxKind::HEADING)
}
fn node_and_ancestors(node: &SyntaxNode) -> impl Iterator<Item = SyntaxNode> {
std::iter::once(node.clone()).chain(node.ancestors())
}
fn heading_target_from_link(link: &Link) -> Option<String> {
if let Some(dest) = link.dest() {
let id = normalize_anchor_label(&dest.hash_anchor_id()?);
return (!id.is_empty()).then_some(id);
}
if link.reference().is_none()
&& let Some(text) = link.text()
{
let label = normalize_label(&text.text_content());
return (!label.is_empty()).then_some(label);
}
None
}
fn example_label_spans(text: &str) -> impl Iterator<Item = (usize, &str)> {
text.char_indices().filter_map(|(idx, ch)| {
if ch != '(' {
return None;
}
let slice = &text[idx..];
let rest = slice.strip_prefix("(@")?;
let label_end = rest
.chars()
.take_while(|c| c.is_ascii_alphanumeric() || *c == '_' || *c == '-')
.count();
if label_end == 0 {
return None;
}
if rest.chars().nth(label_end) != Some(')') {
return None;
}
Some((idx, &rest[..label_end]))
})
}
fn example_label_at_offset(text: &str, offset: usize) -> Option<&str> {
example_label_span_at_offset(text, offset).map(|(_, label)| label)
}
fn example_label_span_at_offset(text: &str, offset: usize) -> Option<(usize, &str)> {
let start = offset.saturating_sub(128);
let end = (offset + 128).min(text.len());
let window = text.get(start..end)?;
let rel_offset = offset - start;
for (idx, label) in example_label_spans(window) {
let label_start = idx + 2;
let label_end = label_start + label.len();
if label_start <= rel_offset && rel_offset <= label_end {
return Some((start + idx, label));
}
}
None
}
fn chunk_label_entry_at_node(node: &SyntaxNode) -> Option<crate::syntax::ChunkLabelEntry> {
let node_range = node.text_range();
let block = node_and_ancestors(node).find_map(CodeBlock::cast)?;
block.chunk_label_entries().into_iter().find(|entry| {
let declaration = entry.declaration_range();
let value = entry.value_range();
text_range_contains(declaration, node_range) || text_range_contains(value, node_range)
})
}
fn text_range_contains(outer: TextRange, inner: TextRange) -> bool {
outer.start() <= inner.start() && inner.end() <= outer.end()
}
#[cfg(test)]
fn extract_reference_definition_label(node: &SyntaxNode) -> Option<String> {
crate::syntax::ReferenceDefinition::cast(node.clone())
.map(|def| normalize_label(&def.label()))
.filter(|label| !label.is_empty())
}
#[cfg(test)]
fn extract_definition_label(node: &SyntaxNode) -> Option<String> {
match node.kind() {
SyntaxKind::REFERENCE_DEFINITION => extract_reference_definition_label(node),
SyntaxKind::FOOTNOTE_DEFINITION => crate::syntax::FootnoteDefinition::cast(node.clone())
.map(|def| normalize_label(&def.id()))
.filter(|label| !label.is_empty()),
_ => None,
}
}
#[cfg(test)]
pub(crate) fn find_definition_node(
root: &SyntaxNode,
label: &str,
is_footnote: bool,
) -> Option<SyntaxNode> {
let target_kind = if is_footnote {
SyntaxKind::FOOTNOTE_DEFINITION
} else {
SyntaxKind::REFERENCE_DEFINITION
};
root.descendants().find(|node| {
node.kind() == target_kind && extract_definition_label(node).as_deref() == Some(label)
})
}
#[cfg(test)]
pub(crate) fn find_definition_at_offset(root: &SyntaxNode, offset: usize) -> Option<TextRange> {
let mut node = find_node_at_offset(root, offset)?;
loop {
if let Some((label, is_footnote)) = extract_reference_target(&node) {
let definition = find_definition_node(root, &label, is_footnote)?;
return Some(definition.text_range());
}
node = node.parent()?;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(input: &str) -> SyntaxNode {
crate::parse(input, None)
}
#[test]
fn test_extract_symbol_text_range_for_inline_math_content() {
let input = "Text with $x^2$ inline math";
let root = parse(input);
let node = root
.descendants()
.find_map(InlineMath::cast)
.expect("inline math");
let range = extract_symbol_text_range(node.syntax()).expect("content range");
let start: usize = range.start().into();
let end: usize = range.end().into();
assert_eq!(&input[start..end], "x^2");
}
#[test]
fn test_extract_symbol_text_range_for_code_span_content() {
let input = "Use `fmt` command";
let root = parse(input);
let node = root
.descendants()
.find_map(CodeSpan::cast)
.expect("code span");
let range = extract_symbol_text_range(node.syntax()).expect("content range");
let start: usize = range.start().into();
let end: usize = range.end().into();
assert_eq!(&input[start..end], "fmt");
}
#[test]
fn test_find_node_at_offset() {
let root = parse("[text][ref]");
let node = find_node_at_offset(&root, 0);
assert!(node.is_some());
let node = find_node_at_offset(&root, 7);
assert!(node.is_some());
}
#[test]
fn test_normalize_label() {
assert_eq!(crate::utils::normalize_label("Foo"), "foo");
assert_eq!(crate::utils::normalize_label("foo bar"), "foo bar");
assert_eq!(crate::utils::normalize_label("foo bar"), "foo bar");
assert_eq!(crate::utils::normalize_label(" foo bar "), "foo bar");
}
#[test]
fn test_extract_reference_label_from_link_ref() {
let root = parse("[text][ref]");
let link_ref = root
.descendants()
.find_map(LinkRef::cast)
.expect("Should find LinkRef");
let (label, is_footnote) =
extract_reference_label(link_ref.syntax()).expect("Should extract label");
assert_eq!(label, "ref");
assert!(!is_footnote);
}
#[test]
fn test_extract_reference_label_from_footnote() {
let root = parse("[^1]");
let footnote_ref = root
.descendants()
.find_map(FootnoteReference::cast)
.expect("Should find FootnoteReference");
let (label, is_footnote) =
extract_reference_label(footnote_ref.syntax()).expect("Should extract label");
assert_eq!(label, "1");
assert!(is_footnote);
}
#[test]
fn test_extract_symbol_text_range_for_footnote_id_value() {
let input = "Text[^note] here";
let root = parse(input);
let node = root
.descendants()
.find_map(FootnoteReference::cast)
.expect("footnote reference");
let range = extract_symbol_text_range(node.syntax()).expect("id value range");
let start: usize = range.start().into();
let end: usize = range.end().into();
assert_eq!(&input[start..end], "note");
}
#[test]
fn test_extract_definition_label_from_reference() {
let root = parse("[ref]: /url");
let def = root
.descendants()
.find_map(crate::syntax::ReferenceDefinition::cast)
.expect("Should find ReferenceDefinition");
let label = extract_definition_label(def.syntax()).expect("Should extract label");
assert_eq!(label, "ref");
}
#[test]
fn test_extract_definition_label_from_footnote() {
let root = parse("[^1]: content");
let def = root
.descendants()
.find(|n| n.kind() == SyntaxKind::FOOTNOTE_DEFINITION)
.expect("Should find FootnoteDefinition");
let label = extract_definition_label(&def).expect("Should extract label");
assert_eq!(label, "1");
}
#[test]
fn test_find_definition_node_reference() {
let root = parse("[text][ref]\n\n[ref]: /url");
let def = find_definition_node(&root, "ref", false);
assert!(def.is_some());
assert_eq!(def.unwrap().kind(), SyntaxKind::REFERENCE_DEFINITION);
}
#[test]
fn test_find_definition_node_case_insensitive() {
let root = parse("[text][REF]\n\n[ref]: /url");
let def = find_definition_node(&root, "ref", false);
assert!(def.is_some());
}
#[test]
fn test_find_definition_node_footnote() {
let root = parse("Text[^1]\n\n[^1]: content");
let def = find_definition_node(&root, "1", true);
assert!(def.is_some());
assert_eq!(def.unwrap().kind(), SyntaxKind::FOOTNOTE_DEFINITION);
}
#[test]
fn test_find_definition_node_not_found() {
let root = parse("[text][ref]");
let def = find_definition_node(&root, "ref", false);
assert!(def.is_none());
}
#[test]
fn test_find_definition_at_offset_reference_link() {
let input = "[text][ref]\n\n[ref]: /url";
let root = parse(input);
let range = find_definition_at_offset(&root, 7);
assert!(range.is_some());
let range = range.unwrap();
let def_text = &input[range.start().into()..range.end().into()];
assert!(def_text.contains("[ref]: /url"));
}
#[test]
fn test_find_definition_at_offset_footnote() {
let input = "Text[^1]\n\n[^1]: content";
let root = parse(input);
let range = find_definition_at_offset(&root, 5);
assert!(range.is_some());
let range = range.unwrap();
let def_text = &input[range.start().into()..range.end().into()];
assert!(def_text.contains("[^1]:"));
}
#[test]
fn test_find_definition_at_offset_not_on_reference() {
let root = parse("Just some text");
let range = find_definition_at_offset(&root, 0);
assert!(range.is_none());
}
#[test]
fn test_find_definition_at_offset_reference_not_found() {
let root = parse("[text][ref]");
let range = find_definition_at_offset(&root, 7);
assert!(range.is_none());
}
#[test]
fn test_extract_citation_key_from_citation() {
let root = parse("Text @woodward1952 more text");
let citation = root
.descendants()
.find(|n| n.kind() == SyntaxKind::CITATION)
.expect("Should find CITATION");
let key = extract_citation_key(&citation).expect("Should extract citation key");
assert_eq!(key, "woodward1952");
}
#[test]
fn test_extract_citation_key_walks_up_tree() {
let root = parse("Text @woodward1952 more text");
let citation = root
.descendants()
.find(|n| n.kind() == SyntaxKind::CITATION)
.expect("Should find CITATION node");
let key = extract_citation_key(&citation).expect("Should extract citation key");
assert_eq!(key, "woodward1952");
}
#[test]
fn test_find_definition_whitespace_normalization() {
let input = "[text][foo bar]\n\n[foo bar]: /url";
let root = parse(input);
let range = find_definition_at_offset(&root, 7);
assert!(range.is_some());
}
#[test]
fn test_extract_example_label_target_at_offset() {
let config = crate::config::Config {
flavor: crate::config::Flavor::Pandoc,
extensions: crate::config::Extensions::for_flavor(crate::config::Flavor::Pandoc),
..Default::default()
};
let input = "(@good) Example.\n\nAs (@good) illustrates.\n";
let root = crate::parse(input, Some(config));
let offset = input.rfind("good").expect("reference label");
let label = extract_example_label_target_at_offset(&root, offset);
assert_eq!(label, Some("good".to_string()));
}
#[test]
fn test_extract_example_label_target_at_offset_ignores_citation() {
let root = parse("See @good for details.\n");
let offset = 5;
let label = extract_example_label_target_at_offset(&root, offset);
assert_eq!(label, None);
}
#[test]
fn test_example_label_range_at_offset() {
let config = crate::config::Config {
flavor: crate::config::Flavor::Pandoc,
extensions: crate::config::Extensions::for_flavor(crate::config::Flavor::Pandoc),
..Default::default()
};
let input = "As (@good) illustrates.\n";
let root = crate::parse(input, Some(config));
let offset = input.find("good").expect("label offset");
let range = example_label_range_at_offset(&root, offset).expect("label range");
let start: usize = range.start().into();
let end: usize = range.end().into();
assert_eq!(&input[start..end], "good");
}
}