Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283import { firstMeaningfulMermaidLine, meaningfulNumberedMermaidLines, stripMermaidQuotes as stripQuotes,} from "../core/mermaid.js"import { MermaidSyntaxError } from "../diagnostics.js"import type { SequenceArrowHead, SequenceDiagram, SequenceMessage, SequenceParticipant, SequenceParticipantGroup, SequenceStep,} from "./types.js"
const MESSAGE_RE = /^(.+?)\s*(-->>|->>|--x|-x|--\)|-\)|-->|->)([+-]?)\s*(.+?)\s*:\s*(.*)$/const NOTE_RE = /^note\s+(over|left\s+of|right\s+of)\s+(.+?)\s*:\s*(.*)$/iconst PARTICIPANT_RE = /^(?:participant|actor)\s+(\S+)(?:\s+as\s+(.+))?$/iconst ACTIVATION_RE = /^(activate|deactivate)\s+(.+)$/iconst BOX_RE = /^box(?:\s+(.+))?$/iconst ALT_RE = /^alt\s+(.+)$/iconst ELSE_RE = /^else(?:\s+(.+))?$/iconst LOOP_RE = /^loop\s+(.+)$/iconst AUTONUMBER_RE = /^autonumber(?:\s+(\d+)(?:\s+(\d+))?)?$/iconst UNSUPPORTED_BIDIRECTIONAL_MESSAGE_RE = /<<-{1,2}>>/const MESSAGE_OPERATOR_RE = /-->>|->>|--x|-x|--\)|-\)|-->|->/const CSS_COLOR_NAMES = new Set([ "black", "white", "red", "green", "blue", "yellow", "cyan", "magenta", "silver", "gray", "grey", "maroon", "olive", "lime", "aqua", "teal", "navy", "fuchsia", "purple", "orange", "brightblack", "brightred", "brightgreen", "brightblue", "brightyellow", "brightcyan", "brightmagenta", "brightwhite",])
function arrowHeadForSyntax(arrow: string): SequenceArrowHead | undefined { if (arrow.endsWith("x")) return "cross" if (arrow.endsWith(")")) return "async" if (arrow.endsWith(">") && !arrow.endsWith(">>")) return "open" return undefined}
function isBoxColorToken(value: string): boolean { const lowerValue = value.toLowerCase() return ( lowerValue === "transparent" || CSS_COLOR_NAMES.has(lowerValue) || /^#[0-9a-f]{3,8}$/i.test(value) || /^rgba?\(.+\)$/i.test(value) )}
function splitLeadingBoxToken(value: string): { token: string; rest: string } { if (/^rgba?\(/i.test(value)) { const closeIndex = value.indexOf(")") if (closeIndex >= 0) { return { token: value.slice(0, closeIndex + 1), rest: value.slice(closeIndex + 1).trim() } } }
const firstSpace = value.search(/\s/) return firstSpace < 0 ? { token: value, rest: "" } : { token: value.slice(0, firstSpace), rest: value.slice(firstSpace + 1).trim() }}
function boxLabelText(value: string | undefined): string { const rawLabel = (value ?? "").trim() if ((rawLabel.startsWith('"') && rawLabel.endsWith('"')) || (rawLabel.startsWith("'") && rawLabel.endsWith("'"))) { return stripQuotes(rawLabel) }
const label = stripQuotes(rawLabel) if (!label) return "" const { token, rest } = splitLeadingBoxToken(label) return isBoxColorToken(token) ? stripQuotes(rest) : label}
function addParticipantToGroup(group: SequenceParticipantGroup | undefined, participantId: string): void { if (!group || group.participantIds.includes(participantId)) return group.participantIds.push(participantId)}
function ensureParticipant( participants: SequenceParticipant[], id: string, label: string = id, replaceExistingLabel: boolean = false,): void { const existing = participants.find((participant) => participant.id === id) if (existing) { if (replaceExistingLabel) existing.label = label return } participants.push({ id, label })}
export function isMermaidSequenceDiagram(content: string): boolean { return firstMeaningfulMermaidLine(content)?.toLowerCase() === "sequencediagram"}
export function parseMermaidSequenceDiagram(content: string): SequenceDiagram { const participants: SequenceParticipant[] = [] const messages: SequenceMessage[] = [] const steps: SequenceStep[] = [] const groups: SequenceParticipantGroup[] = [] const blockStack: Array<{ kind: "box" | "alt" | "loop"; lineNumber: number; sourceLine: string }> = [] const groupStack: SequenceParticipantGroup[] = [] let nextMessageNumber: number | undefined let messageNumberIncrement = 1
for (const source of meaningfulNumberedMermaidLines(content)) { const line = source.text if (line.toLowerCase() === "sequencediagram") continue if (UNSUPPORTED_BIDIRECTIONAL_MESSAGE_RE.test(line)) { throw new MermaidSyntaxError("sequence", source.lineNumber, line) }
const autonumberMatch = line.match(AUTONUMBER_RE) if (autonumberMatch) { nextMessageNumber = Number.parseInt(autonumberMatch[1] ?? "1", 10) messageNumberIncrement = Number.parseInt(autonumberMatch[2] ?? "1", 10) continue }
const boxMatch = line.match(BOX_RE) if (boxMatch) { const group: SequenceParticipantGroup = { label: boxLabelText(boxMatch[1]), participantIds: [] } groups.push(group) groupStack.push(group) blockStack.push({ kind: "box", lineNumber: source.lineNumber, sourceLine: line }) continue }
const participantMatch = line.match(PARTICIPANT_RE) if (participantMatch) { const id = stripQuotes(participantMatch[1]!) ensureParticipant(participants, id, stripQuotes(participantMatch[2] ?? id), true) addParticipantToGroup(groupStack[groupStack.length - 1], id) continue }
const noteMatch = line.match(NOTE_RE) if (noteMatch) { const position = noteMatch[1]!.toLowerCase().replace(/\s+of$/, "") as "over" | "left" | "right" const over = noteMatch[2]! .split(",") .map((participant) => stripQuotes(participant)) .filter((participant) => participant.length > 0) for (const participant of over) ensureParticipant(participants, participant) steps.push({ type: "note", note: { over, label: stripQuotes(noteMatch[3]!), ...(position === "over" ? {} : { position }), }, }) continue }
const activationMatch = line.match(ACTIVATION_RE) if (activationMatch) { const participant = stripQuotes(activationMatch[2]!) ensureParticipant(participants, participant) steps.push({ type: "activation", activation: { participant, active: activationMatch[1]!.toLowerCase() === "activate" }, }) continue }
const altMatch = line.match(ALT_RE) if (altMatch) { blockStack.push({ kind: "alt", lineNumber: source.lineNumber, sourceLine: line }) steps.push({ type: "fragment", fragment: { kind: "alt", label: stripQuotes(altMatch[1]!) } }) continue }
const loopMatch = line.match(LOOP_RE) if (loopMatch) { blockStack.push({ kind: "loop", lineNumber: source.lineNumber, sourceLine: line }) steps.push({ type: "fragment", fragment: { kind: "loop", label: stripQuotes(loopMatch[1]!) } }) continue }
const elseMatch = line.match(ELSE_RE) if (elseMatch) { if (blockStack[blockStack.length - 1]?.kind !== "alt") { throw new MermaidSyntaxError( "sequence", source.lineNumber, line, 'Unexpected "else" without an open "alt" block', ) } steps.push({ type: "fragment", fragment: { kind: "else", label: stripQuotes(elseMatch[1] ?? "") } }) continue }
if (line.toLowerCase() === "end") { const block = blockStack.pop() if (!block) throw new MermaidSyntaxError("sequence", source.lineNumber, line, 'Unexpected "end" without an open block') if (block.kind === "box") { groupStack.pop() continue } steps.push({ type: "fragment", fragment: { kind: "end", label: block.kind } }) continue }
const messageMatch = line.match(MESSAGE_RE) if (messageMatch) { const from = stripQuotes(messageMatch[1]!) const arrow = messageMatch[2]! const activationMarker = messageMatch[3]! const to = stripQuotes(messageMatch[4]!) if (MESSAGE_OPERATOR_RE.test(from) || MESSAGE_OPERATOR_RE.test(to)) { throw new MermaidSyntaxError("sequence", source.lineNumber, line) } const message: SequenceMessage = { from, to, label: stripQuotes(messageMatch[5]!), style: arrow.startsWith("--") ? "dashed" : "solid", } const activeGroup = groupStack[groupStack.length - 1] ensureParticipant(participants, from) ensureParticipant(participants, to) addParticipantToGroup(activeGroup, from) addParticipantToGroup(activeGroup, to) const head = arrowHeadForSyntax(arrow) if (head) message.head = head if (nextMessageNumber !== undefined) { message.number = nextMessageNumber nextMessageNumber += messageNumberIncrement } if (activationMarker === "+") message.activate = to else if (activationMarker === "-") message.deactivate = from messages.push(message) steps.push({ type: "message", message }) continue }
throw new MermaidSyntaxError("sequence", source.lineNumber, line) }
const unclosedBlock = blockStack[blockStack.length - 1] if (unclosedBlock) { throw new MermaidSyntaxError( "sequence", unclosedBlock.lineNumber, unclosedBlock.sourceLine, `Unclosed ${unclosedBlock.kind} block; expected "end"`, ) }
return { participants, messages, steps, groups }}