mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Respect set rules in where selectors (#3290)
This commit is contained in:
parent
a1e8560ca6
commit
b744b87818
@ -93,7 +93,7 @@ impl Elem {
|
|||||||
|
|
||||||
/// Fields that are visible to the user.
|
/// Fields that are visible to the user.
|
||||||
fn visible_fields(&self) -> impl Iterator<Item = &Field> + Clone {
|
fn visible_fields(&self) -> impl Iterator<Item = &Field> + Clone {
|
||||||
self.real_fields().filter(|field| !field.internal && !field.ghost)
|
self.real_fields().filter(|field| !field.internal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -509,7 +509,11 @@ fn create_field_method(field: &Field) -> TokenStream {
|
|||||||
quote! { (&self, styles: #foundations::StyleChain) -> #output }
|
quote! { (&self, styles: #foundations::StyleChain) -> #output }
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut value = create_style_chain_access(field, quote! { self.#ident.as_ref() });
|
let mut value = create_style_chain_access(
|
||||||
|
field,
|
||||||
|
field.borrowed,
|
||||||
|
quote! { self.#ident.as_ref() },
|
||||||
|
);
|
||||||
if field.resolve {
|
if field.resolve {
|
||||||
value = quote! { #foundations::Resolve::resolve(#value, styles) };
|
value = quote! { #foundations::Resolve::resolve(#value, styles) };
|
||||||
}
|
}
|
||||||
@ -530,7 +534,7 @@ fn create_field_in_method(field: &Field) -> TokenStream {
|
|||||||
|
|
||||||
let ref_ = field.borrowed.then(|| quote! { & });
|
let ref_ = field.borrowed.then(|| quote! { & });
|
||||||
|
|
||||||
let mut value = create_style_chain_access(field, quote! { None });
|
let mut value = create_style_chain_access(field, field.borrowed, quote! { None });
|
||||||
if field.resolve {
|
if field.resolve {
|
||||||
value = quote! { #foundations::Resolve::resolve(#value, styles) };
|
value = quote! { #foundations::Resolve::resolve(#value, styles) };
|
||||||
}
|
}
|
||||||
@ -560,16 +564,20 @@ fn create_set_field_method(field: &Field) -> TokenStream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a style chain access method for a field.
|
/// Create a style chain access method for a field.
|
||||||
fn create_style_chain_access(field: &Field, inherent: TokenStream) -> TokenStream {
|
fn create_style_chain_access(
|
||||||
|
field: &Field,
|
||||||
|
borrowed: bool,
|
||||||
|
inherent: TokenStream,
|
||||||
|
) -> TokenStream {
|
||||||
let Field { ty, default, enum_ident, const_ident, .. } = field;
|
let Field { ty, default, enum_ident, const_ident, .. } = field;
|
||||||
|
|
||||||
let getter = match (field.fold, field.borrowed) {
|
let getter = match (field.fold, borrowed) {
|
||||||
(false, false) => quote! { get },
|
(false, false) => quote! { get },
|
||||||
(false, true) => quote! { get_ref },
|
(false, true) => quote! { get_ref },
|
||||||
(true, _) => quote! { get_folded },
|
(true, _) => quote! { get_folded },
|
||||||
};
|
};
|
||||||
|
|
||||||
let default = if field.borrowed {
|
let default = if borrowed {
|
||||||
quote! { || &#const_ident }
|
quote! { || &#const_ident }
|
||||||
} else {
|
} else {
|
||||||
match default {
|
match default {
|
||||||
@ -821,9 +829,10 @@ fn create_capable_impl(element: &Elem) -> TokenStream {
|
|||||||
/// Creates the element's `Fields` implementation.
|
/// Creates the element's `Fields` implementation.
|
||||||
fn create_fields_impl(element: &Elem) -> TokenStream {
|
fn create_fields_impl(element: &Elem) -> TokenStream {
|
||||||
let into_value = quote! { #foundations::IntoValue::into_value };
|
let into_value = quote! { #foundations::IntoValue::into_value };
|
||||||
|
let visible_non_ghost = || element.visible_fields().filter(|field| !field.ghost);
|
||||||
|
|
||||||
// Fields that can be checked using the `has` method.
|
// Fields that can be checked using the `has` method.
|
||||||
let has_arms = element.visible_fields().map(|field| {
|
let has_arms = visible_non_ghost().map(|field| {
|
||||||
let Field { enum_ident, ident, .. } = field;
|
let Field { enum_ident, ident, .. } = field;
|
||||||
|
|
||||||
let expr = if field.inherent() {
|
let expr = if field.inherent() {
|
||||||
@ -836,7 +845,7 @@ fn create_fields_impl(element: &Elem) -> TokenStream {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Fields that can be accessed using the `field` method.
|
// Fields that can be accessed using the `field` method.
|
||||||
let field_arms = element.visible_fields().map(|field| {
|
let field_arms = visible_non_ghost().filter(|field| !field.ghost).map(|field| {
|
||||||
let Field { enum_ident, ident, .. } = field;
|
let Field { enum_ident, ident, .. } = field;
|
||||||
|
|
||||||
let expr = if field.inherent() {
|
let expr = if field.inherent() {
|
||||||
@ -848,8 +857,29 @@ fn create_fields_impl(element: &Elem) -> TokenStream {
|
|||||||
quote! { Fields::#enum_ident => #expr }
|
quote! { Fields::#enum_ident => #expr }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Fields that can be accessed using the `field_with_styles` method.
|
||||||
|
let field_with_styles_arms = element.visible_fields().map(|field| {
|
||||||
|
let Field { enum_ident, ident, .. } = field;
|
||||||
|
|
||||||
|
let expr = if field.inherent() {
|
||||||
|
quote! { Some(#into_value(self.#ident.clone())) }
|
||||||
|
} else if field.synthesized && field.default.is_none() {
|
||||||
|
quote! { self.#ident.clone().map(#into_value) }
|
||||||
|
} else {
|
||||||
|
let value = create_style_chain_access(
|
||||||
|
field,
|
||||||
|
false,
|
||||||
|
if field.ghost { quote!(None) } else { quote!(self.#ident.as_ref()) },
|
||||||
|
);
|
||||||
|
|
||||||
|
quote! { Some(#into_value(#value)) }
|
||||||
|
};
|
||||||
|
|
||||||
|
quote! { Fields::#enum_ident => #expr }
|
||||||
|
});
|
||||||
|
|
||||||
// Creation of the `fields` dictionary for inherent fields.
|
// Creation of the `fields` dictionary for inherent fields.
|
||||||
let field_inserts = element.visible_fields().map(|field| {
|
let field_inserts = visible_non_ghost().map(|field| {
|
||||||
let Field { ident, name, .. } = field;
|
let Field { ident, name, .. } = field;
|
||||||
let string = quote! { #name.into() };
|
let string = quote! { #name.into() };
|
||||||
|
|
||||||
@ -873,7 +903,7 @@ fn create_fields_impl(element: &Elem) -> TokenStream {
|
|||||||
type Enum = Fields;
|
type Enum = Fields;
|
||||||
|
|
||||||
fn has(&self, id: u8) -> bool {
|
fn has(&self, id: u8) -> bool {
|
||||||
let Ok(id) = <#ident as #foundations::Fields>::Enum::try_from(id) else {
|
let Ok(id) = Fields::try_from(id) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -884,13 +914,21 @@ fn create_fields_impl(element: &Elem) -> TokenStream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn field(&self, id: u8) -> Option<#foundations::Value> {
|
fn field(&self, id: u8) -> Option<#foundations::Value> {
|
||||||
let id = <#ident as #foundations::Fields>::Enum::try_from(id).ok()?;
|
let id = Fields::try_from(id).ok()?;
|
||||||
match id {
|
match id {
|
||||||
#(#field_arms,)*
|
#(#field_arms,)*
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn field_with_styles(&self, id: u8, styles: #foundations::StyleChain) -> Option<#foundations::Value> {
|
||||||
|
let id = Fields::try_from(id).ok()?;
|
||||||
|
match id {
|
||||||
|
#(#field_with_styles_arms,)*
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn fields(&self) -> #foundations::Dict {
|
fn fields(&self) -> #foundations::Dict {
|
||||||
let mut fields = #foundations::Dict::new();
|
let mut fields = #foundations::Dict::new();
|
||||||
#(#field_inserts)*
|
#(#field_inserts)*
|
||||||
|
@ -15,7 +15,8 @@ use crate::diag::{SourceResult, StrResult};
|
|||||||
use crate::engine::Engine;
|
use crate::engine::Engine;
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
elem, func, scope, ty, Dict, Element, Fields, Finalize, Guard, IntoValue, Label,
|
elem, func, scope, ty, Dict, Element, Fields, Finalize, Guard, IntoValue, Label,
|
||||||
NativeElement, Recipe, Repr, Selector, Str, Style, Styles, Synthesize, Value,
|
NativeElement, Recipe, Repr, Selector, Str, Style, StyleChain, Styles, Synthesize,
|
||||||
|
Value,
|
||||||
};
|
};
|
||||||
use crate::introspection::{Locatable, Location, Meta, MetaElem};
|
use crate::introspection::{Locatable, Location, Meta, MetaElem};
|
||||||
use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides};
|
use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides};
|
||||||
@ -181,13 +182,16 @@ impl Content {
|
|||||||
/// This is the preferred way to access fields. However, you can only use it
|
/// This is the preferred way to access fields. However, you can only use it
|
||||||
/// if you have set the field IDs yourself or are using the field IDs
|
/// if you have set the field IDs yourself or are using the field IDs
|
||||||
/// generated by the `#[elem]` macro.
|
/// generated by the `#[elem]` macro.
|
||||||
pub fn get(&self, id: u8) -> Option<Value> {
|
pub fn get(&self, id: u8, styles: Option<StyleChain>) -> Option<Value> {
|
||||||
if id == 255 {
|
if id == 255 {
|
||||||
if let Some(label) = self.label() {
|
if let Some(label) = self.label() {
|
||||||
return Some(label.into_value());
|
return Some(label.into_value());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.inner.elem.field(id)
|
match styles {
|
||||||
|
Some(styles) => self.inner.elem.field_with_styles(id, styles),
|
||||||
|
None => self.inner.elem.field(id),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a field by name.
|
/// Get a field by name.
|
||||||
@ -201,7 +205,7 @@ impl Content {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let id = self.elem().field_id(name)?;
|
let id = self.elem().field_id(name)?;
|
||||||
self.get(id)
|
self.get(id, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a field by ID, returning a missing field error if it does not exist.
|
/// Get a field by ID, returning a missing field error if it does not exist.
|
||||||
@ -210,7 +214,7 @@ impl Content {
|
|||||||
/// if you have set the field IDs yourself or are using the field IDs
|
/// if you have set the field IDs yourself or are using the field IDs
|
||||||
/// generated by the `#[elem]` macro.
|
/// generated by the `#[elem]` macro.
|
||||||
pub fn field(&self, id: u8) -> StrResult<Value> {
|
pub fn field(&self, id: u8) -> StrResult<Value> {
|
||||||
self.get(id)
|
self.get(id, None)
|
||||||
.ok_or_else(|| missing_field(self.elem().field_name(id).unwrap()))
|
.ok_or_else(|| missing_field(self.elem().field_name(id).unwrap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -400,7 +404,7 @@ impl Content {
|
|||||||
pub fn query(&self, selector: Selector) -> Vec<Content> {
|
pub fn query(&self, selector: Selector) -> Vec<Content> {
|
||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
self.traverse(&mut |element| {
|
self.traverse(&mut |element| {
|
||||||
if selector.matches(&element) {
|
if selector.matches(&element, None) {
|
||||||
results.push(element);
|
results.push(element);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -414,7 +418,7 @@ impl Content {
|
|||||||
pub fn query_first(&self, selector: Selector) -> Option<Content> {
|
pub fn query_first(&self, selector: Selector) -> Option<Content> {
|
||||||
let mut result = None;
|
let mut result = None;
|
||||||
self.traverse(&mut |element| {
|
self.traverse(&mut |element| {
|
||||||
if result.is_none() && selector.matches(&element) {
|
if result.is_none() && selector.matches(&element, None) {
|
||||||
result = Some(element);
|
result = Some(element);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -220,6 +220,9 @@ pub trait Fields {
|
|||||||
/// Get the field with the given field ID.
|
/// Get the field with the given field ID.
|
||||||
fn field(&self, id: u8) -> Option<Value>;
|
fn field(&self, id: u8) -> Option<Value>;
|
||||||
|
|
||||||
|
/// Get the field with the given ID in the presence of styles.
|
||||||
|
fn field_with_styles(&self, id: u8, styles: StyleChain) -> Option<Value>;
|
||||||
|
|
||||||
/// Get the fields of the element.
|
/// Get the fields of the element.
|
||||||
fn fields(&self) -> Dict;
|
fn fields(&self) -> Dict;
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ use smallvec::SmallVec;
|
|||||||
use crate::diag::{bail, StrResult};
|
use crate::diag::{bail, StrResult};
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
cast, func, repr, scope, ty, CastInfo, Content, Dict, Element, FromValue, Func,
|
cast, func, repr, scope, ty, CastInfo, Content, Dict, Element, FromValue, Func,
|
||||||
Label, Reflect, Regex, Repr, Str, Type, Value,
|
Label, Reflect, Regex, Repr, Str, StyleChain, Type, Value,
|
||||||
};
|
};
|
||||||
use crate::introspection::{Locatable, Location};
|
use crate::introspection::{Locatable, Location};
|
||||||
use crate::symbols::Symbol;
|
use crate::symbols::Symbol;
|
||||||
@ -128,23 +128,26 @@ impl Selector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the selector matches for the target.
|
/// Whether the selector matches for the target.
|
||||||
pub fn matches(&self, target: &Content) -> bool {
|
pub fn matches(&self, target: &Content, styles: Option<StyleChain>) -> bool {
|
||||||
// TODO: optimize field access to not clone.
|
|
||||||
match self {
|
match self {
|
||||||
Self::Elem(element, dict) => {
|
Self::Elem(element, dict) => {
|
||||||
|
// TODO: Optimize field access to not clone.
|
||||||
target.func() == *element
|
target.func() == *element
|
||||||
&& dict
|
&& dict.iter().flat_map(|dict| dict.iter()).all(|(id, value)| {
|
||||||
.iter()
|
target.get(*id, styles).as_ref() == Some(value)
|
||||||
.flat_map(|dict| dict.iter())
|
})
|
||||||
.all(|(id, value)| target.get(*id).as_ref() == Some(value))
|
|
||||||
}
|
}
|
||||||
Self::Label(label) => target.label() == Some(*label),
|
Self::Label(label) => target.label() == Some(*label),
|
||||||
Self::Regex(regex) => target
|
Self::Regex(regex) => target
|
||||||
.to_packed::<TextElem>()
|
.to_packed::<TextElem>()
|
||||||
.map_or(false, |elem| regex.is_match(elem.text())),
|
.map_or(false, |elem| regex.is_match(elem.text())),
|
||||||
Self::Can(cap) => target.func().can_type_id(*cap),
|
Self::Can(cap) => target.func().can_type_id(*cap),
|
||||||
Self::Or(selectors) => selectors.iter().any(move |sel| sel.matches(target)),
|
Self::Or(selectors) => {
|
||||||
Self::And(selectors) => selectors.iter().all(move |sel| sel.matches(target)),
|
selectors.iter().any(move |sel| sel.matches(target, styles))
|
||||||
|
}
|
||||||
|
Self::And(selectors) => {
|
||||||
|
selectors.iter().all(move |sel| sel.matches(target, styles))
|
||||||
|
}
|
||||||
Self::Location(location) => target.location() == Some(*location),
|
Self::Location(location) => target.location() == Some(*location),
|
||||||
// Not supported here.
|
// Not supported here.
|
||||||
Self::Before { .. } | Self::After { .. } => false,
|
Self::Before { .. } | Self::After { .. } => false,
|
||||||
|
@ -369,10 +369,10 @@ impl Recipe {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the recipe is applicable to the target.
|
/// Whether the recipe is applicable to the target.
|
||||||
pub fn applicable(&self, target: &Content) -> bool {
|
pub fn applicable(&self, target: &Content, styles: StyleChain) -> bool {
|
||||||
self.selector
|
self.selector
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(false, |selector| selector.matches(target))
|
.map_or(false, |selector| selector.matches(target, Some(styles)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply the recipe to the given content.
|
/// Apply the recipe to the given content.
|
||||||
|
@ -127,9 +127,11 @@ impl Introspector {
|
|||||||
indices.iter().map(|&index| self.elems[index].0.clone()).collect()
|
indices.iter().map(|&index| self.elems[index].0.clone()).collect()
|
||||||
})
|
})
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
Selector::Elem(..) | Selector::Regex(_) | Selector::Can(_) => {
|
Selector::Elem(..) | Selector::Regex(_) | Selector::Can(_) => self
|
||||||
self.all().filter(|elem| selector.matches(elem)).cloned().collect()
|
.all()
|
||||||
}
|
.filter(|elem| selector.matches(elem, None))
|
||||||
|
.cloned()
|
||||||
|
.collect(),
|
||||||
Selector::Location(location) => {
|
Selector::Location(location) => {
|
||||||
self.get(location).cloned().into_iter().collect()
|
self.get(location).cloned().into_iter().collect()
|
||||||
}
|
}
|
||||||
|
@ -80,7 +80,7 @@ pub fn applicable(target: &Content, styles: StyleChain) -> bool {
|
|||||||
|
|
||||||
// Find out whether any recipe matches and is unguarded.
|
// Find out whether any recipe matches and is unguarded.
|
||||||
for recipe in styles.recipes() {
|
for recipe in styles.recipes() {
|
||||||
if !target.is_guarded(Guard(n)) && recipe.applicable(target) {
|
if !target.is_guarded(Guard(n)) && recipe.applicable(target, styles) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
n -= 1;
|
n -= 1;
|
||||||
@ -133,7 +133,7 @@ pub fn realize(
|
|||||||
// Find an applicable show rule recipe.
|
// Find an applicable show rule recipe.
|
||||||
for recipe in styles.recipes() {
|
for recipe in styles.recipes() {
|
||||||
let guard = Guard(n);
|
let guard = Guard(n);
|
||||||
if !target.is_guarded(guard) && recipe.applicable(target) {
|
if !target.is_guarded(guard) && recipe.applicable(target, styles) {
|
||||||
if let Some(content) = try_apply(engine, target, recipe, guard)? {
|
if let Some(content) = try_apply(engine, target, recipe, guard)? {
|
||||||
return Ok(Some(content));
|
return Ok(Some(content));
|
||||||
}
|
}
|
||||||
|
BIN
tests/ref/compiler/select-where-styles.png
Normal file
BIN
tests/ref/compiler/select-where-styles.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
91
tests/typ/compiler/select-where-styles.typ
Normal file
91
tests/typ/compiler/select-where-styles.typ
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
// Test that where selectors also work with settable fields.
|
||||||
|
|
||||||
|
---
|
||||||
|
// Test that where selectors also trigger on set rule fields.
|
||||||
|
#show raw.where(block: false): box.with(
|
||||||
|
fill: luma(220),
|
||||||
|
inset: (x: 3pt, y: 0pt),
|
||||||
|
outset: (y: 3pt),
|
||||||
|
radius: 2pt,
|
||||||
|
)
|
||||||
|
|
||||||
|
This is #raw("fn main() {}") some text.
|
||||||
|
|
||||||
|
---
|
||||||
|
// Note: This show rule is horribly inefficient because it triggers for
|
||||||
|
// every individual text element. But it should still work.
|
||||||
|
#show text.where(lang: "de"): set text(red)
|
||||||
|
|
||||||
|
#set text(lang: "es")
|
||||||
|
Hola, mundo!
|
||||||
|
|
||||||
|
#set text(lang: "de")
|
||||||
|
Hallo Welt!
|
||||||
|
|
||||||
|
#set text(lang: "en")
|
||||||
|
Hello World!
|
||||||
|
|
||||||
|
---
|
||||||
|
// Test that folding is taken into account.
|
||||||
|
#set text(5pt)
|
||||||
|
#set text(2em)
|
||||||
|
|
||||||
|
#[
|
||||||
|
#show text.where(size: 2em): set text(blue)
|
||||||
|
2em not blue
|
||||||
|
]
|
||||||
|
|
||||||
|
#[
|
||||||
|
#show text.where(size: 10pt): set text(blue)
|
||||||
|
10pt blue
|
||||||
|
]
|
||||||
|
|
||||||
|
---
|
||||||
|
// Test again that folding is taken into account.
|
||||||
|
#set rect(width: 40pt, height: 10pt)
|
||||||
|
#set rect(stroke: blue)
|
||||||
|
#set rect(stroke: 2pt)
|
||||||
|
|
||||||
|
#{
|
||||||
|
show rect.where(stroke: blue): "Not Triggered"
|
||||||
|
rect()
|
||||||
|
}
|
||||||
|
#{
|
||||||
|
show rect.where(stroke: 2pt): "Not Triggered"
|
||||||
|
rect()
|
||||||
|
}
|
||||||
|
#{
|
||||||
|
show rect.where(stroke: 2pt + blue): "Triggered"
|
||||||
|
rect()
|
||||||
|
}
|
||||||
|
|
||||||
|
---
|
||||||
|
// Test that resolving is *not* taken into account.
|
||||||
|
#set line(start: (1em, 1em + 2pt))
|
||||||
|
|
||||||
|
#{
|
||||||
|
show line.where(start: (1em, 1em + 2pt)): "Triggered"
|
||||||
|
line()
|
||||||
|
}
|
||||||
|
#{
|
||||||
|
show line.where(start: (10pt, 12pt)): "Not Triggered"
|
||||||
|
line()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
// Test again that resolving is *not* taken into account.
|
||||||
|
#set text(hyphenate: auto)
|
||||||
|
|
||||||
|
#[
|
||||||
|
#show text.where(hyphenate: auto): underline
|
||||||
|
Auto
|
||||||
|
]
|
||||||
|
#[
|
||||||
|
#show text.where(hyphenate: true): underline
|
||||||
|
True
|
||||||
|
]
|
||||||
|
#[
|
||||||
|
#show text.where(hyphenate: false): underline
|
||||||
|
False
|
||||||
|
]
|
Loading…
x
Reference in New Issue
Block a user