Skip to content

Commit c68d84d

Browse files
committed
Clean up strange AI choice of grouping functions
1 parent 92b0f51 commit c68d84d

File tree

6 files changed

+760
-319
lines changed

6 files changed

+760
-319
lines changed

src/deps.deno.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export type {
22
MessageEntity,
3+
User,
34
} from "https://lib.deno.dev/x/grammy@^1.36/types.ts";

src/deps.node.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export type { MessageEntity } from "grammy/types";
1+
export type { MessageEntity, User } from "grammy/types";

src/format.ts

Lines changed: 8 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { MessageEntity } from "./deps.deno.ts";
2+
import { consolidateEntities, isEntitiesEqual } from "./util.ts";
23

34
/**
45
* Represents an entity tag used for formatting text via fmt.
@@ -342,7 +343,7 @@ export class FormattedString
342343
// Consolidate adjacent/overlapping entities of the same type
343344
return new FormattedString(
344345
result.rawText,
345-
result.consolidateEntities(result.rawEntities),
346+
consolidateEntities(result.rawEntities),
346347
);
347348
}
348349

@@ -597,7 +598,12 @@ export class FormattedString
597598
);
598599

599600
// Compare entities for exact match
600-
if (this.entitiesEqual(candidate.rawEntities, pattern.rawEntities)) {
601+
if (
602+
isEntitiesEqual(
603+
candidate.rawEntities,
604+
pattern.rawEntities,
605+
)
606+
) {
601607
return textIndex;
602608
}
603609

@@ -608,149 +614,6 @@ export class FormattedString
608614

609615
return -1;
610616
}
611-
612-
/**
613-
* Consolidates overlapping or adjacent entities of the same type
614-
* @param entities Array of entities to consolidate
615-
* @returns New array with consolidated entities
616-
*/
617-
protected consolidateEntities(entities: MessageEntity[]): MessageEntity[] {
618-
if (entities.length <= 1) {
619-
return [...entities];
620-
}
621-
622-
// Sort entities by offset to process them in order
623-
const sortedEntities = [...entities].sort((a, b) => a.offset - b.offset);
624-
const consolidated: MessageEntity[] = [];
625-
626-
let current = { ...sortedEntities[0] };
627-
628-
for (let i = 1; i < sortedEntities.length; i++) {
629-
const next = sortedEntities[i];
630-
631-
// Check if entities can be consolidated
632-
if (this.canConsolidateEntities(current, next)) {
633-
// Merge the entities by extending the current entity
634-
const currentEnd = current.offset + current.length;
635-
const nextEnd = next.offset + next.length;
636-
current.length = Math.max(currentEnd, nextEnd) - current.offset;
637-
} else {
638-
// Cannot consolidate, add current to result and move to next
639-
consolidated.push(current);
640-
current = { ...next };
641-
}
642-
}
643-
644-
// Add the last entity
645-
consolidated.push(current);
646-
647-
return consolidated;
648-
}
649-
650-
/**
651-
* Helper method to check if two entities can be consolidated
652-
* @param entity1 First entity
653-
* @param entity2 Second entity (should have offset >= entity1.offset)
654-
* @returns true if entities can be consolidated, false otherwise
655-
*/
656-
private canConsolidateEntities(
657-
entity1: MessageEntity,
658-
entity2: MessageEntity,
659-
): boolean {
660-
// Must have the same type
661-
if (entity1.type !== entity2.type) {
662-
return false;
663-
}
664-
665-
// Check type-specific properties for compatibility
666-
if (entity1.type === "text_link" && entity2.type === "text_link") {
667-
if (entity1.url !== entity2.url) {
668-
return false;
669-
}
670-
}
671-
672-
if (entity1.type === "pre" && entity2.type === "pre") {
673-
if (entity1.language !== entity2.language) {
674-
return false;
675-
}
676-
}
677-
678-
if (entity1.type === "custom_emoji" && entity2.type === "custom_emoji") {
679-
if (entity1.custom_emoji_id !== entity2.custom_emoji_id) {
680-
return false;
681-
}
682-
}
683-
684-
if (entity1.type === "text_mention" && entity2.type === "text_mention") {
685-
if (entity1.user !== entity2.user) {
686-
return false;
687-
}
688-
}
689-
690-
// Check if entities overlap or are adjacent
691-
const entity1End = entity1.offset + entity1.length;
692-
const entity2Start = entity2.offset;
693-
694-
// Adjacent (touching) or overlapping entities can be consolidated
695-
return entity2Start <= entity1End;
696-
}
697-
698-
/**
699-
* Helper method to compare two arrays of message entities for exact equality
700-
* @param entities1 First array of entities
701-
* @param entities2 Second array of entities
702-
* @returns true if the entities are exactly equal, false otherwise
703-
*/
704-
private entitiesEqual(
705-
entities1: MessageEntity[],
706-
entities2: MessageEntity[],
707-
): boolean {
708-
if (entities1.length !== entities2.length) {
709-
return false;
710-
}
711-
712-
for (let i = 0; i < entities1.length; i++) {
713-
const entity1 = entities1[i];
714-
const entity2 = entities2[i];
715-
716-
// Compare all properties of the entities
717-
if (
718-
entity1.type !== entity2.type ||
719-
entity1.offset !== entity2.offset ||
720-
entity1.length !== entity2.length
721-
) {
722-
return false;
723-
}
724-
725-
// Compare type-specific properties based on entity type
726-
if (entity1.type === "text_link" && entity2.type === "text_link") {
727-
if (entity1.url !== entity2.url) {
728-
return false;
729-
}
730-
}
731-
732-
if (entity1.type === "pre" && entity2.type === "pre") {
733-
if (entity1.language !== entity2.language) {
734-
return false;
735-
}
736-
}
737-
738-
if (entity1.type === "custom_emoji" && entity2.type === "custom_emoji") {
739-
if (entity1.custom_emoji_id !== entity2.custom_emoji_id) {
740-
return false;
741-
}
742-
}
743-
744-
if (entity1.type === "text_mention" && entity2.type === "text_mention") {
745-
// Compare user property (basic equality check)
746-
if (entity1.user !== entity2.user) {
747-
return false;
748-
}
749-
}
750-
}
751-
752-
return true;
753-
}
754617
}
755618

