Skip to content

Commit 92b0f51

Browse files
authored
Merge pull request #42 from KnightNiwrem/master
feat: Add slice, find; improve join
2 parents ec21f35 + 6c156f9 commit 92b0f51

File tree

2 files changed

+645
-16
lines changed

2 files changed

+645
-16
lines changed

src/format.ts

Lines changed: 191 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,12 +332,18 @@ export class FormattedString
332332
}
333333

334334
const sep = separator ?? "";
335-
return items.reduce<FormattedString>((acc, item, index) => {
335+
const result = items.reduce<FormattedString>((acc, item, index) => {
336336
if (index === 0) {
337337
return fmt`${item}`;
338338
}
339339
return fmt`${acc}${sep}${item}`;
340340
}, new FormattedString(""));
341+
342+
// Consolidate adjacent/overlapping entities of the same type
343+
return new FormattedString(
344+
result.rawText,
345+
result.consolidateEntities(result.rawEntities),
346+
);
341347
}
342348

343349
// Instance formatting methods
@@ -561,6 +567,190 @@ export class FormattedString
561567

562568
return new FormattedString(slicedText, slicedEntities);
563569
}
570+
571+
/**
572+
* Finds the first occurrence of a FormattedString pattern within this FormattedString
573+
* that matches both the raw text and raw entities exactly.
574+
* @param pattern The FormattedString pattern to search for
575+
* @returns The offset where the pattern is found, or -1 if not found
576+
*/
577+
find(pattern: FormattedString): number {
578+
// Handle empty pattern - matches at the beginning
579+
if (pattern.rawText.length === 0) {
580+
return 0;
581+
}
582+
583+
// Pattern cannot be longer than source
584+
if (pattern.rawText.length > this.rawText.length) {
585+
return -1;
586+
}
587+
588+
// Use indexOf to find text matches efficiently
589+
let searchStart = 0;
590+
let textIndex = this.rawText.indexOf(pattern.rawText, searchStart);
591+
592+
while (textIndex !== -1) {
593+
// Use slice to extract candidate and compare entities
594+
const candidate = this.slice(
595+
textIndex,
596+
textIndex + pattern.rawText.length,
597+
);
598+
599+
// Compare entities for exact match
600+
if (this.entitiesEqual(candidate.rawEntities, pattern.rawEntities)) {
601+
return textIndex;
602+
}
603+
604+
// Continue searching from the next position
605+
searchStart = textIndex + 1;
606+
textIndex = this.rawText.indexOf(pattern.rawText, searchStart);
607+
}
608+
609+
return -1;
610+
}
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+
}
564754
}
565755

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

0 commit comments

Comments
 (0)