use std::ops::Range;
use crate::policy::{Policy, SingletonRule};
use crate::scanner::{
EmojiLike, EmojiModification, EmojiSequence, EmojiStem, Presentation, ScanItem, ScanKind,
ZwjJoinedEmoji, ZwjLink,
};
use crate::unicode;
mod fixed;
use fixed::FixedEmojiLike;
#[cfg(test)]
mod tests;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Violation {
UnsanctionedSelectorsOnly,
Primary(PrimaryViolation),
}
impl Violation {
const fn primary(kind: PrimaryViolationKind, has_unsanctioned_selectors: bool) -> Self {
Self::Primary(PrimaryViolation::new(kind, has_unsanctioned_selectors))
}
const fn with_unsanctioned_selectors(self) -> Self {
match self {
Self::UnsanctionedSelectorsOnly => Self::UnsanctionedSelectorsOnly,
Self::Primary(primary) => Self::Primary(PrimaryViolation {
has_unsanctioned_selectors: true,
..primary
}),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct PrimaryViolation {
pub kind: PrimaryViolationKind,
pub has_unsanctioned_selectors: bool,
}
impl PrimaryViolation {
const fn new(kind: PrimaryViolationKind, has_unsanctioned_selectors: bool) -> Self {
Self {
kind,
has_unsanctioned_selectors,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PrimaryViolationKind {
NotFullyQualifiedSequence,
RedundantSelector,
BareNeedsResolution,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ReplacementDecision {
Text,
Emoji,
}
impl ReplacementDecision {
const fn from_presentation(presentation: Presentation) -> Self {
match presentation {
Presentation::Text => Self::Text,
Presentation::Emoji => Self::Emoji,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DecisionSlot {
choices: Vec<ReplacementDecision>,
default: ReplacementDecision,
}
impl DecisionSlot {
#[must_use]
pub fn choices(&self) -> &[ReplacementDecision] {
&self.choices
}
#[must_use]
pub const fn default_decision(&self) -> ReplacementDecision {
self.default
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct SlotReplacement {
decision: ReplacementDecision,
replacement: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ReplacementSlotPlan {
public: DecisionSlot,
replacements: Vec<SlotReplacement>,
}
impl ReplacementSlotPlan {
fn replacement(&self, decision: ReplacementDecision) -> Option<&str> {
self.replacements
.iter()
.find(|replacement| replacement.decision == decision)
.map(|replacement| replacement.replacement.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum ReplacementPiece {
Literal(String),
Slot(usize),
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ReplacementPlan {
violation: Violation,
decision_slots: Vec<DecisionSlot>,
slots: Vec<ReplacementSlotPlan>,
pieces: Vec<ReplacementPiece>,
default_replacement: String,
}
impl ReplacementPlan {
#[must_use]
const fn violation(&self) -> Violation {
self.violation
}
#[must_use]
fn decision_slots(&self) -> &[DecisionSlot] {
&self.decision_slots
}
#[must_use]
fn default_decision(&self) -> Vec<ReplacementDecision> {
self.slots
.iter()
.map(|slot| slot.public.default_decision())
.collect()
}
#[must_use]
fn default_replacement(&self) -> &str {
&self.default_replacement
}
fn render_replacement(&self, decisions: &[ReplacementDecision]) -> Option<String> {
if decisions.len() != self.slots.len() {
return None;
}
let mut out = String::new();
for piece in &self.pieces {
match piece {
ReplacementPiece::Literal(text) => out.push_str(text),
ReplacementPiece::Slot(slot_index) => {
let decision = decisions.get(*slot_index).copied()?;
out.push_str(self.slots.get(*slot_index)?.replacement(decision)?);
}
}
}
Some(out)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Finding<'a> {
pub span: Range<usize>,
pub raw: &'a str,
replacement_plan: ReplacementPlan,
}
impl Finding<'_> {
#[must_use]
pub const fn violation(&self) -> Violation {
self.replacement_plan.violation()
}
#[must_use]
pub fn decision_slots(&self) -> &[DecisionSlot] {
self.replacement_plan.decision_slots()
}
#[must_use]
pub fn default_decision(&self) -> Vec<ReplacementDecision> {
self.replacement_plan.default_decision()
}
#[must_use]
pub fn default_replacement(&self) -> &str {
self.replacement_plan.default_replacement()
}
#[must_use]
pub fn replacement(&self, decision: &[ReplacementDecision]) -> Option<String> {
self.replacement_plan.render_replacement(decision)
}
}
#[must_use]
pub fn analyze_scan_item<'a>(item: &ScanItem<'a>, policy: &Policy) -> Option<Finding<'a>> {
match &item.kind {
ScanKind::Passthrough => None,
ScanKind::UnsanctionedPresentationSelectors(_) => Some(unambiguous_finding(
item,
Violation::UnsanctionedSelectorsOnly,
String::new(),
)),
ScanKind::EmojiSequence(sequence) => match sequence {
EmojiSequence::LinksOnly(links) => analyze_links_only_zwj_sequence(item, links),
EmojiSequence::EmojiHeaded {
first,
joined,
trailing_links,
} if joined.is_empty() => analyze_single_emoji(item, first, trailing_links, policy),
EmojiSequence::EmojiHeaded {
first,
joined,
trailing_links,
} => analyze_multi_emoji_zwj_sequence(item, first, joined, trailing_links, policy),
},
}
}
fn analyze_single_emoji<'a>(
item: &ScanItem<'a>,
emoji: &EmojiLike,
trailing_links: &[ZwjLink],
policy: &Policy,
) -> Option<Finding<'a>> {
match &emoji.stem {
EmojiStem::SingletonBase {
base,
presentation_selectors_after_base,
} => {
let outcome = singleton_analysis_outcome(
*base,
presentation_selectors_after_base,
&emoji.modifiers,
policy,
);
single_emoji_singleton_finding(
item,
*base,
presentation_selectors_after_base,
&emoji.modifiers,
trailing_links,
outcome,
)
}
EmojiStem::Flag {
first_ri,
presentation_selectors_after_first_ri,
second_ri,
presentation_selectors_after_second_ri,
} => analyze_single_flag(
item,
*first_ri,
presentation_selectors_after_first_ri,
*second_ri,
presentation_selectors_after_second_ri,
&emoji.modifiers,
trailing_links,
),
}
}
fn analyze_single_flag<'a>(
item: &ScanItem<'a>,
first_ri: char,
presentation_selectors_after_first_ri: &[Presentation],
second_ri: char,
presentation_selectors_after_second_ri: &[Presentation],
modifications: &[EmojiModification],
trailing_links: &[ZwjLink],
) -> Option<Finding<'a>> {
let has_ri_presentation_selectors = !presentation_selectors_after_first_ri.is_empty()
|| !presentation_selectors_after_second_ri.is_empty();
let has_modification_presentation_selectors =
has_trailing_modification_presentation_selectors(modifications);
let has_link_presentation_selectors = trailing_links.iter().any(zwj_link_has_selectors);
if !has_ri_presentation_selectors
&& !has_modification_presentation_selectors
&& !has_link_presentation_selectors
{
return None;
}
Some(unambiguous_finding(
item,
Violation::UnsanctionedSelectorsOnly,
render_flag_with_links(first_ri, second_ri, modifications, trailing_links),
))
}
fn analyze_links_only_zwj_sequence<'a>(
item: &ScanItem<'a>,
links: &[ZwjLink],
) -> Option<Finding<'a>> {
let has_link_presentation_selectors = links.iter().any(zwj_link_has_selectors);
if !has_link_presentation_selectors {
return None;
}
Some(unambiguous_finding(
item,
Violation::UnsanctionedSelectorsOnly,
render_zwj_links_only_sequence(links),
))
}
fn single_emoji_singleton_finding<'a>(
item: &ScanItem<'a>,
base: char,
presentation_selectors_after_base: &[Presentation],
modifications: &[EmojiModification],
trailing_links: &[ZwjLink],
outcome: SingletonAnalysisOutcome,
) -> Option<Finding<'a>> {
let has_link_presentation_selectors = trailing_links.iter().any(zwj_link_has_selectors);
match outcome {
SingletonAnalysisOutcome::Canonical if !has_link_presentation_selectors => None,
SingletonAnalysisOutcome::Canonical => {
debug_assert!(
presentation_selectors_after_base.len() <= 1,
"canonical singleton base cannot carry multiple presentation selectors"
);
Some(unambiguous_finding(
item,
Violation::UnsanctionedSelectorsOnly,
render_singleton_with_links(
base,
presentation_selectors_after_base.first().copied(),
modifications,
trailing_links,
),
))
}
SingletonAnalysisOutcome::Repair {
canonical_presentation,
violation,
} => Some(unambiguous_finding(
item,
if has_link_presentation_selectors {
violation.with_unsanctioned_selectors()
} else {
violation
},
render_singleton_with_links(
base,
canonical_presentation,
modifications,
trailing_links,
),
)),
SingletonAnalysisOutcome::ResolvePresentation { default } if trailing_links.is_empty() => {
Some(resolve_presentation_finding(
item,
base,
modifications,
default,
))
}
SingletonAnalysisOutcome::ResolvePresentation { default } => {
Some(resolve_zwj_wrapper_presentation_finding(
item,
base,
modifications,
trailing_links,
default,
has_link_presentation_selectors,
))
}
}
}
fn analyze_multi_emoji_zwj_sequence<'a>(
item: &ScanItem<'a>,
first: &EmojiLike,
joined: &[ZwjJoinedEmoji],
trailing_links: &[ZwjLink],
policy: &Policy,
) -> Option<Finding<'a>> {
let mut builder = ReplacementPlanBuilder::new();
let mut has_unsanctioned_selectors = false;
let mut has_noncanonical_component = false;
let mut has_resolution_slot = false;
let first_outcome = zwj_component_outcome(first, policy);
has_unsanctioned_selectors |= first_outcome.has_unsanctioned_selectors;
has_noncanonical_component |= first_outcome.is_noncanonical;
has_resolution_slot |= first_outcome.has_resolution_slot;
builder.extend(first_outcome.pieces, first_outcome.slots);
for joined in joined {
if zwj_link_has_selectors(&joined.link) {
has_unsanctioned_selectors = true;
}
builder.push_literal(unicode::ZWJ.to_string());
let joined_outcome = zwj_component_outcome(&joined.emoji, policy);
has_unsanctioned_selectors |= joined_outcome.has_unsanctioned_selectors;
has_noncanonical_component |= joined_outcome.is_noncanonical;
has_resolution_slot |= joined_outcome.has_resolution_slot;
builder.extend(joined_outcome.pieces, joined_outcome.slots);
}
if trailing_links.iter().any(zwj_link_has_selectors) {
has_unsanctioned_selectors = true;
}
for _ in trailing_links {
builder.push_literal(unicode::ZWJ.to_string());
}
if !has_noncanonical_component && !has_unsanctioned_selectors {
return None;
}
let violation = if has_resolution_slot {
Violation::primary(
PrimaryViolationKind::BareNeedsResolution,
has_unsanctioned_selectors,
)
} else if has_noncanonical_component {
Violation::primary(
PrimaryViolationKind::NotFullyQualifiedSequence,
has_unsanctioned_selectors,
)
} else {
Violation::UnsanctionedSelectorsOnly
};
Some(finding_from_builder(item, violation, builder))
}
fn zwj_link_has_selectors(link: &ZwjLink) -> bool {
!link.presentation_selectors_after_link.is_empty()
}
struct ZwjComponentOutcome {
pieces: Vec<ComponentReplacementPiece>,
slots: Vec<ReplacementSlotPlan>,
is_noncanonical: bool,
has_unsanctioned_selectors: bool,
has_resolution_slot: bool,
}
enum ComponentReplacementPiece {
Literal(String),
Slot(usize),
}
fn zwj_component_outcome(emoji: &EmojiLike, policy: &Policy) -> ZwjComponentOutcome {
match &emoji.stem {
EmojiStem::SingletonBase {
base,
presentation_selectors_after_base,
} => zwj_singleton_component_outcome(
*base,
presentation_selectors_after_base,
&emoji.modifiers,
policy,
),
EmojiStem::Flag {
first_ri,
presentation_selectors_after_first_ri,
second_ri,
presentation_selectors_after_second_ri,
} => {
let has_unsanctioned_selectors = !presentation_selectors_after_first_ri.is_empty()
|| !presentation_selectors_after_second_ri.is_empty()
|| has_trailing_modification_presentation_selectors(&emoji.modifiers);
ZwjComponentOutcome {
pieces: vec![ComponentReplacementPiece::Literal(
FixedEmojiLike::flag(*first_ri, *second_ri, &emoji.modifiers)
.render_to_string(),
)],
slots: vec![],
is_noncanonical: has_unsanctioned_selectors,
has_unsanctioned_selectors,
has_resolution_slot: false,
}
}
}
}
fn zwj_singleton_component_outcome(
base: char,
presentation_selectors_after_base: &[Presentation],
modifications: &[EmojiModification],
policy: &Policy,
) -> ZwjComponentOutcome {
match singleton_analysis_outcome(
base,
presentation_selectors_after_base,
modifications,
policy,
) {
SingletonAnalysisOutcome::Canonical => {
let presentation = presentation_selectors_after_base.first().copied();
ZwjComponentOutcome {
pieces: vec![ComponentReplacementPiece::Literal(render_singleton(
base,
presentation,
modifications,
))],
slots: vec![],
is_noncanonical: false,
has_unsanctioned_selectors: false,
has_resolution_slot: false,
}
}
SingletonAnalysisOutcome::Repair {
canonical_presentation,
violation,
} => ZwjComponentOutcome {
pieces: vec![ComponentReplacementPiece::Literal(render_singleton(
base,
canonical_presentation,
modifications,
))],
slots: vec![],
is_noncanonical: true,
has_unsanctioned_selectors: matches!(violation, Violation::UnsanctionedSelectorsOnly)
|| matches!(
violation,
Violation::Primary(PrimaryViolation {
has_unsanctioned_selectors: true,
..
})
),
has_resolution_slot: false,
},
SingletonAnalysisOutcome::ResolvePresentation { default } => {
let slot = presentation_slot(
ReplacementDecision::from_presentation(default),
[
(
ReplacementDecision::Text,
render_singleton(base, Some(Presentation::Text), modifications),
),
(
ReplacementDecision::Emoji,
render_singleton(base, Some(Presentation::Emoji), modifications),
),
],
);
ZwjComponentOutcome {
pieces: vec![ComponentReplacementPiece::Slot(0)],
slots: vec![slot],
is_noncanonical: true,
has_unsanctioned_selectors: false,
has_resolution_slot: true,
}
}
}
}
fn render_zwj_links_only_sequence(links: &[ZwjLink]) -> String {
let mut out = String::new();
render_zwj_links(&mut out, links);
out
}
fn render_zwj_links(out: &mut String, links: &[ZwjLink]) {
for _ in links {
out.push(unicode::ZWJ);
}
}
#[derive(Clone, Copy)]
enum SingletonAnalysisOutcome {
Canonical,
Repair {
canonical_presentation: Option<Presentation>,
violation: Violation,
},
ResolvePresentation { default: Presentation },
}
fn singleton_analysis_outcome(
base: char,
presentation_selectors_after_base: &[Presentation],
modifications: &[EmojiModification],
policy: &Policy,
) -> SingletonAnalysisOutcome {
match modifications.first() {
Some(EmojiModification::EmojiModifier { .. }) => {
singleton_fixed_cleanup_outcome(presentation_selectors_after_base, modifications, None)
}
Some(EmojiModification::TagModifier(_)) => singleton_fixed_cleanup_outcome(
presentation_selectors_after_base,
modifications,
if unicode::is_emoji_default(base) {
None
} else {
sanctioned_presentation(base, Presentation::Emoji)
},
),
first_modification => standalone_singleton_analysis_outcome(
base,
presentation_selectors_after_base,
unicode::has_variation_sequence(base),
matches!(
first_modification,
Some(EmojiModification::EnclosingKeycap { .. })
),
policy,
),
}
}
fn sanctioned_presentation(base: char, presentation: Presentation) -> Option<Presentation> {
unicode::has_variation_sequence(base).then_some(presentation)
}
fn singleton_fixed_cleanup_outcome(
presentation_selectors_after_base: &[Presentation],
modifications: &[EmojiModification],
canonical_presentation: Option<Presentation>,
) -> SingletonAnalysisOutcome {
if singleton_base_presentation_is_canonical(
presentation_selectors_after_base,
canonical_presentation,
) && !has_trailing_modification_presentation_selectors(modifications)
{
return SingletonAnalysisOutcome::Canonical;
}
SingletonAnalysisOutcome::Repair {
canonical_presentation,
violation: Violation::UnsanctionedSelectorsOnly,
}
}
fn singleton_base_presentation_is_canonical(
presentation_selectors_after_base: &[Presentation],
presentation: Option<Presentation>,
) -> bool {
presentation_selectors_after_base == presentation.as_slice()
}
fn standalone_singleton_analysis_outcome(
base: char,
presentation_selectors_after_base: &[Presentation],
has_sanctioned_presentation: bool,
is_keycap: bool,
policy: &Policy,
) -> SingletonAnalysisOutcome {
if !has_sanctioned_presentation {
return if presentation_selectors_after_base.is_empty() {
SingletonAnalysisOutcome::Canonical
} else {
SingletonAnalysisOutcome::Repair {
canonical_presentation: None,
violation: Violation::UnsanctionedSelectorsOnly,
}
};
}
match (
policy.singleton_rule(base, is_keycap),
presentation_selectors_after_base,
) {
(SingletonRule::TextToBare, &[Presentation::Text, _, ..])
| (SingletonRule::EmojiToBare, &[Presentation::Emoji, _, ..]) => {
SingletonAnalysisOutcome::Repair {
canonical_presentation: None,
violation: Violation::primary(PrimaryViolationKind::RedundantSelector, true),
}
}
(_, &[current_presentation, _, ..]) => SingletonAnalysisOutcome::Repair {
canonical_presentation: Some(current_presentation),
violation: Violation::UnsanctionedSelectorsOnly,
},
(SingletonRule::BareToEmoji, &[]) => SingletonAnalysisOutcome::ResolvePresentation {
default: Presentation::Emoji,
},
(SingletonRule::BareToText, &[]) => SingletonAnalysisOutcome::ResolvePresentation {
default: Presentation::Text,
},
(SingletonRule::TextToBare, &[Presentation::Text])
| (SingletonRule::EmojiToBare, &[Presentation::Emoji]) => {
SingletonAnalysisOutcome::Repair {
canonical_presentation: None,
violation: Violation::primary(PrimaryViolationKind::RedundantSelector, false),
}
}
(SingletonRule::TextToBare, &[Presentation::Emoji] | &[])
| (SingletonRule::EmojiToBare, &[Presentation::Text] | &[])
| (SingletonRule::BareToText | SingletonRule::BareToEmoji, &[_]) => {
SingletonAnalysisOutcome::Canonical
}
}
}
fn modification_has_trailing_presentation_selectors(m: &EmojiModification) -> bool {
match m {
EmojiModification::EmojiModifier {
presentation_selectors_after_modifier,
..
} => !presentation_selectors_after_modifier.is_empty(),
EmojiModification::EnclosingKeycap {
presentation_selectors_after_keycap,
} => !presentation_selectors_after_keycap.is_empty(),
EmojiModification::TagModifier(runs) => runs
.iter()
.any(|r| !r.presentation_selectors_after_tag.is_empty()),
}
}
fn has_trailing_modification_presentation_selectors(modifications: &[EmojiModification]) -> bool {
modifications
.iter()
.any(modification_has_trailing_presentation_selectors)
}
fn unambiguous_finding<'a>(
item: &ScanItem<'a>,
violation: Violation,
fix_replacement: String,
) -> Finding<'a> {
let mut builder = ReplacementPlanBuilder::new();
builder.push_literal(fix_replacement);
finding_from_builder(item, violation, builder)
}
fn resolve_presentation_finding<'a>(
item: &ScanItem<'a>,
base: char,
modifications: &[EmojiModification],
default: Presentation,
) -> Finding<'a> {
let mut builder = ReplacementPlanBuilder::new();
builder.push_slot(presentation_slot(
ReplacementDecision::from_presentation(default),
[
(
ReplacementDecision::Text,
render_singleton(base, Some(Presentation::Text), modifications),
),
(
ReplacementDecision::Emoji,
render_singleton(base, Some(Presentation::Emoji), modifications),
),
],
));
finding_from_builder(
item,
Violation::primary(PrimaryViolationKind::BareNeedsResolution, false),
builder,
)
}
fn resolve_zwj_wrapper_presentation_finding<'a>(
item: &ScanItem<'a>,
base: char,
modifications: &[EmojiModification],
trailing_links: &[ZwjLink],
default: Presentation,
has_unsanctioned_selectors: bool,
) -> Finding<'a> {
debug_assert!(
!trailing_links.is_empty(),
"ZWJ wrapper presentation resolution requires at least one trailing link"
);
let mut builder = ReplacementPlanBuilder::new();
builder.push_slot(presentation_slot(
ReplacementDecision::from_presentation(default),
[
(
ReplacementDecision::Text,
render_singleton_with_links(
base,
Some(Presentation::Text),
modifications,
trailing_links,
),
),
(
ReplacementDecision::Emoji,
render_singleton_with_links(
base,
Some(Presentation::Emoji),
modifications,
trailing_links,
),
),
],
));
finding_from_builder(
item,
Violation::primary(
PrimaryViolationKind::BareNeedsResolution,
has_unsanctioned_selectors,
),
builder,
)
}
fn presentation_slot<const N: usize>(
default: ReplacementDecision,
replacements: [(ReplacementDecision, String); N],
) -> ReplacementSlotPlan {
let replacements: Vec<_> = replacements
.into_iter()
.map(|(decision, replacement)| SlotReplacement {
decision,
replacement,
})
.collect();
let choices = replacements
.iter()
.map(|replacement| replacement.decision)
.collect();
ReplacementSlotPlan {
public: DecisionSlot { choices, default },
replacements,
}
}
struct ReplacementPlanBuilder {
slots: Vec<ReplacementSlotPlan>,
pieces: Vec<ReplacementPiece>,
}
impl ReplacementPlanBuilder {
const fn new() -> Self {
Self {
slots: Vec::new(),
pieces: Vec::new(),
}
}
fn push_literal(&mut self, literal: String) {
self.pieces.push(ReplacementPiece::Literal(literal));
}
fn push_slot(&mut self, slot: ReplacementSlotPlan) {
let slot_index = self.slots.len();
self.slots.push(slot);
self.pieces.push(ReplacementPiece::Slot(slot_index));
}
fn extend(&mut self, pieces: Vec<ComponentReplacementPiece>, slots: Vec<ReplacementSlotPlan>) {
let slot_offset = self.slots.len();
self.slots.extend(slots);
self.pieces
.extend(pieces.into_iter().map(|piece| match piece {
ComponentReplacementPiece::Literal(literal) => ReplacementPiece::Literal(literal),
ComponentReplacementPiece::Slot(slot_index) => {
ReplacementPiece::Slot(slot_offset + slot_index)
}
}));
}
fn build(self, violation: Violation) -> ReplacementPlan {
let default_replacement = render_default_replacement(&self.slots, &self.pieces);
let plan = ReplacementPlan {
violation,
decision_slots: self.slots.iter().map(|slot| slot.public.clone()).collect(),
slots: self.slots,
pieces: self.pieces,
default_replacement,
};
debug_assert_eq!(
plan.render_replacement(&plan.default_decision()),
Some(plan.default_replacement.clone())
);
plan
}
}
fn render_default_replacement(
slots: &[ReplacementSlotPlan],
pieces: &[ReplacementPiece],
) -> String {
let mut out = String::new();
for piece in pieces {
match piece {
ReplacementPiece::Literal(text) => out.push_str(text),
ReplacementPiece::Slot(slot_index) => {
if let Some(slot) = slots.get(*slot_index)
&& let Some(replacement) = slot.replacement(slot.public.default_decision())
{
out.push_str(replacement);
}
}
}
}
out
}
fn finding_from_builder<'a>(
item: &ScanItem<'a>,
violation: Violation,
builder: ReplacementPlanBuilder,
) -> Finding<'a> {
Finding {
span: item.span.clone(),
raw: item.raw,
replacement_plan: builder.build(violation),
}
}
fn render_singleton(
base: char,
presentation: Option<Presentation>,
modifications: &[EmojiModification],
) -> String {
FixedEmojiLike::singleton_base(base, presentation, modifications).render_to_string()
}
fn render_singleton_with_links(
base: char,
presentation: Option<Presentation>,
modifications: &[EmojiModification],
trailing_links: &[ZwjLink],
) -> String {
let mut out =
FixedEmojiLike::singleton_base(base, presentation, modifications).render_to_string();
render_zwj_links(&mut out, trailing_links);
out
}
fn render_flag_with_links(
first_ri: char,
second_ri: char,
modifications: &[EmojiModification],
trailing_links: &[ZwjLink],
) -> String {
let mut out = FixedEmojiLike::flag(first_ri, second_ri, modifications).render_to_string();
render_zwj_links(&mut out, trailing_links);
out
}