756619
function buildFormatter<T extends Array<unknown> = never>(

src/util.ts

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import type { MessageEntity, User } from "./deps.deno.ts";
2+
3+
/**
4+
* Compares two user objects for deep equality.
5+
* @param user1 First user object to compare
6+
* @param user2 Second user object to compare
7+
* @returns true if the users have the same properties, false otherwise
8+
*/
9+
export function isUserEqual(user1: User, user2: User): boolean {
10+
const propertyComparisonMap = new Map<string, unknown>();
11+
12+
for (const [key, value] of Object.entries(user1)) {
13+
if (value == undefined) {
14+
// We shall consider absent properties, null, and undefined as "equal"
15+
continue;
16+
}
17+
propertyComparisonMap.set(key, value);
18+
}
19+
20+
for (const [key, value] of Object.entries(user2)) {
21+
if (value == undefined) {
22+
continue;
23+
}
24+
if (!propertyComparisonMap.has(key)) {
25+
return false; // user2 has a non-nil property that user1 does not
26+
}
27+
if (propertyComparisonMap.get(key) !== value) {
28+
return false;
29+
}
30+
// Remove the property from the map to ensure all properties match
31+
propertyComparisonMap.delete(key);
32+
}
33+
34+
return propertyComparisonMap.size === 0; // All properties matched
35+
}
36+
37+
/**
38+
* Checks if two entities are similar without requiring length and offset to be equal.
39+
* This method compares entity types and type-specific properties but ignores position and size.
40+
* @param entity1 First entity to compare
41+
* @param entity2 Second entity to compare
42+
* @returns true if the entities are similar (same type and properties), false otherwise
43+
*/
44+
export function isEntitySimilar(
45+
entity1: MessageEntity,
46+
entity2: MessageEntity,
47+
) {
48+
// Must have the same type
49+
if (entity1.type !== entity2.type) {
50+
return false;
51+
}
52+
53+
// Compare type-specific properties based on entity type
54+
if (entity1.type === "text_link" && entity2.type === "text_link") {
55+
return entity1.url === entity2.url;
56+
}
57+
58+
if (entity1.type === "pre" && entity2.type === "pre") {
59+
return entity1.language === entity2.language;
60+
}
61+
62+
if (entity1.type === "custom_emoji" && entity2.type === "custom_emoji") {
63+
return entity1.custom_emoji_id === entity2.custom_emoji_id;
64+
}
65+
66+
if (entity1.type === "text_mention" && entity2.type === "text_mention") {
67+
return isUserEqual(entity1.user, entity2.user);
68+
}
69+
70+
// For entities without type-specific properties, having the same type means they are similar
71+
return true;
72+
}
73+
74+
/**
75+
* Checks if two entities are equal, including their offset and length properties.
76+
* This method performs a complete comparison of all entity properties.
77+
* @param entity1 First entity to compare
78+
* @param entity2 Second entity to compare
79+
* @returns true if the entities are completely equal, false otherwise
80+
*/
81+
export function isEntityEqual(
82+
entity1: MessageEntity,
83+
entity2: MessageEntity,
84+
) {
85+
// First check if they are similar (type and type-specific properties)
86+
if (!isEntitySimilar(entity1, entity2)) {
87+
return false;
88+
}
89+
90+
// Then check offset and length
91+
return entity1.offset === entity2.offset && entity1.length === entity2.length;
92+
}
93+
94+
/**
95+
* Helper method to compare two arrays of message entities for exact equality
96+
* @param entities1 First array of entities
97+
* @param entities2 Second array of entities
98+
* @returns true if the entities are exactly equal, false otherwise
99+
*/
100+
export function isEntitiesEqual(
101+
entities1: MessageEntity[],
102+
entities2: MessageEntity[],
103+
) {
104+
if (entities1.length !== entities2.length) {
105+
return false;
106+
}
107+
108+
for (let i = 0; i < entities1.length; i++) {
109+
const entity1 = entities1[i];
110+
const entity2 = entities2[i];
111+
if (!isEntityEqual(entity1, entity2)) {
112+
return false;
113+
}
114+
}
115+
return true;
116+
}
117+
118+
/**
119+
* Helper method to check if two entities can be consolidated
120+
* @param entity1 First entity
121+
* @param entity2 Second entity (should have offset >= entity1.offset)
122+
* @returns true if entities can be consolidated, false otherwise
123+
*/
124+
export function canConsolidateEntities(
125+
entity1: MessageEntity,
126+
entity2: MessageEntity,
127+
) {
128+
// Must be similar entities
129+
if (!isEntitySimilar(entity1, entity2)) {
130+
return false;
131+
}
132+
133+
// Check if entities overlap or are adjacent
134+
const entity1End = entity1.offset + entity1.length;
135+
const entity2Start = entity2.offset;
136+
137+
// Adjacent (touching) or overlapping entities can be consolidated
138+
return entity2Start <= entity1End;
139+
}
140+
141+
/**
142+
* Consolidates overlapping or adjacent entities of the same type
143+
* @param entities Array of entities to consolidate
144+
* @returns New array with consolidated entities
145+
*/
146+
export function consolidateEntities(
147+
entities: MessageEntity[],
148+
): MessageEntity[] {
149+
if (entities.length <= 1) {
150+
return entities;
151+
}
152+
153+
// Sort entities by offset to process them in order
154+
const sortedEntities = [...entities].sort((a, b) => a.offset - b.offset);
155+
const consolidated: MessageEntity[] = [];
156+
157+
let current = { ...sortedEntities[0] };
158+
159+
for (let i = 1; i < sortedEntities.length; i++) {
160+
const next = sortedEntities[i];
161+
162+
// Check if entities can be consolidated
163+
if (canConsolidateEntities(current, next)) {
164+
// Merge the entities by extending the current entity
165+
const currentEnd = current.offset + current.length;
166+
const nextEnd = next.offset + next.length;
167+
current.length = Math.max(currentEnd, nextEnd) - current.offset;
168+
} else {
169+
// Cannot consolidate, add current to result and move to next
170+
consolidated.push(current);
171+
current = { ...next };
172+
}
173+
}
174+
175+
// Add the last entity
176+
consolidated.push(current);
177+
178+
return consolidated;
179+
}

0 commit comments

Comments
 (0)