Skip to content

Commit cb3636b

Browse files
Merge pull request #345 from CrewForm/fix/trello-parser-flexible-headings
fix(trello): robust output parser with 4 fallback strategies
2 parents b18b235 + 05a29bd commit cb3636b

1 file changed

Lines changed: 75 additions & 5 deletions

File tree

task-runner/src/webhookDispatcher.ts

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -750,12 +750,81 @@ function sectionEmoji(heading: string): string {
750750
* doesn't have enough structure to justify splitting.
751751
*/
752752
function parseOutputSections(output: string): TrelloSection[] {
753-
const headingRegex = /^(#{1,2})\s+(.+)$/gm;
754-
const matches = [...output.matchAll(headingRegex)];
753+
// Strategy 1: Markdown headings (h1–h4)
754+
const headingRegex = /^(#{1,4})\s+(.+)$/gm;
755+
let matches = [...output.matchAll(headingRegex)];
755756

756-
// Need at least 2 headings to justify splitting into multiple cards
757-
if (matches.length < 2) return [];
757+
if (matches.length >= 2) {
758+
console.log(`[Trello] parseOutputSections: found ${matches.length} markdown headings`);
759+
return buildSectionsFromMatches(output, matches);
760+
}
761+
762+
// Strategy 2: Bold-line section headers (e.g., **Section Name** on its own line)
763+
const boldRegex = /^\*\*([^*]{3,80})\*\*\s*$/gm;
764+
matches = [...output.matchAll(boldRegex)];
765+
766+
if (matches.length >= 2) {
767+
console.log(`[Trello] parseOutputSections: found ${matches.length} bold-line sections`);
768+
return buildSectionsFromMatches(output, matches);
769+
}
770+
771+
// Strategy 3: Horizontal rule separators (---)
772+
const parts = output.split(/\n---+\n/);
773+
if (parts.length >= 2) {
774+
console.log(`[Trello] parseOutputSections: found ${parts.length} separator-delimited sections`);
775+
const sections: TrelloSection[] = [];
776+
for (let i = 0; i < parts.length; i++) {
777+
const part = parts[i].trim();
778+
if (!part) continue;
779+
780+
// Try to extract a title from the first line
781+
const firstLine = part.split('\n')[0].trim()
782+
.replace(/^#+\s*/, '') // strip leading #
783+
.replace(/^\*\*|\*\*$/g, '') // strip bold markers
784+
.substring(0, 80);
785+
const rest = part.substring(part.indexOf('\n') + 1).trim();
786+
787+
sections.push({
788+
title: `${sectionEmoji(firstLine)} ${firstLine}`,
789+
content: rest || part,
790+
});
791+
}
792+
return sections.length >= 2 ? sections : [];
793+
}
794+
795+
// Strategy 4: Large content with no clear structure — split by size
796+
if (output.length > 2000) {
797+
console.log(`[Trello] parseOutputSections: no structure found, splitting by paragraphs`);
798+
const paragraphs = output.split(/\n\n+/);
799+
const sections: TrelloSection[] = [];
800+
let currentContent = '';
801+
let sectionIndex = 1;
802+
803+
for (const para of paragraphs) {
804+
if (currentContent.length + para.length > 4000 && currentContent.length > 500) {
805+
sections.push({
806+
title: `📄 Part ${sectionIndex}`,
807+
content: currentContent.trim(),
808+
});
809+
currentContent = para;
810+
sectionIndex++;
811+
} else {
812+
currentContent += (currentContent ? '\n\n' : '') + para;
813+
}
814+
}
815+
if (currentContent.trim()) {
816+
sections.push({
817+
title: `📄 Part ${sectionIndex}`,
818+
content: currentContent.trim(),
819+
});
820+
}
821+
return sections.length >= 2 ? sections : [];
822+
}
823+
824+
return [];
825+
}
758826

827+
function buildSectionsFromMatches(output: string, matches: RegExpMatchArray[]): TrelloSection[] {
759828
const sections: TrelloSection[] = [];
760829

761830
// Preamble — any text before the first heading
@@ -766,7 +835,8 @@ function parseOutputSections(output: string): TrelloSection[] {
766835

767836
// Each heading becomes a card
768837
for (let i = 0; i < matches.length; i++) {
769-
const heading = matches[i][2].trim();
838+
// Use capture group 2 if present (markdown headings), otherwise group 1 (bold)
839+
const heading = (matches[i][2] ?? matches[i][1]).trim();
770840
const contentStart = matches[i].index! + matches[i][0].length;
771841
const contentEnd = i + 1 < matches.length ? matches[i + 1].index! : output.length;
772842
const content = output.substring(contentStart, contentEnd).trim();

0 commit comments

Comments
 (0)