+flowchart LR +nA[Default] --> A@{ icon: 'fa:bell', form: 'rounded' } + ++
+flowchart LR +nA[Style] --> A@{ icon: 'fa:bell', form: 'rounded' } +style A fill:#f9f,stroke:#333,stroke-width:4px ++
+flowchart LR +nA[Class] --> A@{ icon: 'fa:bell', form: 'rounded' } +A:::AClass +classDef AClass fill:#f9f,stroke:#333,stroke-width:4px ++
+flowchart LR + nA[Class] --> A@{ icon: 'logos:aws', form: 'rounded' } + ++
+flowchart LR +nA[Default] --> A@{ icon: 'fa:bell', form: 'square' } + ++
+flowchart LR +nA[Style] --> A@{ icon: 'fa:bell', form: 'square' } +style A fill:#f9f,stroke:#333,stroke-width:4px ++
+flowchart LR +nA[Class] --> A@{ icon: 'fa:bell', form: 'square' } +A:::AClass +classDef AClass fill:#f9f,stroke:#333,stroke-width:4px ++
+flowchart LR + nA[Class] --> A@{ icon: 'logos:aws', form: 'square' } + ++
+flowchart LR +nA[Default] --> A@{ icon: 'fa:bell', form: 'circle' } + ++
+flowchart LR +nA[Style] --> A@{ icon: 'fa:bell', form: 'circle' } +style A fill:#f9f,stroke:#333,stroke-width:4px ++
+flowchart LR +nA[Class] --> A@{ icon: 'fa:bell', form: 'circle' } +A:::AClass +classDef AClass fill:#f9f,stroke:#333,stroke-width:4px ++
+flowchart LR + nA[Class] --> A@{ icon: 'logos:aws', form: 'circle' } + A:::AClass + classDef AClass fill:#f9f,stroke:#333,stroke-width:4px ++
+flowchart LR + nA[Style] --> A@{ icon: 'logos:aws', form: 'circle' } + style A fill:#f9f,stroke:#333,stroke-width:4px +
kanban id2[In progress] docs[Create Blog about the new diagram]@{ priority: 'Very Low', ticket: MC-2037, assigned: 'knsv' }-
+--- config: kanban: @@ -118,6 +191,30 @@ kanban From fdb8ae5b5357d1e2a1940d628917be99dc447c1f Mon Sep 17 00:00:00 2001 From: saurabhg772244Date: Mon, 27 Jan 2025 16:16:43 +0530 Subject: [PATCH 150/230] Set custom font family for legend in user diagrams. --- packages/mermaid/src/diagrams/user-journey/styles.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/mermaid/src/diagrams/user-journey/styles.js b/packages/mermaid/src/diagrams/user-journey/styles.js index 9cdbcd12e..391f64b87 100644 --- a/packages/mermaid/src/diagrams/user-journey/styles.js +++ b/packages/mermaid/src/diagrams/user-journey/styles.js @@ -13,6 +13,8 @@ const getStyles = (options) => .legend { fill: ${options.textColor}; + font-family: ${options.fontFamily}; + font-size: ${options.textSize}; } .label text { From 5e9c887385466a35a74295d94f25065bcee9f14d Mon Sep 17 00:00:00 2001 From: saurabhg772244 Date: Mon, 27 Jan 2025 16:30:35 +0530 Subject: [PATCH 151/230] Fixed unit tests. --- packages/mermaid/src/diagrams/user-journey/styles.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/mermaid/src/diagrams/user-journey/styles.js b/packages/mermaid/src/diagrams/user-journey/styles.js index 391f64b87..a0528294f 100644 --- a/packages/mermaid/src/diagrams/user-journey/styles.js +++ b/packages/mermaid/src/diagrams/user-journey/styles.js @@ -14,7 +14,6 @@ const getStyles = (options) => .legend { fill: ${options.textColor}; font-family: ${options.fontFamily}; - font-size: ${options.textSize}; } .label text { From 47d4d56fa61f29252e5eb2fc475ad7c531a922fa Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Mon, 27 Jan 2025 07:14:10 -0800 Subject: [PATCH 152/230] Remove -unified from renderer file name --- packages/mermaid/src/diagrams/requirement/requirementDiagram.ts | 2 +- .../{requirementRenderer-unified.ts => requirementRenderer.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/mermaid/src/diagrams/requirement/{requirementRenderer-unified.ts => requirementRenderer.ts} (100%) diff --git a/packages/mermaid/src/diagrams/requirement/requirementDiagram.ts b/packages/mermaid/src/diagrams/requirement/requirementDiagram.ts index 5fb4c3fc7..619f5b052 100644 --- a/packages/mermaid/src/diagrams/requirement/requirementDiagram.ts +++ b/packages/mermaid/src/diagrams/requirement/requirementDiagram.ts @@ -3,7 +3,7 @@ import type { DiagramDefinition } from '../../diagram-api/types.js'; import parser from './parser/requirementDiagram.jison'; import db from './requirementDb.js'; import styles from './styles.js'; -import renderer from './requirementRenderer-unified.js'; +import renderer from './requirementRenderer.js'; export const diagram: DiagramDefinition = { parser, diff --git a/packages/mermaid/src/diagrams/requirement/requirementRenderer-unified.ts b/packages/mermaid/src/diagrams/requirement/requirementRenderer.ts similarity index 100% rename from packages/mermaid/src/diagrams/requirement/requirementRenderer-unified.ts rename to packages/mermaid/src/diagrams/requirement/requirementRenderer.ts From 97788df7e3a30f91ae3a3251fe30188d70732a7a Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Mon, 27 Jan 2025 08:21:52 -0800 Subject: [PATCH 153/230] Change variable casing --- .../rendering-elements/shapes/requirementBox.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/requirementBox.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/requirementBox.ts index 3e0a310e6..da28f5db2 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/requirementBox.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/requirementBox.ts @@ -19,8 +19,8 @@ export async function requirementBox ( const requirementNode = node as unknown as Requirement; const elementNode = node as unknown as Element; const config = getConfig().requirement; - const PADDING = 20; - const GAP = 20; + const padding = 20; + const gap = 20; const isRequirementNode = 'verifyMethod' in node; const classes = getNodeClasses(node); @@ -49,7 +49,7 @@ export async function requirementBox ( accumulativeHeight, node.labelStyle ); - accumulativeHeight += nameHeight + GAP; + accumulativeHeight += nameHeight + gap; // Requirement if (isRequirementNode) { @@ -99,7 +99,7 @@ export async function requirementBox ( } const totalWidth = Math.max( - (shapeSvg.node()?.getBBox().width ?? 200) + PADDING, + (shapeSvg.node()?.getBBox().width ?? 200) + padding, config?.rect_min_width ?? 200 ); const totalHeight = totalWidth; @@ -141,22 +141,22 @@ export async function requirementBox ( } const newTranslateY = translateY - totalHeight / 2; - let newTranslateX = x + PADDING / 2; + let newTranslateX = x + padding / 2; // Keep type and name labels centered. if (i === 0 || i === 1) { newTranslateX = translateX; } // Set the updated transform attribute - text.attr('transform', `translate(${newTranslateX}, ${newTranslateY + PADDING})`); + text.attr('transform', `translate(${newTranslateX}, ${newTranslateY + padding})`); }); // Insert divider line const roughLine = rc.line( x, - y + typeHeight + nameHeight + GAP, + y + typeHeight + nameHeight + gap, x + totalWidth, - y + typeHeight + nameHeight + GAP, + y + typeHeight + nameHeight + gap, options ); const dividerLine = shapeSvg.insert(() => roughLine); From 9609aced140eabda0ea800afff511217ba7dacfc Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Mon, 27 Jan 2025 09:21:34 -0800 Subject: [PATCH 154/230] Update requirementDb to class to encapsulate data --- .../src/diagrams/requirement/requirementDb.ts | 580 +++++++++--------- .../requirement/requirementDiagram.ts | 6 +- 2 files changed, 298 insertions(+), 288 deletions(-) diff --git a/packages/mermaid/src/diagrams/requirement/requirementDb.ts b/packages/mermaid/src/diagrams/requirement/requirementDb.ts index 76787fd8f..972c5e44e 100644 --- a/packages/mermaid/src/diagrams/requirement/requirementDb.ts +++ b/packages/mermaid/src/diagrams/requirement/requirementDb.ts @@ -1,4 +1,5 @@ import { getConfig } from '../../diagram-api/diagramAPI.js'; +import type { DiagramDB } from '../../diagram-api/types.js'; import { log } from '../../logger.js'; import type { Node, Edge } from '../../rendering-util/types.js'; @@ -22,320 +23,327 @@ import type { VerifyType, } from './types.js'; -const RequirementType = { - REQUIREMENT: 'Requirement', - FUNCTIONAL_REQUIREMENT: 'Functional Requirement', - INTERFACE_REQUIREMENT: 'Interface Requirement', - PERFORMANCE_REQUIREMENT: 'Performance Requirement', - PHYSICAL_REQUIREMENT: 'Physical Requirement', - DESIGN_CONSTRAINT: 'Design Constraint', -}; +export class RequirementDB implements DiagramDB { + private relations: Relation[] = []; + private latestRequirement: Requirement = this.getInitialRequirement(); + private requirements = new Map (); + private latestElement: Element = this.getInitialElement(); + private elements = new Map (); + private classes = new Map (); + private direction = 'TB'; -const RiskLevel = { - LOW_RISK: 'Low', - MED_RISK: 'Medium', - HIGH_RISK: 'High', -}; + private RequirementType = { + REQUIREMENT: 'Requirement', + FUNCTIONAL_REQUIREMENT: 'Functional Requirement', + INTERFACE_REQUIREMENT: 'Interface Requirement', + PERFORMANCE_REQUIREMENT: 'Performance Requirement', + PHYSICAL_REQUIREMENT: 'Physical Requirement', + DESIGN_CONSTRAINT: 'Design Constraint', + }; -const VerifyType = { - VERIFY_ANALYSIS: 'Analysis', - VERIFY_DEMONSTRATION: 'Demonstration', - VERIFY_INSPECTION: 'Inspection', - VERIFY_TEST: 'Test', -}; + private RiskLevel = { + LOW_RISK: 'Low', + MED_RISK: 'Medium', + HIGH_RISK: 'High', + }; -const Relationships = { - CONTAINS: 'contains', - COPIES: 'copies', - DERIVES: 'derives', - SATISFIES: 'satisfies', - VERIFIES: 'verifies', - REFINES: 'refines', - TRACES: 'traces', -}; + private VerifyType = { + VERIFY_ANALYSIS: 'Analysis', + VERIFY_DEMONSTRATION: 'Demonstration', + VERIFY_INSPECTION: 'Inspection', + VERIFY_TEST: 'Test', + }; -let direction = 'TB'; -const getDirection = () => direction; -const setDirection = (dir: string) => { - direction = dir; -}; + private Relationships = { + CONTAINS: 'contains', + COPIES: 'copies', + DERIVES: 'derives', + SATISFIES: 'satisfies', + VERIFIES: 'verifies', + REFINES: 'refines', + TRACES: 'traces', + }; -const getInitialRequirement = (): Requirement => ({ - requirementId: '', - text: '', - risk: '' as RiskLevel, - verifyMethod: '' as VerifyType, - name: '', - type: '' as RequirementType, - cssStyles: [], - classes: ['default'], -}); + constructor() { + this.clear(); -const getInitialElement = (): Element => ({ - name: '', - type: '', - docRef: '', - cssStyles: [], - classes: ['default'], -}); + // Needed for JISON since it only supports direct properties + this.setDirection = this.setDirection.bind(this); + this.addRequirement = this.addRequirement.bind(this); + this.setNewReqId = this.setNewReqId.bind(this); + this.setNewReqRisk = this.setNewReqRisk.bind(this); + this.setNewReqText = this.setNewReqText.bind(this); + this.setNewReqVerifyMethod = this.setNewReqVerifyMethod.bind(this); + this.addElement = this.addElement.bind(this); + this.setNewElementType = this.setNewElementType.bind(this); + this.setNewElementDocRef = this.setNewElementDocRef.bind(this); + this.addRelationship = this.addRelationship.bind(this); + this.setCssStyle = this.setCssStyle.bind(this); + this.setClass = this.setClass.bind(this); + this.defineClass = this.defineClass.bind(this); + this.setAccTitle = this.setAccTitle.bind(this); + this.setAccDescription = this.setAccDescription.bind(this); + } -// Update initial declarations -let relations: Relation[] = []; -let latestRequirement: Requirement = getInitialRequirement(); -let requirements = new Map (); -let latestElement: Element = getInitialElement(); -let elements = new Map (); -let classes = new Map (); + public getDirection() { + return this.direction; + } + public setDirection(dir: string) { + this.direction = dir; + } -// Add reset functions -const resetLatestRequirement = () => { - latestRequirement = getInitialRequirement(); -}; + private resetLatestRequirement() { + this.latestRequirement = this.getInitialRequirement(); + } -const resetLatestElement = () => { - latestElement = getInitialElement(); -}; + private resetLatestElement() { + this.latestElement = this.getInitialElement(); + } -const addRequirement = (name: string, type: RequirementType) => { - if (!requirements.has(name)) { - requirements.set(name, { - name, - type, - requirementId: latestRequirement.requirementId, - text: latestRequirement.text, - risk: latestRequirement.risk, - verifyMethod: latestRequirement.verifyMethod, + private getInitialRequirement(): Requirement { + return { + requirementId: '', + text: '', + risk: '' as RiskLevel, + verifyMethod: '' as VerifyType, + name: '', + type: '' as RequirementType, cssStyles: [], classes: ['default'], - }); + }; } - resetLatestRequirement(); - return requirements.get(name); -}; - -const getRequirements = () => requirements; - -const setNewReqId = (id: string) => { - if (latestRequirement !== undefined) { - latestRequirement.requirementId = id; - } -}; - -const setNewReqText = (text: string) => { - if (latestRequirement !== undefined) { - latestRequirement.text = text; - } -}; - -const setNewReqRisk = (risk: RiskLevel) => { - if (latestRequirement !== undefined) { - latestRequirement.risk = risk; - } -}; - -const setNewReqVerifyMethod = (verifyMethod: VerifyType) => { - if (latestRequirement !== undefined) { - latestRequirement.verifyMethod = verifyMethod; - } -}; - -const addElement = (name: string) => { - if (!elements.has(name)) { - elements.set(name, { - name, - type: latestElement.type, - docRef: latestElement.docRef, + private getInitialElement(): Element { + return { + name: '', + type: '', + docRef: '', cssStyles: [], classes: ['default'], - }); - log.info('Added new element: ', name); + }; } - resetLatestElement(); - return elements.get(name); -}; - -const getElements = () => elements; - -const setNewElementType = (type: string) => { - if (latestElement !== undefined) { - latestElement.type = type; - } -}; - -const setNewElementDocRef = (docRef: string) => { - if (latestElement !== undefined) { - latestElement.docRef = docRef; - } -}; - -const addRelationship = (type: RelationshipType, src: string, dst: string) => { - relations.push({ - type, - src, - dst, - }); -}; - -const getRelationships = () => relations; - -const clear = () => { - relations = []; - resetLatestRequirement(); - requirements = new Map(); - resetLatestElement(); - elements = new Map(); - classes = new Map(); - commonClear(); -}; - -export const setCssStyle = function (ids: string[], styles: string[]) { - for (const id of ids) { - const node = requirements.get(id) ?? elements.get(id); - if (!styles || !node) { - return; - } - for (const s of styles) { - if (s.includes(',')) { - node.cssStyles.push(...s.split(',')); - } else { - node.cssStyles.push(s); - } - } - } -}; - -export const setClass = function (ids: string[], classNames: string[]) { - for (const id of ids) { - const node = requirements.get(id) ?? elements.get(id); - if (node) { - for (const _class of classNames) { - node.classes.push(_class); - const styles = classes.get(_class)?.styles; - if (styles) { - node.cssStyles.push(...styles); - } - } - } - } -}; - -export const defineClass = function (ids: string[], style: string[]) { - for (const id of ids) { - let styleClass = classes.get(id); - if (styleClass === undefined) { - styleClass = { id, styles: [], textStyles: [] }; - classes.set(id, styleClass); - } - - if (style) { - style.forEach(function (s) { - if (/color/.exec(s)) { - const newStyle = s.replace('fill', 'bgFill'); // .replace('color', 'fill'); - styleClass.textStyles.push(newStyle); - } - styleClass.styles.push(s); + public addRequirement(name: string, type: RequirementType) { + if (!this.requirements.has(name)) { + this.requirements.set(name, { + name, + type, + requirementId: this.latestRequirement.requirementId, + text: this.latestRequirement.text, + risk: this.latestRequirement.risk, + verifyMethod: this.latestRequirement.verifyMethod, + cssStyles: [], + classes: ['default'], }); } + this.resetLatestRequirement(); - requirements.forEach((value) => { - if (value.classes.includes(id)) { - value.cssStyles.push(...style.flatMap((s) => s.split(','))); - } - }); - elements.forEach((value) => { - if (value.classes.includes(id)) { - value.cssStyles.push(...style.flatMap((s) => s.split(','))); - } + return this.requirements.get(name); + } + + public getRequirements() { + return this.requirements; + } + + public setNewReqId(id: string) { + if (this.latestRequirement !== undefined) { + this.latestRequirement.requirementId = id; + } + } + + public setNewReqText(text: string) { + if (this.latestRequirement !== undefined) { + this.latestRequirement.text = text; + } + } + + public setNewReqRisk(risk: RiskLevel) { + if (this.latestRequirement !== undefined) { + this.latestRequirement.risk = risk; + } + } + + public setNewReqVerifyMethod(verifyMethod: VerifyType) { + if (this.latestRequirement !== undefined) { + this.latestRequirement.verifyMethod = verifyMethod; + } + } + + public addElement(name: string) { + if (!this.elements.has(name)) { + this.elements.set(name, { + name, + type: this.latestElement.type, + docRef: this.latestElement.docRef, + cssStyles: [], + classes: ['default'], + }); + log.info('Added new element: ', name); + } + this.resetLatestElement(); + + return this.elements.get(name); + } + + public getElements() { + return this.elements; + } + + public setNewElementType(type: string) { + if (this.latestElement !== undefined) { + this.latestElement.type = type; + } + } + + public setNewElementDocRef(docRef: string) { + if (this.latestElement !== undefined) { + this.latestElement.docRef = docRef; + } + } + + public addRelationship(type: RelationshipType, src: string, dst: string) { + this.relations.push({ + type, + src, + dst, }); } -}; -export const getClasses = () => { - return classes; -}; - -const getData = () => { - const config = getConfig(); - const nodes: Node[] = []; - const edges: Edge[] = []; - for (const requirement of requirements.values()) { - const node = requirement as unknown as Node; - node.id = requirement.name; - node.cssStyles = requirement.cssStyles; - node.cssClasses = requirement.classes.join(' '); - node.shape = 'requirementBox'; - node.look = config.look; - nodes.push(node); + public getRelationships() { + return this.relations; } - for (const element of elements.values()) { - const node = element as unknown as Node; - node.shape = 'requirementBox'; - node.look = config.look; - node.id = element.name; - node.cssStyles = element.cssStyles; - node.cssClasses = element.classes.join(' '); - - nodes.push(node); + public clear() { + this.relations = []; + this.resetLatestRequirement(); + this.requirements = new Map(); + this.resetLatestElement(); + this.elements = new Map(); + this.classes = new Map(); + commonClear(); } - for (const relation of relations) { - let counter = 0; - const isContains = relation.type === Relationships.CONTAINS; - const edge: Edge = { - id: `${relation.src}-${relation.dst}-${counter}`, - start: requirements.get(relation.src)?.name ?? elements.get(relation.src)?.name, - end: requirements.get(relation.dst)?.name ?? elements.get(relation.dst)?.name, - label: `<<${relation.type}>>`, - classes: 'relationshipLine', - style: ['fill:none', isContains ? '' : 'stroke-dasharray: 10,7'], - labelpos: 'c', - thickness: 'normal', - type: 'normal', - pattern: isContains ? 'normal' : 'dashed', - arrowTypeEnd: isContains ? 'requirement_contains' : 'requirement_arrow', - look: config.look, - }; - - edges.push(edge); - counter++; + public setCssStyle(ids: string[], styles: string[]) { + for (const id of ids) { + const node = this.requirements.get(id) ?? this.elements.get(id); + if (!styles || !node) { + return; + } + for (const s of styles) { + if (s.includes(',')) { + node.cssStyles.push(...s.split(',')); + } else { + node.cssStyles.push(s); + } + } + } } - return { nodes, edges, other: {}, config, direction: getDirection() }; -}; + public setClass(ids: string[], classNames: string[]) { + for (const id of ids) { + const node = this.requirements.get(id) ?? this.elements.get(id); + if (node) { + for (const _class of classNames) { + node.classes.push(_class); + const styles = this.classes.get(_class)?.styles; + if (styles) { + node.cssStyles.push(...styles); + } + } + } + } + } -export default { - Relationships, - RequirementType, - RiskLevel, - VerifyType, - getConfig: () => getConfig().requirement, - addRequirement, - getRequirements, - setNewReqId, - setNewReqText, - setNewReqRisk, - setNewReqVerifyMethod, - setAccTitle, - getAccTitle, - setAccDescription, - getAccDescription, - setDiagramTitle, - getDiagramTitle, - getDirection, - setDirection, - addElement, - getElements, - setNewElementType, - setNewElementDocRef, - addRelationship, - getRelationships, - clear, - setCssStyle, - setClass, - defineClass, - getClasses, - getData, -}; + public defineClass(ids: string[], style: string[]) { + for (const id of ids) { + let styleClass = this.classes.get(id); + if (styleClass === undefined) { + styleClass = { id, styles: [], textStyles: [] }; + this.classes.set(id, styleClass); + } + + if (style) { + style.forEach(function (s) { + if (/color/.exec(s)) { + const newStyle = s.replace('fill', 'bgFill'); // .replace('color', 'fill'); + styleClass.textStyles.push(newStyle); + } + styleClass.styles.push(s); + }); + } + + this.requirements.forEach((value) => { + if (value.classes.includes(id)) { + value.cssStyles.push(...style.flatMap((s) => s.split(','))); + } + }); + this.elements.forEach((value) => { + if (value.classes.includes(id)) { + value.cssStyles.push(...style.flatMap((s) => s.split(','))); + } + }); + } + } + + public getClasses() { + return this.classes; + } + + public getData() { + const config = getConfig(); + const nodes: Node[] = []; + const edges: Edge[] = []; + for (const requirement of this.requirements.values()) { + const node = requirement as unknown as Node; + node.id = requirement.name; + node.cssStyles = requirement.cssStyles; + node.cssClasses = requirement.classes.join(' '); + node.shape = 'requirementBox'; + node.look = config.look; + nodes.push(node); + } + + for (const element of this.elements.values()) { + const node = element as unknown as Node; + node.shape = 'requirementBox'; + node.look = config.look; + node.id = element.name; + node.cssStyles = element.cssStyles; + node.cssClasses = element.classes.join(' '); + + nodes.push(node); + } + + for (const relation of this.relations) { + let counter = 0; + const isContains = relation.type === this.Relationships.CONTAINS; + const edge: Edge = { + id: `${relation.src}-${relation.dst}-${counter}`, + start: this.requirements.get(relation.src)?.name ?? this.elements.get(relation.src)?.name, + end: this.requirements.get(relation.dst)?.name ?? this.elements.get(relation.dst)?.name, + label: `<<${relation.type}>>`, + classes: 'relationshipLine', + style: ['fill:none', isContains ? '' : 'stroke-dasharray: 10,7'], + labelpos: 'c', + thickness: 'normal', + type: 'normal', + pattern: isContains ? 'normal' : 'dashed', + arrowTypeEnd: isContains ? 'requirement_contains' : 'requirement_arrow', + look: config.look, + }; + + edges.push(edge); + counter++; + } + + return { nodes, edges, other: {}, config, direction: this.getDirection() }; + } + + public setAccTitle = setAccTitle; + public getAccTitle = getAccTitle; + public setAccDescription = setAccDescription; + public getAccDescription = getAccDescription; + public setDiagramTitle = setDiagramTitle; + public getDiagramTitle = getDiagramTitle; + public getConfig = () => getConfig().class; +} diff --git a/packages/mermaid/src/diagrams/requirement/requirementDiagram.ts b/packages/mermaid/src/diagrams/requirement/requirementDiagram.ts index 619f5b052..246d91197 100644 --- a/packages/mermaid/src/diagrams/requirement/requirementDiagram.ts +++ b/packages/mermaid/src/diagrams/requirement/requirementDiagram.ts @@ -1,13 +1,15 @@ import type { DiagramDefinition } from '../../diagram-api/types.js'; // @ts-ignore: JISON doesn't support types import parser from './parser/requirementDiagram.jison'; -import db from './requirementDb.js'; +import { RequirementDB } from './requirementDb.js'; import styles from './styles.js'; import renderer from './requirementRenderer.js'; export const diagram: DiagramDefinition = { parser, - db, + get db() { + return new RequirementDB(); + }, renderer, styles, }; From 523286bbcba2129d4ab89da98a3d30ecf5887b69 Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Mon, 27 Jan 2025 09:29:51 -0800 Subject: [PATCH 155/230] Update test file to work with new Db --- .../mermaid/src/diagrams/requirement/requirementDb.spec.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/mermaid/src/diagrams/requirement/requirementDb.spec.ts b/packages/mermaid/src/diagrams/requirement/requirementDb.spec.ts index da6684ca1..715d4d053 100644 --- a/packages/mermaid/src/diagrams/requirement/requirementDb.spec.ts +++ b/packages/mermaid/src/diagrams/requirement/requirementDb.spec.ts @@ -1,8 +1,9 @@ -import requirementDb from './requirementDb.js'; +import { RequirementDB } from './requirementDb.js'; import { describe, it, expect } from 'vitest'; -import type { RelationshipType } from './types.js'; +import type { Relation, RelationshipType } from './types.js'; describe('requirementDb', () => { + const requirementDb = new RequirementDB(); beforeEach(() => { requirementDb.clear(); }); @@ -23,7 +24,7 @@ describe('requirementDb', () => { requirementDb.addRelationship('contains' as RelationshipType, 'src', 'dst'); const relationships = requirementDb.getRelationships(); const relationship = relationships.find( - (r) => r.type === 'contains' && r.src === 'src' && r.dst === 'dst' + (r: Relation) => r.type === 'contains' && r.src === 'src' && r.dst === 'dst' ); expect(relationship).toBeDefined(); }); From a3f35f63677c03f813ce92bd2369fc5dfc3fda88 Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Mon, 27 Jan 2025 09:40:09 -0800 Subject: [PATCH 156/230] Fix parser test file --- .../src/diagrams/requirement/parser/requirementDiagram.spec.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/mermaid/src/diagrams/requirement/parser/requirementDiagram.spec.js b/packages/mermaid/src/diagrams/requirement/parser/requirementDiagram.spec.js index 743543f28..5875e143d 100644 --- a/packages/mermaid/src/diagrams/requirement/parser/requirementDiagram.spec.js +++ b/packages/mermaid/src/diagrams/requirement/parser/requirementDiagram.spec.js @@ -1,5 +1,5 @@ import { setConfig } from '../../../config.js'; -import requirementDb from '../requirementDb.js'; +import { RequirementDB } from '../requirementDb.js'; import reqDiagram from './requirementDiagram.jison'; setConfig({ @@ -7,6 +7,7 @@ setConfig({ }); describe('when parsing requirement diagram it...', function () { + const requirementDb = new RequirementDB(); beforeEach(function () { reqDiagram.parser.yy = requirementDb; reqDiagram.parser.yy.clear(); From b53cf0a1fb40f39d132ce5bf61c522d4dde209c2 Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Mon, 27 Jan 2025 10:33:45 -0800 Subject: [PATCH 157/230] Fix getConfig to return correct config --- packages/mermaid/src/diagrams/requirement/requirementDb.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mermaid/src/diagrams/requirement/requirementDb.ts b/packages/mermaid/src/diagrams/requirement/requirementDb.ts index 972c5e44e..b7c628040 100644 --- a/packages/mermaid/src/diagrams/requirement/requirementDb.ts +++ b/packages/mermaid/src/diagrams/requirement/requirementDb.ts @@ -345,5 +345,5 @@ export class RequirementDB implements DiagramDB { public getAccDescription = getAccDescription; public setDiagramTitle = setDiagramTitle; public getDiagramTitle = getDiagramTitle; - public getConfig = () => getConfig().class; + public getConfig = () => getConfig().requirement; } From f00507449b668aee3e37b5038f79b8480a70dfc7 Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Tue, 28 Jan 2025 07:51:06 -0800 Subject: [PATCH 158/230] Remove unnecessary default export --- .../mermaid/src/diagrams/requirement/requirementDiagram.ts | 2 +- .../mermaid/src/diagrams/requirement/requirementRenderer.ts | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/mermaid/src/diagrams/requirement/requirementDiagram.ts b/packages/mermaid/src/diagrams/requirement/requirementDiagram.ts index 246d91197..5ad89949a 100644 --- a/packages/mermaid/src/diagrams/requirement/requirementDiagram.ts +++ b/packages/mermaid/src/diagrams/requirement/requirementDiagram.ts @@ -3,7 +3,7 @@ import type { DiagramDefinition } from '../../diagram-api/types.js'; import parser from './parser/requirementDiagram.jison'; import { RequirementDB } from './requirementDb.js'; import styles from './styles.js'; -import renderer from './requirementRenderer.js'; +import * as renderer from './requirementRenderer.js'; export const diagram: DiagramDefinition = { parser, diff --git a/packages/mermaid/src/diagrams/requirement/requirementRenderer.ts b/packages/mermaid/src/diagrams/requirement/requirementRenderer.ts index 7e190f2c9..de18a08f3 100644 --- a/packages/mermaid/src/diagrams/requirement/requirementRenderer.ts +++ b/packages/mermaid/src/diagrams/requirement/requirementRenderer.ts @@ -34,7 +34,3 @@ export const draw = async function (text: string, id: string, _version: string, setupViewPortForSVG(svg, padding, 'requirementDiagram', conf?.useMaxWidth ?? true); }; - -export default { - draw, -}; From d0768cbc37d4fa5ce447382486e2054722f63420 Mon Sep 17 00:00:00 2001 From: yari-dewalt Date: Tue, 28 Jan 2025 08:10:07 -0800 Subject: [PATCH 159/230] Update shape --- .../shapes/requirementBox.ts | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/requirementBox.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/requirementBox.ts index da28f5db2..a41e1483a 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/requirementBox.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/requirementBox.ts @@ -18,7 +18,6 @@ export async function requirementBox ( node.labelStyle = labelStyles; const requirementNode = node as unknown as Requirement; const elementNode = node as unknown as Element; - const config = getConfig().requirement; const padding = 20; const gap = 20; const isRequirementNode = 'verifyMethod' in node; @@ -47,7 +46,7 @@ export async function requirementBox ( shapeSvg, requirementNode.name, accumulativeHeight, - node.labelStyle + node.labelStyle + '; font-weight: bold;' ); accumulativeHeight += nameHeight + gap; @@ -98,11 +97,8 @@ export async function requirementBox ( ); } - const totalWidth = Math.max( - (shapeSvg.node()?.getBBox().width ?? 200) + padding, - config?.rect_min_width ?? 200 - ); - const totalHeight = totalWidth; + const totalWidth = (shapeSvg.node()?.getBBox().width ?? 200) + padding; + const totalHeight = (shapeSvg.node()?.getBBox().height ?? 200) + padding; const x = -totalWidth / 2; const y = -totalHeight / 2; @@ -151,16 +147,18 @@ export async function requirementBox ( text.attr('transform', `translate(${newTranslateX}, ${newTranslateY + padding})`); }); - // Insert divider line - const roughLine = rc.line( - x, - y + typeHeight + nameHeight + gap, - x + totalWidth, - y + typeHeight + nameHeight + gap, - options - ); - const dividerLine = shapeSvg.insert(() => roughLine); - dividerLine.attr('style', nodeStyles); + // Insert divider line if there is body text + if (accumulativeHeight > typeHeight + nameHeight + gap) { + const roughLine = rc.line( + x, + y + typeHeight + nameHeight + gap, + x + totalWidth, + y + typeHeight + nameHeight + gap, + options + ); + const dividerLine = shapeSvg.insert(() => roughLine); + dividerLine.attr('style', nodeStyles); + } updateNodeBounds(node, rect); @@ -201,6 +199,9 @@ async function addText ( const textChild = text.children[0]; for (const child of textChild.children) { child.textContent = child.textContent.replaceAll('>', '>').replaceAll('<', '<'); + if (style) { + child.setAttribute('style', style); + } } // Get the bounding box after the text update bbox = text.getBBox(); From d155e414a0c4d463102032296283dfd056b187b6 Mon Sep 17 00:00:00 2001 From: homersimpsons Date: Sat, 1 Feb 2025 11:20:06 +0100 Subject: [PATCH 160/230] Add git graph preview in README.md --- README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/README.md b/README.md index 280725af7..047f8254c 100644 --- a/README.md +++ b/README.md @@ -257,6 +257,34 @@ pie ### Git graph [experimental - live editor] +``` +gitGraph + commit + commit + branch develop + checkout develop + commit + commit + checkout main + merge develop + commit + commit +``` + +```mermaid +gitGraph + commit + commit + branch develop + checkout develop + commit + commit + checkout main + merge develop + commit + commit +``` + ### Bar chart (using gantt chart) [docs - live editor] ``` From a0e5408850f3bbfec0ad6219f45d7becae80dd84 Mon Sep 17 00:00:00 2001 From: saurabhg772244 Date: Tue, 4 Feb 2025 16:41:54 +0530 Subject: [PATCH 161/230] convert stateDb to class, added test case. --- .../state/parser/state-parser.spec.js | 4 +- .../diagrams/state/parser/state-style.spec.js | 4 +- packages/mermaid/src/diagrams/state/shapes.js | 12 +- .../mermaid/src/diagrams/state/stateDb.js | 1131 +++++++++-------- .../src/diagrams/state/stateDb.spec.js | 30 +- .../diagrams/state/stateDiagram-v2.spec.js | 4 +- .../src/diagrams/state/stateDiagram-v2.ts | 6 +- .../src/diagrams/state/stateDiagram.spec.js | 4 +- .../src/diagrams/state/stateDiagram.ts | 6 +- packages/mermaid/src/mermaidAPI.spec.ts | 26 + 10 files changed, 669 insertions(+), 558 deletions(-) diff --git a/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js b/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js index 9fa8acab8..bb5345996 100644 --- a/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js +++ b/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js @@ -1,4 +1,4 @@ -import stateDb from '../stateDb.js'; +import { StateDB } from '../stateDb.js'; import stateDiagram from './stateDiagram.jison'; import { setConfig } from '../../../config.js'; @@ -7,7 +7,9 @@ setConfig({ }); describe('state parser can parse...', () => { + let stateDb; beforeEach(function () { + stateDb = new StateDB(); stateDiagram.parser.yy = stateDb; stateDiagram.parser.yy.clear(); }); diff --git a/packages/mermaid/src/diagrams/state/parser/state-style.spec.js b/packages/mermaid/src/diagrams/state/parser/state-style.spec.js index fed63c444..c37bed3c7 100644 --- a/packages/mermaid/src/diagrams/state/parser/state-style.spec.js +++ b/packages/mermaid/src/diagrams/state/parser/state-style.spec.js @@ -1,4 +1,4 @@ -import stateDb from '../stateDb.js'; +import { StateDB } from '../stateDb.js'; import stateDiagram from './stateDiagram.jison'; import { setConfig } from '../../../config.js'; @@ -7,7 +7,9 @@ setConfig({ }); describe('ClassDefs and classes when parsing a State diagram', () => { + let stateDb; beforeEach(function () { + stateDb = new StateDB(); stateDiagram.parser.yy = stateDb; stateDiagram.parser.yy.clear(); }); diff --git a/packages/mermaid/src/diagrams/state/shapes.js b/packages/mermaid/src/diagrams/state/shapes.js index f0ab4136b..b18b4ca0e 100644 --- a/packages/mermaid/src/diagrams/state/shapes.js +++ b/packages/mermaid/src/diagrams/state/shapes.js @@ -1,6 +1,6 @@ import { line, curveBasis } from 'd3'; import idCache from './id-cache.js'; -import stateDb from './stateDb.js'; +import { StateDB } from './stateDb.js'; import utils from '../../utils.js'; import common from '../common/common.js'; import { getConfig } from '../../diagram-api/diagramAPI.js'; @@ -414,13 +414,13 @@ let edgeCount = 0; export const drawEdge = function (elem, path, relation) { const getRelationType = function (type) { switch (type) { - case stateDb.relationType.AGGREGATION: + case StateDB.relationType.AGGREGATION: return 'aggregation'; - case stateDb.relationType.EXTENSION: + case StateDB.relationType.EXTENSION: return 'extension'; - case stateDb.relationType.COMPOSITION: + case StateDB.relationType.COMPOSITION: return 'composition'; - case stateDb.relationType.DEPENDENCY: + case StateDB.relationType.DEPENDENCY: return 'dependency'; } }; @@ -459,7 +459,7 @@ export const drawEdge = function (elem, path, relation) { svgPath.attr( 'marker-end', - 'url(' + url + '#' + getRelationType(stateDb.relationType.DEPENDENCY) + 'End' + ')' + 'url(' + url + '#' + getRelationType(StateDB.relationType.DEPENDENCY) + 'End' + ')' ); if (relation.title !== undefined) { diff --git a/packages/mermaid/src/diagrams/state/stateDb.js b/packages/mermaid/src/diagrams/state/stateDb.js index 1f12425e6..2da0719ca 100644 --- a/packages/mermaid/src/diagrams/state/stateDb.js +++ b/packages/mermaid/src/diagrams/state/stateDb.js @@ -1,28 +1,28 @@ +import { getConfig } from '../../diagram-api/diagramAPI.js'; import { log } from '../../logger.js'; import { generateId } from '../../utils.js'; import common from '../common/common.js'; -import { getConfig } from '../../diagram-api/diagramAPI.js'; import { - setAccTitle, - getAccTitle, - getAccDescription, - setAccDescription, clear as commonClear, - setDiagramTitle, + getAccDescription, + getAccTitle, getDiagramTitle, + setAccDescription, + setAccTitle, + setDiagramTitle, } from '../common/commonDb.js'; import { dataFetcher, reset as resetDataFetching } from './dataFetcher.js'; import { getDir } from './stateRenderer-v3-unified.js'; import { DEFAULT_DIAGRAM_DIRECTION, - STMT_STATE, - STMT_RELATION, - STMT_CLASSDEF, - STMT_STYLEDEF, - STMT_APPLYCLASS, DEFAULT_STATE_TYPE, DIVIDER_TYPE, + STMT_APPLYCLASS, + STMT_CLASSDEF, + STMT_RELATION, + STMT_STATE, + STMT_STYLEDEF, } from './stateCommon.js'; const START_NODE = '[*]'; @@ -46,15 +46,6 @@ function newClassesList() { return new Map(); } -let nodes = []; -let edges = []; - -let direction = DEFAULT_DIAGRAM_DIRECTION; -let rootDoc = []; -let classes = newClassesList(); // style classes defined by a classDef - -// -------------------------------------- - const newDoc = () => { return { /** @type {{ id1: string, id2: string, relationTitle: string }[]} */ @@ -63,564 +54,622 @@ const newDoc = () => { documents: {}, }; }; -let documents = { - root: newDoc(), -}; - -let currentDocument = documents.root; -let startEndCount = 0; -let dividerCnt = 0; - -export const lineType = { - LINE: 0, - DOTTED_LINE: 1, -}; - -export const relationType = { - AGGREGATION: 0, - EXTENSION: 1, - COMPOSITION: 2, - DEPENDENCY: 3, -}; const clone = (o) => JSON.parse(JSON.stringify(o)); -const setRootDoc = (o) => { - log.info('Setting root doc', o); - // rootDoc = { id: 'root', doc: o }; - rootDoc = o; -}; +export class StateDB { + constructor() { + this.clear(); -const getRootDoc = () => rootDoc; - -const docTranslator = (parent, node, first) => { - if (node.stmt === STMT_RELATION) { - docTranslator(parent, node.state1, true); - docTranslator(parent, node.state2, false); - } else { - if (node.stmt === STMT_STATE) { - if (node.id === '[*]') { - node.id = first ? parent.id + '_start' : parent.id + '_end'; - node.start = first; - } else { - // This is just a plain state, not a start or end - node.id = node.id.trim(); - } - } - - if (node.doc) { - const doc = []; - // Check for concurrency - let currentDoc = []; - let i; - for (i = 0; i < node.doc.length; i++) { - if (node.doc[i].type === DIVIDER_TYPE) { - // debugger; - const newNode = clone(node.doc[i]); - newNode.doc = clone(currentDoc); - doc.push(newNode); - currentDoc = []; - } else { - currentDoc.push(node.doc[i]); - } - } - - // If any divider was encountered - if (doc.length > 0 && currentDoc.length > 0) { - const newNode = { - stmt: STMT_STATE, - id: generateId(), - type: 'divider', - doc: clone(currentDoc), - }; - doc.push(clone(newNode)); - node.doc = doc; - } - - node.doc.forEach((docNode) => docTranslator(node, docNode, true)); - } - } -}; -const getRootDocV2 = () => { - docTranslator({ id: 'root' }, { id: 'root', doc: rootDoc }, true); - return { id: 'root', doc: rootDoc }; - // Here -}; - -/** - * Convert all of the statements (stmts) that were parsed into states and relationships. - * This is done because a state diagram may have nested sections, - * where each section is a 'document' and has its own set of statements. - * Ex: the section within a fork has its own statements, and incoming and outgoing statements - * refer to the fork as a whole (document). - * See the parser grammar: the definition of a document is a document then a 'line', where a line can be a statement. - * This will push the statement into the list of statements for the current document. - * - * @param _doc - */ -const extract = (_doc) => { - // const res = { states: [], relations: [] }; - let doc; - if (_doc.doc) { - doc = _doc.doc; - } else { - doc = _doc; - } - // let doc = root.doc; - // if (!doc) { - // doc = root; - // } - log.info(doc); - clear(true); - - log.info('Extract initial document:', doc); - - doc.forEach((item) => { - log.warn('Statement', item.stmt); - switch (item.stmt) { - case STMT_STATE: - addState( - item.id.trim(), - item.type, - item.doc, - item.description, - item.note, - item.classes, - item.styles, - item.textStyles - ); - break; - case STMT_RELATION: - addRelation(item.state1, item.state2, item.description); - break; - case STMT_CLASSDEF: - addStyleClass(item.id.trim(), item.classes); - break; - case STMT_STYLEDEF: - { - const ids = item.id.trim().split(','); - const styles = item.styleClass.split(','); - ids.forEach((id) => { - let foundState = getState(id); - if (foundState === undefined) { - const trimmedId = id.trim(); - addState(trimmedId); - foundState = getState(trimmedId); - } - foundState.styles = styles.map((s) => s.replace(/;/g, '')?.trim()); - }); - } - break; - case STMT_APPLYCLASS: - setCssClass(item.id.trim(), item.styleClass); - break; - } - }); - - const diagramStates = getStates(); - const config = getConfig(); - const look = config.look; - resetDataFetching(); - dataFetcher(undefined, getRootDocV2(), diagramStates, nodes, edges, true, look, classes); - nodes.forEach((node) => { - if (Array.isArray(node.label)) { - // add the rest as description - node.description = node.label.slice(1); - if (node.isGroup && node.description.length > 0) { - throw new Error( - 'Group nodes can only have label. Remove the additional description for node [' + - node.id + - ']' - ); - } - // add first description as label - node.label = node.label[0]; - } - }); -}; - -/** - * Function called by parser when a node definition has been found. - * - * @param {null | string} id - * @param {null | string} type - * @param {null | string} doc - * @param {null | string | string[]} descr - description for the state. Can be a string or a list or strings - * @param {null | string} note - * @param {null | string | string[]} classes - class styles to apply to this state. Can be a string (1 style) or an array of styles. If it's just 1 class, convert it to an array of that 1 class. - * @param {null | string | string[]} styles - styles to apply to this state. Can be a string (1 style) or an array of styles. If it's just 1 style, convert it to an array of that 1 style. - * @param {null | string | string[]} textStyles - text styles to apply to this state. Can be a string (1 text test) or an array of text styles. If it's just 1 text style, convert it to an array of that 1 text style. - */ -export const addState = function ( - id, - type = DEFAULT_STATE_TYPE, - doc = null, - descr = null, - note = null, - classes = null, - styles = null, - textStyles = null -) { - const trimmedId = id?.trim(); - // add the state if needed - if (!currentDocument.states.has(trimmedId)) { - log.info('Adding state ', trimmedId, descr); - currentDocument.states.set(trimmedId, { - id: trimmedId, - descriptions: [], - type, - doc, - note, - classes: [], - styles: [], - textStyles: [], - }); - } else { - if (!currentDocument.states.get(trimmedId).doc) { - currentDocument.states.get(trimmedId).doc = doc; - } - if (!currentDocument.states.get(trimmedId).type) { - currentDocument.states.get(trimmedId).type = type; - } + // Needed for JISON since it only supports direct properties + this.setRootDoc = this.setRootDoc.bind(this); + this.getDividerId = this.getDividerId.bind(this); + this.setDirection = this.setDirection.bind(this); + this.trimColon = this.trimColon.bind(this); } - if (descr) { - log.info('Setting state description', trimmedId, descr); - if (typeof descr === 'string') { - addDescription(trimmedId, descr.trim()); - } - - if (typeof descr === 'object') { - descr.forEach((des) => addDescription(trimmedId, des.trim())); - } - } - - if (note) { - const doc2 = currentDocument.states.get(trimmedId); - doc2.note = note; - doc2.note.text = common.sanitizeText(doc2.note.text, getConfig()); - } - - if (classes) { - log.info('Setting state classes', trimmedId, classes); - const classesList = typeof classes === 'string' ? [classes] : classes; - classesList.forEach((cssClass) => setCssClass(trimmedId, cssClass.trim())); - } - - if (styles) { - log.info('Setting state styles', trimmedId, styles); - const stylesList = typeof styles === 'string' ? [styles] : styles; - stylesList.forEach((style) => setStyle(trimmedId, style.trim())); - } - - if (textStyles) { - log.info('Setting state styles', trimmedId, styles); - const textStylesList = typeof textStyles === 'string' ? [textStyles] : textStyles; - textStylesList.forEach((textStyle) => setTextStyle(trimmedId, textStyle.trim())); - } -}; - -export const clear = function (saveCommon) { + /** + * @private + * @type {Array} + */ nodes = []; + /** + * @private + * @type {Array} + */ edges = []; + + /** + * @private + * @type {string} + */ + direction = DEFAULT_DIAGRAM_DIRECTION; + /** + * @private + * @type {Array} + */ + rootDoc = []; + /** + * @private + * @type {Map } + */ + classes = newClassesList(); // style classes defined by a classDef + + /** + * @private + * @type {Object} + */ documents = { root: newDoc(), }; - currentDocument = documents.root; - // number of start and end nodes; used to construct ids + /** + * @private + * @type {Object} + */ + currentDocument = this.documents.root; + /** + * @private + * @type {number} + */ startEndCount = 0; - classes = newClassesList(); - if (!saveCommon) { - commonClear(); + /** + * @private + * @type {number} + */ + dividerCnt = 0; + + static relationType = { + AGGREGATION: 0, + EXTENSION: 1, + COMPOSITION: 2, + DEPENDENCY: 3, + }; + + setRootDoc(o) { + log.info('Setting root doc', o); + // rootDoc = { id: 'root', doc: o }; + this.rootDoc = o; } -}; -export const getState = function (id) { - return currentDocument.states.get(id); -}; - -export const getStates = function () { - return currentDocument.states; -}; -export const logDocuments = function () { - log.info('Documents = ', documents); -}; -export const getRelations = function () { - return currentDocument.relations; -}; - -/** - * If the id is a start node ( [*] ), then return a new id constructed from - * the start node name and the current start node count. - * else return the given id - * - * @param {string} id - * @returns {string} - the id (original or constructed) - */ -function startIdIfNeeded(id = '') { - let fixedId = id; - if (id === START_NODE) { - startEndCount++; - fixedId = `${START_TYPE}${startEndCount}`; + getRootDoc() { + return this.rootDoc; } - return fixedId; -} -/** - * If the id is a start node ( [*] ), then return the start type ('start') - * else return the given type - * - * @param {string} id - * @param {string} type - * @returns {string} - the type that should be used - */ -function startTypeIfNeeded(id = '', type = DEFAULT_STATE_TYPE) { - return id === START_NODE ? START_TYPE : type; -} + /** + * @private + * @param {Object} parent + * @param {Object} node + * @param {boolean} first + */ + docTranslator(parent, node, first) { + if (node.stmt === STMT_RELATION) { + this.docTranslator(parent, node.state1, true); + this.docTranslator(parent, node.state2, false); + } else { + if (node.stmt === STMT_STATE) { + if (node.id === '[*]') { + node.id = first ? parent.id + '_start' : parent.id + '_end'; + node.start = first; + } else { + // This is just a plain state, not a start or end + node.id = node.id.trim(); + } + } -/** - * If the id is an end node ( [*] ), then return a new id constructed from - * the end node name and the current start_end node count. - * else return the given id - * - * @param {string} id - * @returns {string} - the id (original or constructed) - */ -function endIdIfNeeded(id = '') { - let fixedId = id; - if (id === END_NODE) { - startEndCount++; - fixedId = `${END_TYPE}${startEndCount}`; + if (node.doc) { + const doc = []; + // Check for concurrency + let currentDoc = []; + let i; + for (i = 0; i < node.doc.length; i++) { + if (node.doc[i].type === DIVIDER_TYPE) { + const newNode = clone(node.doc[i]); + newNode.doc = clone(currentDoc); + doc.push(newNode); + currentDoc = []; + } else { + currentDoc.push(node.doc[i]); + } + } + + // If any divider was encountered + if (doc.length > 0 && currentDoc.length > 0) { + const newNode = { + stmt: STMT_STATE, + id: generateId(), + type: 'divider', + doc: clone(currentDoc), + }; + doc.push(clone(newNode)); + node.doc = doc; + } + + node.doc.forEach((docNode) => this.docTranslator(node, docNode, true)); + } + } + } + getRootDocV2() { + this.docTranslator({ id: 'root' }, { id: 'root', doc: this.rootDoc }, true); + return { id: 'root', doc: this.rootDoc }; + // Here } - return fixedId; -} -/** - * If the id is an end node ( [*] ), then return the end type - * else return the given type - * - * @param {string} id - * @param {string} type - * @returns {string} - the type that should be used - */ -function endTypeIfNeeded(id = '', type = DEFAULT_STATE_TYPE) { - return id === END_NODE ? END_TYPE : type; -} + /** + * Convert all of the statements (stmts) that were parsed into states and relationships. + * This is done because a state diagram may have nested sections, + * where each section is a 'document' and has its own set of statements. + * Ex: the section within a fork has its own statements, and incoming and outgoing statements + * refer to the fork as a whole (document). + * See the parser grammar: the definition of a document is a document then a 'line', where a line can be a statement. + * This will push the statement into the list of statements for the current document. + * + * @param _doc + */ + extract(_doc) { + // const res = { states: [], relations: [] }; + let doc; + if (_doc.doc) { + doc = _doc.doc; + } else { + doc = _doc; + } + // let doc = root.doc; + // if (!doc) { + // doc = root; + // } + log.info(doc); + this.clear(true); -/** - * - * @param item1 - * @param item2 - * @param relationTitle - */ -export function addRelationObjs(item1, item2, relationTitle) { - let id1 = startIdIfNeeded(item1.id.trim()); - let type1 = startTypeIfNeeded(item1.id.trim(), item1.type); - let id2 = startIdIfNeeded(item2.id.trim()); - let type2 = startTypeIfNeeded(item2.id.trim(), item2.type); + log.info('Extract initial document:', doc); + doc.forEach((item) => { + log.warn('Statement', item.stmt); + switch (item.stmt) { + case STMT_STATE: + this.addState( + item.id.trim(), + item.type, + item.doc, + item.description, + item.note, + item.classes, + item.styles, + item.textStyles + ); + break; + case STMT_RELATION: + this.addRelation(item.state1, item.state2, item.description); + break; + case STMT_CLASSDEF: + this.addStyleClass(item.id.trim(), item.classes); + break; + case STMT_STYLEDEF: + { + const ids = item.id.trim().split(','); + const styles = item.styleClass.split(','); + ids.forEach((id) => { + let foundState = this.getState(id); + if (foundState === undefined) { + const trimmedId = id.trim(); + this.addState(trimmedId); + foundState = this.getState(trimmedId); + } + foundState.styles = styles.map((s) => s.replace(/;/g, '')?.trim()); + }); + } + break; + case STMT_APPLYCLASS: + this.setCssClass(item.id.trim(), item.styleClass); + break; + } + }); + + const diagramStates = this.getStates(); + const config = getConfig(); + const look = config.look; + + resetDataFetching(); + dataFetcher( + undefined, + this.getRootDocV2(), + diagramStates, + this.nodes, + this.edges, + true, + look, + this.classes + ); + this.nodes.forEach((node) => { + if (Array.isArray(node.label)) { + // add the rest as description + node.description = node.label.slice(1); + if (node.isGroup && node.description.length > 0) { + throw new Error( + 'Group nodes can only have label. Remove the additional description for node [' + + node.id + + ']' + ); + } + // add first description as label + node.label = node.label[0]; + } + }); + } + + /** + * Function called by parser when a node definition has been found. + * + * @param {null | string} id + * @param {null | string} type + * @param {null | string} doc + * @param {null | string | string[]} descr - description for the state. Can be a string or a list or strings + * @param {null | string} note + * @param {null | string | string[]} classes - class styles to apply to this state. Can be a string (1 style) or an array of styles. If it's just 1 class, convert it to an array of that 1 class. + * @param {null | string | string[]} styles - styles to apply to this state. Can be a string (1 style) or an array of styles. If it's just 1 style, convert it to an array of that 1 style. + * @param {null | string | string[]} textStyles - text styles to apply to this state. Can be a string (1 text test) or an array of text styles. If it's just 1 text style, convert it to an array of that 1 text style. + */ addState( - id1, - type1, - item1.doc, - item1.description, - item1.note, - item1.classes, - item1.styles, - item1.textStyles - ); - addState( - id2, - type2, - item2.doc, - item2.description, - item2.note, - item2.classes, - item2.styles, - item2.textStyles - ); + id, + type = DEFAULT_STATE_TYPE, + doc = null, + descr = null, + note = null, + classes = null, + styles = null, + textStyles = null + ) { + const trimmedId = id?.trim(); + // add the state if needed + if (!this.currentDocument.states.has(trimmedId)) { + log.info('Adding state ', trimmedId, descr); + this.currentDocument.states.set(trimmedId, { + id: trimmedId, + descriptions: [], + type, + doc, + note, + classes: [], + styles: [], + textStyles: [], + }); + } else { + if (!this.currentDocument.states.get(trimmedId).doc) { + this.currentDocument.states.get(trimmedId).doc = doc; + } + if (!this.currentDocument.states.get(trimmedId).type) { + this.currentDocument.states.get(trimmedId).type = type; + } + } - currentDocument.relations.push({ - id1, - id2, - relationTitle: common.sanitizeText(relationTitle, getConfig()), - }); -} + if (descr) { + log.info('Setting state description', trimmedId, descr); + if (typeof descr === 'string') { + this.addDescription(trimmedId, descr.trim()); + } -/** - * Add a relation between two items. The items may be full objects or just the string id of a state. - * - * @param {string | object} item1 - * @param {string | object} item2 - * @param {string} title - */ -export const addRelation = function (item1, item2, title) { - if (typeof item1 === 'object') { - addRelationObjs(item1, item2, title); - } else { - const id1 = startIdIfNeeded(item1.trim()); - const type1 = startTypeIfNeeded(item1); - const id2 = endIdIfNeeded(item2.trim()); - const type2 = endTypeIfNeeded(item2); + if (typeof descr === 'object') { + descr.forEach((des) => this.addDescription(trimmedId, des.trim())); + } + } - addState(id1, type1); - addState(id2, type2); - currentDocument.relations.push({ + if (note) { + const doc2 = this.currentDocument.states.get(trimmedId); + doc2.note = note; + doc2.note.text = common.sanitizeText(doc2.note.text, getConfig()); + } + + if (classes) { + log.info('Setting state classes', trimmedId, classes); + const classesList = typeof classes === 'string' ? [classes] : classes; + classesList.forEach((cssClass) => this.setCssClass(trimmedId, cssClass.trim())); + } + + if (styles) { + log.info('Setting state styles', trimmedId, styles); + const stylesList = typeof styles === 'string' ? [styles] : styles; + stylesList.forEach((style) => this.setStyle(trimmedId, style.trim())); + } + + if (textStyles) { + log.info('Setting state styles', trimmedId, styles); + const textStylesList = typeof textStyles === 'string' ? [textStyles] : textStyles; + textStylesList.forEach((textStyle) => this.setTextStyle(trimmedId, textStyle.trim())); + } + } + + clear(saveCommon) { + this.nodes = []; + this.edges = []; + this.documents = { + root: newDoc(), + }; + this.currentDocument = this.documents.root; + + // number of start and end nodes; used to construct ids + this.startEndCount = 0; + this.classes = newClassesList(); + if (!saveCommon) { + commonClear(); + } + } + + getState(id) { + return this.currentDocument.states.get(id); + } + getStates() { + return this.currentDocument.states; + } + logDocuments() { + log.info('Documents = ', this.documents); + } + getRelations() { + return this.currentDocument.relations; + } + + /** + * If the id is a start node ( [*] ), then return a new id constructed from + * the start node name and the current start node count. + * else return the given id + * + * @param {string} id + * @returns {string} - the id (original or constructed) + * @private + */ + startIdIfNeeded(id = '') { + let fixedId = id; + if (id === START_NODE) { + this.startEndCount++; + fixedId = `${START_TYPE}${this.startEndCount}`; + } + return fixedId; + } + + /** + * If the id is a start node ( [*] ), then return the start type ('start') + * else return the given type + * + * @param {string} id + * @param {string} type + * @returns {string} - the type that should be used + * @private + */ + startTypeIfNeeded(id = '', type = DEFAULT_STATE_TYPE) { + return id === START_NODE ? START_TYPE : type; + } + + /** + * If the id is an end node ( [*] ), then return a new id constructed from + * the end node name and the current start_end node count. + * else return the given id + * + * @param {string} id + * @returns {string} - the id (original or constructed) + * @private + */ + endIdIfNeeded(id = '') { + let fixedId = id; + if (id === END_NODE) { + this.startEndCount++; + fixedId = `${END_TYPE}${this.startEndCount}`; + } + return fixedId; + } + + /** + * If the id is an end node ( [*] ), then return the end type + * else return the given type + * + * @param {string} id + * @param {string} type + * @returns {string} - the type that should be used + * @private + */ + endTypeIfNeeded(id = '', type = DEFAULT_STATE_TYPE) { + return id === END_NODE ? END_TYPE : type; + } + + /** + * + * @param item1 + * @param item2 + * @param relationTitle + */ + addRelationObjs(item1, item2, relationTitle) { + let id1 = this.startIdIfNeeded(item1.id.trim()); + let type1 = this.startTypeIfNeeded(item1.id.trim(), item1.type); + let id2 = this.startIdIfNeeded(item2.id.trim()); + let type2 = this.startTypeIfNeeded(item2.id.trim(), item2.type); + + this.addState( + id1, + type1, + item1.doc, + item1.description, + item1.note, + item1.classes, + item1.styles, + item1.textStyles + ); + this.addState( + id2, + type2, + item2.doc, + item2.description, + item2.note, + item2.classes, + item2.styles, + item2.textStyles + ); + + this.currentDocument.relations.push({ id1, id2, - title: common.sanitizeText(title, getConfig()), + relationTitle: common.sanitizeText(relationTitle, getConfig()), }); } -}; -export const addDescription = function (id, descr) { - const theState = currentDocument.states.get(id); - const _descr = descr.startsWith(':') ? descr.replace(':', '').trim() : descr; - theState.descriptions.push(common.sanitizeText(_descr, getConfig())); -}; + /** + * Add a relation between two items. The items may be full objects or just the string id of a state. + * + * @param {string | object} item1 + * @param {string | object} item2 + * @param {string} title + */ + addRelation(item1, item2, title) { + if (typeof item1 === 'object') { + this.addRelationObjs(item1, item2, title); + } else { + const id1 = this.startIdIfNeeded(item1.trim()); + const type1 = this.startTypeIfNeeded(item1); + const id2 = this.endIdIfNeeded(item2.trim()); + const type2 = this.endTypeIfNeeded(item2); -export const cleanupLabel = function (label) { - if (label.substring(0, 1) === ':') { - return label.substr(2).trim(); - } else { - return label.trim(); - } -}; - -const getDividerId = () => { - dividerCnt++; - return 'divider-id-' + dividerCnt; -}; - -/** - * Called when the parser comes across a (style) class definition - * @example classDef my-style fill:#f96; - * - * @param {string} id - the id of this (style) class - * @param {string | null} styleAttributes - the string with 1 or more style attributes (each separated by a comma) - */ -export const addStyleClass = function (id, styleAttributes = '') { - // create a new style class object with this id - if (!classes.has(id)) { - classes.set(id, { id: id, styles: [], textStyles: [] }); // This is a classDef - } - const foundClass = classes.get(id); - if (styleAttributes !== undefined && styleAttributes !== null) { - styleAttributes.split(STYLECLASS_SEP).forEach((attrib) => { - // remove any trailing ; - const fixedAttrib = attrib.replace(/([^;]*);/, '$1').trim(); - - // replace some style keywords - if (RegExp(COLOR_KEYWORD).exec(attrib)) { - const newStyle1 = fixedAttrib.replace(FILL_KEYWORD, BG_FILL); - const newStyle2 = newStyle1.replace(COLOR_KEYWORD, FILL_KEYWORD); - foundClass.textStyles.push(newStyle2); - } - foundClass.styles.push(fixedAttrib); - }); - } -}; - -/** - * Return all of the style classes - * @returns {{} | any | classes} - */ -export const getClasses = function () { - return classes; -}; - -/** - * Add a (style) class or css class to a state with the given id. - * If the state isn't already in the list of known states, add it. - * Might be called by parser when a style class or CSS class should be applied to a state - * - * @param {string | string[]} itemIds The id or a list of ids of the item(s) to apply the css class to - * @param {string} cssClassName CSS class name - */ -export const setCssClass = function (itemIds, cssClassName) { - itemIds.split(',').forEach(function (id) { - let foundState = getState(id); - if (foundState === undefined) { - const trimmedId = id.trim(); - addState(trimmedId); - foundState = getState(trimmedId); + this.addState(id1, type1); + this.addState(id2, type2); + this.currentDocument.relations.push({ + id1, + id2, + title: common.sanitizeText(title, getConfig()), + }); } - foundState.classes.push(cssClassName); - }); -}; - -/** - * Add a style to a state with the given id. - * @example style stateId fill:#f9f,stroke:#333,stroke-width:4px - * where 'style' is the keyword - * stateId is the id of a state - * the rest of the string is the styleText (all of the attributes to be applied to the state) - * - * @param itemId The id of item to apply the style to - * @param styleText - the text of the attributes for the style - */ -export const setStyle = function (itemId, styleText) { - const item = getState(itemId); - if (item !== undefined) { - item.styles.push(styleText); } -}; -/** - * Add a text style to a state with the given id - * - * @param itemId The id of item to apply the css class to - * @param cssClassName CSS class name - */ -export const setTextStyle = function (itemId, cssClassName) { - const item = getState(itemId); - if (item !== undefined) { - item.textStyles.push(cssClassName); + addDescription(id, descr) { + const theState = this.currentDocument.states.get(id); + const _descr = descr.startsWith(':') ? descr.replace(':', '').trim() : descr; + theState.descriptions.push(common.sanitizeText(_descr, getConfig())); } -}; -const getDirection = () => direction; -const setDirection = (dir) => { - direction = dir; -}; + cleanupLabel(label) { + if (label.substring(0, 1) === ':') { + return label.substr(2).trim(); + } else { + return label.trim(); + } + } -const trimColon = (str) => (str && str[0] === ':' ? str.substr(1).trim() : str.trim()); + getDividerId() { + this.dividerCnt++; + return 'divider-id-' + this.dividerCnt; + } -export const getData = () => { - const config = getConfig(); - return { nodes, edges, other: {}, config, direction: getDir(getRootDocV2()) }; -}; + /** + * Called when the parser comes across a (style) class definition + * @example classDef my-style fill:#f96; + * + * @param {string} id - the id of this (style) class + * @param {string | null} styleAttributes - the string with 1 or more style attributes (each separated by a comma) + */ + addStyleClass(id, styleAttributes = '') { + // create a new style class object with this id + if (!this.classes.has(id)) { + this.classes.set(id, { id: id, styles: [], textStyles: [] }); // This is a classDef + } + const foundClass = this.classes.get(id); + if (styleAttributes !== undefined && styleAttributes !== null) { + styleAttributes.split(STYLECLASS_SEP).forEach((attrib) => { + // remove any trailing ; + const fixedAttrib = attrib.replace(/([^;]*);/, '$1').trim(); -export default { - getConfig: () => getConfig().state, - getData, - addState, - clear, - getState, - getStates, - getRelations, - getClasses, - getDirection, - addRelation, - getDividerId, - setDirection, - cleanupLabel, - lineType, - relationType, - logDocuments, - getRootDoc, - setRootDoc, - getRootDocV2, - extract, - trimColon, - getAccTitle, - setAccTitle, - getAccDescription, - setAccDescription, - addStyleClass, - setCssClass, - addDescription, - setDiagramTitle, - getDiagramTitle, -}; + // replace some style keywords + if (RegExp(COLOR_KEYWORD).exec(attrib)) { + const newStyle1 = fixedAttrib.replace(FILL_KEYWORD, BG_FILL); + const newStyle2 = newStyle1.replace(COLOR_KEYWORD, FILL_KEYWORD); + foundClass.textStyles.push(newStyle2); + } + foundClass.styles.push(fixedAttrib); + }); + } + } + + /** + * Return all of the style classes + * @returns {{} | any | classes} + */ + getClasses() { + return this.classes; + } + + /** + * Add a (style) class or css class to a state with the given id. + * If the state isn't already in the list of known states, add it. + * Might be called by parser when a style class or CSS class should be applied to a state + * + * @param {string | string[]} itemIds The id or a list of ids of the item(s) to apply the css class to + * @param {string} cssClassName CSS class name + */ + setCssClass(itemIds, cssClassName) { + itemIds.split(',').forEach((id) => { + let foundState = this.getState(id); + if (foundState === undefined) { + const trimmedId = id.trim(); + this.addState(trimmedId); + foundState = this.getState(trimmedId); + } + foundState.classes.push(cssClassName); + }); + } + + /** + * Add a style to a state with the given id. + * @example style stateId fill:#f9f,stroke:#333,stroke-width:4px + * where 'style' is the keyword + * stateId is the id of a state + * the rest of the string is the styleText (all of the attributes to be applied to the state) + * + * @param itemId The id of item to apply the style to + * @param styleText - the text of the attributes for the style + */ + setStyle(itemId, styleText) { + const item = this.getState(itemId); + if (item !== undefined) { + item.styles.push(styleText); + } + } + + /** + * Add a text style to a state with the given id + * + * @param itemId The id of item to apply the css class to + * @param cssClassName CSS class name + */ + setTextStyle(itemId, cssClassName) { + const item = this.getState(itemId); + if (item !== undefined) { + item.textStyles.push(cssClassName); + } + } + + getDirection() { + return this.direction; + } + setDirection(dir) { + this.direction = dir; + } + + trimColon(str) { + return str && str[0] === ':' ? str.substr(1).trim() : str.trim(); + } + + getData() { + const config = getConfig(); + return { + nodes: this.nodes, + edges: this.edges, + other: {}, + config, + direction: getDir(this.getRootDocV2()), + }; + } + + getConfig() { + return getConfig().state; + } + getAccTitle = getAccTitle; + setAccTitle = setAccTitle; + getAccDescription = getAccDescription; + setAccDescription = setAccDescription; + setDiagramTitle = setDiagramTitle; + getDiagramTitle = getDiagramTitle; +} diff --git a/packages/mermaid/src/diagrams/state/stateDb.spec.js b/packages/mermaid/src/diagrams/state/stateDb.spec.js index ff0581200..73f1a4be9 100644 --- a/packages/mermaid/src/diagrams/state/stateDb.spec.js +++ b/packages/mermaid/src/diagrams/state/stateDb.spec.js @@ -1,8 +1,9 @@ -import stateDb from './stateDb.js'; +import { StateDB } from './stateDb.js'; describe('State Diagram stateDb', () => { + let stateDb; beforeEach(() => { - stateDb.clear(); + stateDb = new StateDB(); }); describe('addStyleClass', () => { @@ -20,8 +21,9 @@ describe('State Diagram stateDb', () => { }); describe('addDescription to a state', () => { + let stateDb; beforeEach(() => { - stateDb.clear(); + stateDb = new StateDB(); stateDb.addState('state1'); }); @@ -73,3 +75,25 @@ describe('State Diagram stateDb', () => { }); }); }); + +describe('state db class', () => { + let stateDb; + beforeEach(() => { + stateDb = new StateDB(); + }); + // This is to ensure that functions used in state JISON are exposed as function from StateDb + it('should have functions used in flow JISON as own property', () => { + const functionsUsedInParser = [ + 'setRootDoc', + 'trimColon', + 'getDividerId', + 'setAccTitle', + 'setAccDescription', + 'setDirection', + ]; + + for (const fun of functionsUsedInParser) { + expect(Object.hasOwn(stateDb, fun)).toBe(true); + } + }); +}); diff --git a/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js b/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js index 53063f41a..d1edc5b40 100644 --- a/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js +++ b/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js @@ -1,11 +1,13 @@ import { parser } from './parser/stateDiagram.jison'; -import stateDb from './stateDb.js'; +import { StateDB } from './stateDb.js'; import stateDiagram from './parser/stateDiagram.jison'; describe('state diagram V2, ', function () { // TODO - these examples should be put into ./parser/stateDiagram.spec.js describe('when parsing an info graph it', function () { + let stateDb; beforeEach(function () { + stateDb = new StateDB(); parser.yy = stateDb; stateDiagram.parser.yy = stateDb; stateDiagram.parser.yy.clear(); diff --git a/packages/mermaid/src/diagrams/state/stateDiagram-v2.ts b/packages/mermaid/src/diagrams/state/stateDiagram-v2.ts index 8fd98e930..f7bc716c6 100644 --- a/packages/mermaid/src/diagrams/state/stateDiagram-v2.ts +++ b/packages/mermaid/src/diagrams/state/stateDiagram-v2.ts @@ -1,13 +1,15 @@ import type { DiagramDefinition } from '../../diagram-api/types.js'; // @ts-ignore: JISON doesn't support types import parser from './parser/stateDiagram.jison'; -import db from './stateDb.js'; +import { StateDB } from './stateDb.js'; import styles from './styles.js'; import renderer from './stateRenderer-v3-unified.js'; export const diagram: DiagramDefinition = { parser, - db, + get db() { + return new StateDB(); + }, renderer, styles, init: (cnf) => { diff --git a/packages/mermaid/src/diagrams/state/stateDiagram.spec.js b/packages/mermaid/src/diagrams/state/stateDiagram.spec.js index 7fcf4d0a6..8175ef041 100644 --- a/packages/mermaid/src/diagrams/state/stateDiagram.spec.js +++ b/packages/mermaid/src/diagrams/state/stateDiagram.spec.js @@ -1,9 +1,11 @@ import { parser } from './parser/stateDiagram.jison'; -import stateDb from './stateDb.js'; +import { StateDB } from './stateDb.js'; describe('state diagram, ', function () { describe('when parsing an info graph it', function () { + let stateDb; beforeEach(function () { + stateDb = new StateDB(); parser.yy = stateDb; }); diff --git a/packages/mermaid/src/diagrams/state/stateDiagram.ts b/packages/mermaid/src/diagrams/state/stateDiagram.ts index bd8383287..a6f9d7c63 100644 --- a/packages/mermaid/src/diagrams/state/stateDiagram.ts +++ b/packages/mermaid/src/diagrams/state/stateDiagram.ts @@ -1,13 +1,15 @@ import type { DiagramDefinition } from '../../diagram-api/types.js'; // @ts-ignore: JISON doesn't support types import parser from './parser/stateDiagram.jison'; -import db from './stateDb.js'; +import { StateDB } from './stateDb.js'; import styles from './styles.js'; import renderer from './stateRenderer.js'; export const diagram: DiagramDefinition = { parser, - db, + get db() { + return new StateDB(); + }, renderer, styles, init: (cnf) => { diff --git a/packages/mermaid/src/mermaidAPI.spec.ts b/packages/mermaid/src/mermaidAPI.spec.ts index e51d419d3..e1b13dbd7 100644 --- a/packages/mermaid/src/mermaidAPI.spec.ts +++ b/packages/mermaid/src/mermaidAPI.spec.ts @@ -71,6 +71,7 @@ import { decodeEntities, encodeEntities } from './utils.js'; import { toBase64 } from './utils/base64.js'; import { ClassDB } from './diagrams/class/classDb.js'; import { FlowDB } from './diagrams/flowchart/flowDb.js'; +import { StateDB } from './diagrams/state/stateDb.js'; /** * @see https://vitest.dev/guide/mocking.html Mock part of a module @@ -836,6 +837,31 @@ graph TD;A--x|text including URL space|B;`) }); it('should not modify db when rendering different diagrams', async () => { + const stateDiagram1 = await mermaidAPI.getDiagramFromText( + `stateDiagram + direction LR + [*] --> Still + Still --> [*] + Still --> Moving + Moving --> Still + Moving --> Crash + Crash --> [*]` + ); + const stateDiagram2 = await mermaidAPI.getDiagramFromText( + `stateDiagram + direction TB + [*] --> Still + Still --> [*] + Still --> Moving + Moving --> Still + Moving --> Crash + Crash --> [*]` + ); + expect(stateDiagram1.db).not.toBe(stateDiagram2.db); + assert(stateDiagram1.db instanceof StateDB); + assert(stateDiagram2.db instanceof StateDB); + expect(stateDiagram1.db.getDirection()).not.toEqual(stateDiagram2.db.getDirection()); + const flowDiagram1 = await mermaidAPI.getDiagramFromText( `flowchart LR A -- text --> B -- text2 --> C` From 56d66cdabcbce7dafa2ba3cc7044f4d6c1cf2d73 Mon Sep 17 00:00:00 2001 From: saurabhg772244 Date: Tue, 4 Feb 2025 16:45:45 +0530 Subject: [PATCH 162/230] Fix issue where data was not being set in the db after parsing. --- packages/mermaid/src/diagrams/state/stateDb.js | 1 + packages/mermaid/src/diagrams/state/stateRenderer-v3-unified.ts | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mermaid/src/diagrams/state/stateDb.js b/packages/mermaid/src/diagrams/state/stateDb.js index 2da0719ca..50785587e 100644 --- a/packages/mermaid/src/diagrams/state/stateDb.js +++ b/packages/mermaid/src/diagrams/state/stateDb.js @@ -130,6 +130,7 @@ export class StateDB { log.info('Setting root doc', o); // rootDoc = { id: 'root', doc: o }; this.rootDoc = o; + this.extract(o); } getRootDoc() { diff --git a/packages/mermaid/src/diagrams/state/stateRenderer-v3-unified.ts b/packages/mermaid/src/diagrams/state/stateRenderer-v3-unified.ts index 109417c03..2998c8173 100644 --- a/packages/mermaid/src/diagrams/state/stateRenderer-v3-unified.ts +++ b/packages/mermaid/src/diagrams/state/stateRenderer-v3-unified.ts @@ -36,7 +36,6 @@ export const getClasses = function ( text: string, diagramObj: any ): Map { - diagramObj.db.extract(diagramObj.db.getRootDocV2()); return diagramObj.db.getClasses(); }; From 9cad3c7aea3bbbc61495b23225ccff76d312783f Mon Sep 17 00:00:00 2001 From: saurabhg772244 Date: Tue, 4 Feb 2025 17:09:09 +0530 Subject: [PATCH 163/230] added changeset --- .changeset/grumpy-cheetahs-deliver.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/grumpy-cheetahs-deliver.md diff --git a/.changeset/grumpy-cheetahs-deliver.md b/.changeset/grumpy-cheetahs-deliver.md new file mode 100644 index 000000000..4213083f2 --- /dev/null +++ b/.changeset/grumpy-cheetahs-deliver.md @@ -0,0 +1,5 @@ +--- +'mermaid': patch +--- + +`mermaidAPI.getDiagramFromText()` now returns a new different db for each state diagram From 4dbaa2b5d6d3ffaa0dbbc7e78b4f3888d0850db8 Mon Sep 17 00:00:00 2001 From: saurabhg772244 Date: Tue, 4 Feb 2025 19:18:46 +0530 Subject: [PATCH 164/230] code refactor --- .../diagrams/state/parser/state-parser.spec.js | 8 -------- .../diagrams/state/parser/state-style.spec.js | 16 +--------------- packages/mermaid/src/diagrams/state/stateDb.js | 2 +- .../src/diagrams/state/stateDiagram-v2.spec.js | 5 +---- 4 files changed, 3 insertions(+), 28 deletions(-) diff --git a/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js b/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js index bb5345996..1ef6912d9 100644 --- a/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js +++ b/packages/mermaid/src/diagrams/state/parser/state-parser.spec.js @@ -20,7 +20,6 @@ describe('state parser can parse...', () => { const diagramText = `stateDiagram-v2 state "Small State 1" as namedState1`; stateDiagram.parser.parse(diagramText); - stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); const states = stateDiagram.parser.yy.getStates(); expect(states.get('namedState1')).not.toBeUndefined(); @@ -33,7 +32,6 @@ describe('state parser can parse...', () => { const diagramText = `stateDiagram-v2 namedState1 : Small State 1`; stateDiagram.parser.parse(diagramText); - stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); const states = stateDiagram.parser.yy.getStates(); expect(states.get('namedState1')).not.toBeUndefined(); @@ -44,7 +42,6 @@ describe('state parser can parse...', () => { const diagramText = `stateDiagram-v2 namedState1:Small State 1`; stateDiagram.parser.parse(diagramText); - stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); const states = stateDiagram.parser.yy.getStates(); expect(states.get('namedState1')).not.toBeUndefined(); @@ -62,7 +59,6 @@ describe('state parser can parse...', () => { state assemblies `; stateDiagram.parser.parse(diagramText); - stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); const states = stateDiagram.parser.yy.getStates(); expect(states.get('assemble')).not.toBeUndefined(); expect(states.get('assemblies')).not.toBeUndefined(); @@ -73,7 +69,6 @@ describe('state parser can parse...', () => { state "as" as as `; stateDiagram.parser.parse(diagramText); - stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); const states = stateDiagram.parser.yy.getStates(); expect(states.get('as')).not.toBeUndefined(); expect(states.get('as').descriptions.join(' ')).toEqual('as'); @@ -98,7 +93,6 @@ describe('state parser can parse...', () => { namedState2 --> bigState2: should point to \\nbigState2 container`; stateDiagram.parser.parse(diagramText); - stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); const states = stateDiagram.parser.yy.getStates(); expect(states.get('namedState1')).not.toBeUndefined(); @@ -122,7 +116,6 @@ describe('state parser can parse...', () => { inner1 --> inner2 }`; stateDiagram.parser.parse(diagramText); - stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); const states = stateDiagram.parser.yy.getStates(); expect(states.get('bigState1')).not.toBeUndefined(); @@ -139,7 +132,6 @@ describe('state parser can parse...', () => { stateDiagram-v2 [*] --> ${prop} ${prop} --> [*]`); - stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); const states = stateDiagram.parser.yy.getStates(); expect(states.get(prop)).not.toBeUndefined(); }); diff --git a/packages/mermaid/src/diagrams/state/parser/state-style.spec.js b/packages/mermaid/src/diagrams/state/parser/state-style.spec.js index c37bed3c7..3f0f6ab00 100644 --- a/packages/mermaid/src/diagrams/state/parser/state-style.spec.js +++ b/packages/mermaid/src/diagrams/state/parser/state-style.spec.js @@ -1,6 +1,6 @@ +import { setConfig } from '../../../config.js'; import { StateDB } from '../stateDb.js'; import stateDiagram from './stateDiagram.jison'; -import { setConfig } from '../../../config.js'; setConfig({ securityLevel: 'strict', @@ -18,7 +18,6 @@ describe('ClassDefs and classes when parsing a State diagram', () => { describe('defining (classDef)', () => { it('has "classDef" as a keyword, an id, and can set a css style attribute', function () { stateDiagram.parser.parse('stateDiagram-v2\n classDef exampleClass background:#bbb;'); - stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); const styleClasses = stateDb.getClasses(); expect(styleClasses.get('exampleClass').styles.length).toEqual(1); @@ -29,7 +28,6 @@ describe('ClassDefs and classes when parsing a State diagram', () => { stateDiagram.parser.parse( 'stateDiagram-v2\n classDef exampleClass background:#bbb, font-weight:bold, font-style:italic;' ); - stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); const styleClasses = stateDb.getClasses(); expect(styleClasses.get('exampleClass').styles.length).toEqual(3); @@ -43,7 +41,6 @@ describe('ClassDefs and classes when parsing a State diagram', () => { stateDiagram.parser.parse( 'stateDiagram-v2\n classDef exampleStyleClass background:#bbb,border:1.5px solid red;' ); - stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); const classes = stateDiagram.parser.yy.getClasses(); expect(classes.get('exampleStyleClass').styles.length).toBe(2); @@ -55,7 +52,6 @@ describe('ClassDefs and classes when parsing a State diagram', () => { stateDiagram.parser.parse( 'stateDiagram-v2\n classDef exampleStyleClass background: #bbb,border:1.5px solid red;' ); - stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); const classes = stateDiagram.parser.yy.getClasses(); expect(classes.get('exampleStyleClass').styles.length).toBe(2); @@ -67,7 +63,6 @@ describe('ClassDefs and classes when parsing a State diagram', () => { stateDiagram.parser.parse( 'stateDiagram-v2\n classDef __proto__ background:#bbb,border:1.5px solid red;\n classDef constructor background:#bbb,border:1.5px solid red;' ); - stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); const classes = stateDiagram.parser.yy.getClasses(); expect(classes.get('__proto__').styles.length).toBe(2); expect(classes.get('constructor').styles.length).toBe(2); @@ -83,7 +78,6 @@ describe('ClassDefs and classes when parsing a State diagram', () => { diagram += 'class a exampleStyleClass'; stateDiagram.parser.parse(diagram); - stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); const classes = stateDb.getClasses(); expect(classes.get('exampleStyleClass').styles.length).toEqual(2); @@ -104,7 +98,6 @@ describe('ClassDefs and classes when parsing a State diagram', () => { diagram += 'class a_a exampleStyleClass'; stateDiagram.parser.parse(diagram); - stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); const classes = stateDiagram.parser.yy.getClasses(); expect(classes.get('exampleStyleClass').styles.length).toBe(2); @@ -124,7 +117,6 @@ describe('ClassDefs and classes when parsing a State diagram', () => { diagram += 'a --> b:::exampleStyleClass' + '\n'; stateDiagram.parser.parse(diagram); - stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); const states = stateDiagram.parser.yy.getStates(); const classes = stateDiagram.parser.yy.getClasses(); @@ -163,7 +155,6 @@ describe('ClassDefs and classes when parsing a State diagram', () => { diagram += 'class a,b exampleStyleClass'; stateDiagram.parser.parse(diagram); - stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); let classes = stateDiagram.parser.yy.getClasses(); let states = stateDiagram.parser.yy.getStates(); @@ -182,7 +173,6 @@ describe('ClassDefs and classes when parsing a State diagram', () => { diagram += 'class a,b,c, d, e exampleStyleClass'; stateDiagram.parser.parse(diagram); - stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); const classes = stateDiagram.parser.yy.getClasses(); const states = stateDiagram.parser.yy.getStates(); @@ -210,7 +200,6 @@ describe('ClassDefs and classes when parsing a State diagram', () => { diagram += '}\n'; stateDiagram.parser.parse(diagram); - stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); const states = stateDiagram.parser.yy.getStates(); @@ -226,7 +215,6 @@ describe('ClassDefs and classes when parsing a State diagram', () => { stateDiagram.parser.parse(`stateDiagram-v2 id1 style id1 background:#bbb`); - stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); const data4Layout = stateDiagram.parser.yy.getData(); expect(data4Layout.nodes[0].cssStyles).toEqual(['background:#bbb']); @@ -236,7 +224,6 @@ describe('ClassDefs and classes when parsing a State diagram', () => { id1 id2 style id1,id2 background:#bbb`); - stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); const data4Layout = stateDiagram.parser.yy.getData(); expect(data4Layout.nodes[0].cssStyles).toEqual(['background:#bbb']); @@ -249,7 +236,6 @@ describe('ClassDefs and classes when parsing a State diagram', () => { id2 style id1,id2 background:#bbb, font-weight:bold, font-style:italic;`); - stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); const data4Layout = stateDiagram.parser.yy.getData(); expect(data4Layout.nodes[0].cssStyles).toEqual([ diff --git a/packages/mermaid/src/diagrams/state/stateDb.js b/packages/mermaid/src/diagrams/state/stateDb.js index 50785587e..cc44659eb 100644 --- a/packages/mermaid/src/diagrams/state/stateDb.js +++ b/packages/mermaid/src/diagrams/state/stateDb.js @@ -204,7 +204,7 @@ export class StateDB { * refer to the fork as a whole (document). * See the parser grammar: the definition of a document is a document then a 'line', where a line can be a statement. * This will push the statement into the list of statements for the current document. - * + * @private * @param _doc */ extract(_doc) { diff --git a/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js b/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js index d1edc5b40..ce20879c3 100644 --- a/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js +++ b/packages/mermaid/src/diagrams/state/stateDiagram-v2.spec.js @@ -1,6 +1,5 @@ -import { parser } from './parser/stateDiagram.jison'; +import stateDiagram, { parser } from './parser/stateDiagram.jison'; import { StateDB } from './stateDb.js'; -import stateDiagram from './parser/stateDiagram.jison'; describe('state diagram V2, ', function () { // TODO - these examples should be put into ./parser/stateDiagram.spec.js @@ -129,7 +128,6 @@ describe('state diagram V2, ', function () { `; stateDiagram.parser.parse(diagram); - stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); const rels = stateDb.getRelations(); const rel_1_2 = rels.find((rel) => rel.id1 === 'State1' && rel.id2 === 'State2'); @@ -404,7 +402,6 @@ describe('state diagram V2, ', function () { `; stateDiagram.parser.parse(diagram); - stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2()); const states = stateDb.getStates(); expect(states.get('Active').doc[0].id).toEqual('Idle'); From 4a19740aea90a20447500e1dfab7a3dae8a26e4f Mon Sep 17 00:00:00 2001 From: "Radmila M." Date: Mon, 10 Feb 2025 13:24:38 +0300 Subject: [PATCH 165/230] Update tutorials.md --- .../mermaid/src/docs/ecosystem/tutorials.md | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/packages/mermaid/src/docs/ecosystem/tutorials.md b/packages/mermaid/src/docs/ecosystem/tutorials.md index 7258361bf..82c272a4e 100644 --- a/packages/mermaid/src/docs/ecosystem/tutorials.md +++ b/packages/mermaid/src/docs/ecosystem/tutorials.md @@ -42,16 +42,7 @@ https://codepen.io/Ryuno-Ki/pen/LNxwgR ## Mermaid in open source docs -[K8s.io Diagram Guide](https://kubernetes.io/docs/contribute/style/diagram-guide/) - -[K8s.dev blog: Improve your documentation with Mermaid.js diagrams](https://www.kubernetes.dev/blog/2021/12/01/improve-your-documentation-with-mermaid.js-diagrams/) - -## Jupyter Integration with mermaid-js - -Here's an example of Python integration with mermaid-js which uses the mermaid.ink service, that displays the graph in a Jupyter notebook. - -```python -import base64 +[K8s.io Diagram Guide](https://kubernetes.io/docs/contribute/style/diagram-guide/)import base64 from IPython.display import Image, display import matplotlib.pyplot as plt @@ -61,6 +52,28 @@ def mm(graph): base64_string = base64_bytes.decode("ascii") display(Image(url="https://mermaid.ink/img/" + base64_string)) +[K8s.dev blog: Improve your documentation with Mermaid.js diagrams](https://www.kubernetes.dev/blog/2021/12/01/improve-your-documentation-with-mermaid.js-diagrams/) + +## Jupyter / Python Integration with mermaid-js + +Here's an example of Python integration with mermaid-js which uses the mermaid.ink service, that displays the graph in a Jupyter notebook and save it as *.png* image with the stated resolution (in this example, `dpi=1200`). + +```python +import base64 +import io, requests +from IPython.display import Image, display +from PIL import Image as im +import matplotlib.pyplot as plt + +def mm(graph): + graphbytes = graph.encode("utf8") + base64_bytes = base64.urlsafe_b64encode(graphbytes) + base64_string = base64_bytes.decode("ascii") + img = im.open(io.BytesIO(requests.get('https://mermaid.ink/img/' + base64_string).content)) + plt.imshow(img) + plt.axis('off') # allow to hide axis + plt.savefig('image.png', dpi=1200) + mm(""" graph LR; A--> B & C & D; @@ -73,6 +86,6 @@ graph LR; **Output** - + From a79c0f4c002198faee2eb585dab2461d34c8a174 Mon Sep 17 00:00:00 2001 From: "Radmila M." Date: Mon, 10 Feb 2025 14:01:09 +0300 Subject: [PATCH 166/230] Update tutorials.md --- .../mermaid/src/docs/ecosystem/tutorials.md | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/packages/mermaid/src/docs/ecosystem/tutorials.md b/packages/mermaid/src/docs/ecosystem/tutorials.md index 82c272a4e..146ffb282 100644 --- a/packages/mermaid/src/docs/ecosystem/tutorials.md +++ b/packages/mermaid/src/docs/ecosystem/tutorials.md @@ -42,15 +42,7 @@ https://codepen.io/Ryuno-Ki/pen/LNxwgR ## Mermaid in open source docs -[K8s.io Diagram Guide](https://kubernetes.io/docs/contribute/style/diagram-guide/)import base64 -from IPython.display import Image, display -import matplotlib.pyplot as plt - -def mm(graph): - graphbytes = graph.encode("utf8") - base64_bytes = base64.urlsafe_b64encode(graphbytes) - base64_string = base64_bytes.decode("ascii") - display(Image(url="https://mermaid.ink/img/" + base64_string)) +[K8s.io Diagram Guide](https://kubernetes.io/docs/contribute/style/diagram-guide/) [K8s.dev blog: Improve your documentation with Mermaid.js diagrams](https://www.kubernetes.dev/blog/2021/12/01/improve-your-documentation-with-mermaid.js-diagrams/) @@ -76,16 +68,16 @@ def mm(graph): mm(""" graph LR; - A--> B & C & D; - B--> A & E; - C--> A & E; - D--> A & E; - E--> B & C & D; + A--> B & C & D + B--> A & E + C--> A & E + D--> A & E + E--> B & C & D """) ``` **Output** - + From d07f85e6ac4cbd4a75ddfad395884678d5f8c2f8 Mon Sep 17 00:00:00 2001 From: "Radmila M." Date: Mon, 10 Feb 2025 14:05:55 +0300 Subject: [PATCH 167/230] Add files via upload --- .../img/python-mermaid-integration-updated.png | Bin 0 -> 261810 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 packages/mermaid/src/docs/ecosystem/img/python-mermaid-integration-updated.png diff --git a/packages/mermaid/src/docs/ecosystem/img/python-mermaid-integration-updated.png b/packages/mermaid/src/docs/ecosystem/img/python-mermaid-integration-updated.png new file mode 100644 index 0000000000000000000000000000000000000000..37ad58420671462588b204632c0bdcbc3d2aa85c GIT binary patch literal 261810 zcmeEv2Ut^g+c&Lk?ND(%R;mbf03r%%WCR&)MUh<)L6D(>MA;CKnS|ClQK$?tGEzlk z8Bj3nBseK6Q7}YiL?AH)2rI-862AY5i$1OReOKSFm(Tm0TvwZuoET2-bI;$sPXB0a zI{(Y%U&_hJ%{SX+Vkal31P;HB`TR5R)Jf7D15e119eYD|2Y7{q9SA-oXLTSX&^I8& z_sGF-j~)s}9|`bBXzFMpHhg P7)&Z~*gss4AIs!I>upa OSg=WOf&^9RfyFn`QK0ocC6_H`Eafb}P=KWAf) z|118u*R}F<=u18yEp^-f%i=F)`NI95&t7Q_pNHu)3;W2y`~mX^%pdQj04yJ{e7u`G z a|dicVEKUM z1D20>Qvj9^SU%p(9kBg? iS6Te- zE7~qM`xJ5bJWQW=bB~-H%pWj+!2I!U3c&IK%g4L91J<9ge8BPn%g4JZ0Lup~AMfT4 z*nYtB0m}z0AMd6BEFZ9Zyqh~<`vJ=bEFZ9Zyqf~Be8BSYZtj5X2P_}3e8BSYZVJHi z0n5j`xdXNzABcP`j1rdgY+d~6kB_bIPUAK$q_0_iBo?3cS+;LWq*ijJi 9OaPbz zU=f&oFBz-@U?n)aCj?so*bFMnYNvmO{Q%eznw3LfPY`y6_s_~9a4Y}^L)Ei#2pkQ< z;V_H`%sRvj21H;)2!_O9OmH?k3WK6BC<=q3Feo~kj);L#;s5QZ@Qcx*8Fn&;+8Cs$ z72hh9$Sc j{r& zXjR2AT{}ZJ(mGCtxo($}d)ieHI8Cz|GW #&QCMg$b+DGq3*%Yn;*DBQw_!Z`g88_`u zhku(Z{;smWgH@~<89Mg%!eXeky=qm>1jTc;o$ypE1$5{}`bqpQRV!*9C^yH|>^ zFZnRW$5}$Y9(JA(#zta|2wL&@j2?vW1m<%&N5LeiwnU;p(=Kz P4%N+|#_Yq(=>ymYicuo(-PYhq9Z+XD+*)M7QN=~lo(djU?x4-Bw zjsjQ0fL--p(B vBSJg+aV_Bx5-nN>d9H4-(e#)Jhe(e$T zE!S#v%TVcECI!+B9u+6D4VmC*+qJQKlFpvHl8HLSUfnLV3zjV zXII#VRkeTqJSg&(^SO;zem*Dot>jz3+h|6t! M(mUik=*YO^ zLGJQB6y$~az6#xLes9`H+U3IeZ7p=~=VKqsS! 7`_Efq@dk z*@k$Te%p_zJsxqBNIGGp^;kdR%{!M{YJKKo7Wn&scVex6#XbPPU|wTMSNX>?e#)N? zt53r`1Bmh=NX}V@e1>=DiWw#>b)D;(ZOhLU3|}yydaj7_cRy%EFfHeXL>@2g6>UtZ z*9?8o6O^JqH64k0z#X~RtJ9qZK$YvJ!SavC@b@0_<2wVuw>jDc$KU({he+<2Bd7}K z4-IfJXNfyJsR=>KljLn`OrGv|;`0dFunP)(mDBHr#%{rf sAk}RG9%q#82^c_X1?(u(DuKA$u~O_9r8m1fcAO) zxY2r{{!o&5FaXz{RvyB&v*+D8{#7GBGcz;Cp~yW&%iW)*sHCKWGOwM`SQ&f&;3J~L z>h#JnQ57 ZvYw%{!*80HiCoW0}MlN&nMCbAPG<{WV-)0LTKR)Yx$C`1-hsVt&H zB-T96L++{h=(aZJ>qR^|@$r~4xjE~enPz998>#8$k3MMcH+oQ}D`#rPuSnKzKF`sC z`+%-=3k;UHd2DPKyVRhU$jOyClugbI&0iDo&2!8@;G=v1Zqf&E2{S#l7>xrY=g?Zf zM&Sl=!NjIP3*fh{D|OKX9QZ<|=lqbPR_y)Nbylj08dWOv03(7f>RL0leo5k>MNPo- zvE1yQo*s6CuTHGDYgyoB rb+@8u8HOtulYLsa)-+^jzrDRiYvEOgNZ-MMfvu>cVPTohwK}RCp L21~o!x0D%*Xh9`WUCKp|>pml=O9X)ZWDX^X{51j(z&8 z<$}bk*6LZ7357iw@u FGHR&z?U04VmcA3ODelU9Isb^w7XtWO BSBo zQ#K(fVa6Y^8`i`dG^Lqw?%$O+?nsN^TDUJ~M2hO&yVA-f6YZJK*){}!T1R-#Ia}gB z|MDy(S|VWTqTF8GSq9m(I7x4Zz4_588`Z>|mWPGQ5 Z$E)U=6PAu|8An|Ml%?13_o)?sAv|W(li1o48e1DM!JRylQ4&PS_UQ(e z$+*YUNX19bLj4d9M@1%Hui225bocIEwq?<0BQhInXCA@Eqb?!1$c;|?aBfppPD`_m zac5e@v(svq5VbFo?do!OXJ0~aXz8)uDn7=|9=t 3E8Na# z9dn61+{hW^baHR8qSUzxJF|-J8Yj5iQFimE4|tt40zc8wVl>hE#fHpJ=wBw7dL&}w zf4+iJ%^exWIc#X{RhSra0_byHkl+tMz#9R4eE>(45GDQrD*StNFkBj`d1U#yot@r( z`F+8yY2U=x94k!LGcxk0HFy>0-%tsiVHNqq!#9v)&okq7P(zh`3fuCM_)$FeUkrUT z+Y-&bu+RUrH<_(nc!bt*wB4@}-yT3EL;RO*o$gH=Hz Exi>& zh%ATz4jxng7dwOcCu_MC7d**k`dSI)?PFo%88z6Vj2oP`-|P!Xin$rQ{ZR!+R!&ZK zdAeO?eq|`s2pV~Ie_dGjo#UGLxk}-`L08A269Q;c 5t#;f&o z0_F5#6S8Cy!)%N%>S$ipZrnQ;JM|-4mF#%Y^5?N;gQ~CH57n#%F^{TqTsz^=h6{6T ziJg*kc0*r%N-6?39p64Z=eHy~Yu1wtC(qJym52>W(_5Pb**(2@f;bTJFy}=bpMj;m z7NQx?*H3>|J>%t{4?hHz|6gHy?;i%hU<-NCn_Ga>aBbQui^g4?YOw;v*o+<~nhF9e zdmjTO({cyHl;JGGh%Qys$ZjADyMWs`6h;Mcmx!I3iU5w7Bl=pBotvo65eQ*fPh3um zREkBb0&Iuf{gB=a9M0(Ijx@Y=wk=T>h-r<93?Q$T-Jsf#-Mj7Zb5?h1wKf1D<#0Jx z;YI*3l$)1I4UCH8D7S05#atqtPpx` zJfMEp-R9yh=C>1yv7SbaUd1^rLW+>gZa5}yq`;UQiN^sO+n)=JQoP~BUg&CpK((hu z++ESNkP+Flg7qY=oO> YJz4pQBR2|FQdCyl9a-nZVqvYf>DSQ+ z+g8#Eb5q(2RfI>2g=>@|o*zWB3n^XFu7-SfNk7|CJ*$Cpd-+&`YiiOSvA$&7FR#~s zRc>-;at_H109$iQNi7dK_e)xqPgecN>wWXiac8wc?Qvw84E!=zKZN`;gJfM4)!&;e zJ*gCV6**=z6uB?zvR$19EzMj0cs!~?YVt&|$Zm^n3BTMB_)AQV@|UwZmizDiS3m_H zfCT@^Z&dwevMB%2;afviFUWE6@s$i`%@kYzu&`1jf wJOkh$bNbCkCRVv77 zsq7LIup4-8!it=heO;p$*bUs~>{LYT{VR}xNUXhzLfTCY^3mm!klcM|MHdS* zBD;3WV>UoaLC1_PSju(qEUjtk04b-rAB->S9CYw+qM)c52B5q1JId8T8VVSS;(8 zAe#psE}jB!SFDn3JepdJ7K!=A{^&N$;u=2``K)2ez_@(;hVzjl+f+xM@KTULv| z)Y`Ufu)S>W%AKTyZiV;+ZDDKZ(8%}VGWyRW7LD!lHFm%bD*NE3pH{L&ZfJHof2s!j zKyx#<{LiT+b?YxK&m1oCTxxmls)L;=u`%zB@RUFT_6KpAi6z^btD9|*98nGb#a`Oa zn =-3Ya&G0X9tJfty&&Cg5t5~}NFG&F~^NolT>x9@M0PvE`Gfw74CsC0(P=fQ5YH zF0 z4d+6{37ZEZtcxeckX%$+Z)Cyuijy~S;o%B>0H`U1FmyMMiv{IYagvOh5fD%+GA3zZ z6o8hr(y=XRNSrhpGI?DhT9^*1OEMH@N-CgACntUk*GRm#Gt;+obfil1f?y@Qgs4As zlx7u|aOkK)N>9(kc~)m*LFhEQBe4;$oFNWEvpe}%3+TJPzRet2U#0E#>Gq3V2TW=l zvh}_F9Nr{s-jl+)^<-ePl6R&f(y1B7IQ zPq;!$pq=&>T9qB2HPsU{^tXBDhdKLSam!yn!NLpCCFf#66u`t^J@!&X(Mo;SnkNn& zUZ&-C)*AyrjsRM;Am9C*H<^#&`_Q8McN4r{ss65}^79uVBgOCzU0TvxXI2Vi@$v23 z-=n<(0{H8F%{n3AWW(37IW3-Bj?mV&Gxr4-SSu l z6(O6)5M3>ti?yf@^aB4ok?EcWK!9s2N f zT^)e%PIwqcAG)mgnY*NTSa7Z{2eYs_Kr*rI?DhXALpO4LLOjfp>g3;AQuVCLS%v;Z z(WBvFPV%n?mGq2H!A(p>ZF97kRjjJNI09$gKa32jC6oNtx#tx@;oBFE_oIyhpe)x; zl98y^Mh8V1ITv6;GJ0`a9vn*s158Q42fX3}k_cCj2)yML=jJ0~ySw90fGKp{0&u#7 zbKSw^09+6!%iKs1`e>sfl^nUzHtYu=BjGK0>;uJ8nxO#j&-i9i@WZ5N!hpWEX$%!? zd5sWMbgs5cXh8S9Bm5fF*KPsWNc}^C1<(!jAnN_($79&Q6`UcRq221Ds$Id`-Q^Rd zCFy5(R4m9%vD)kY2u-%8Yh!B_CY;yc`y+g_#0=ilW3-`dk+pDbxmzSH_Nqfg*G t?nkrSIfx#1q)r+`o}?44oo=Hnd2X8NjFT>#&P5g< z#%bpPKO8}K@usa!`M!QQ z^7gVjG$oB^p?-2$iUB!F1^kuq*&xBnYl~tu_Dp`5(a9;P4NS-u~aw zMrK7{@Hc&++5qi!bMID^YN8qB-+(AB)8(K}sy2!}7GduL#ZC^~7nts^+(9H0#=_VV zOR18 i!zjd(t88mEntwi@Hm z&^2ZtKkqWUbb_yRZfZ~uNZ{Zuso8;BiaSU U{tV zhxXdKd A7wJ0Nu1isEd*|QY9esI=(W!FG##i9~4oX@y77> zvAk(% u1&{JU^iC=e{0tdKqo4t*3)ed#(Vb z2Oto(TOco=im0rtltszxhQnI{qYQW``_QU-RkV)KqDNxgeIZ0^zHZ0DzQwiC5Z*-; zMR%p)p~%|AP9y!;%-g^HeARY)^9d!}$%0*@Zgt^l **&5W5^!4;k1v3Xrs8L%9 zDE=HnQ7;HZL7rz@p5Qz#EDG%f E)-)E) z-mX0Pqdl*gAPqsYY1{a|G|TMP#f8Au8X3iTtLPjsQt8>8+E{Ss!sCr!$FeQ;2{Ue+ z9F~y5kG`59bh_iXzfLs9Nkt?+CB^d!sT4zC4a-6vLq#tqa#|ieszQ!E+%CU^G(-^( zu^aZWCQH~2RhSgOP-*%kmYYwjG~?BPs=&}+j90x4e`o%=SI5;yoBso<#s4cb-Att7 zx05p6V_n>*4U|?R{{F#5j7yfP0B$<`IL%2j1p>~4rUy{QD;SZz=S?pmge)A+UgMki z5Kz{PGLSKuZ-HCSdBrtZy_}hyJ297cb>)8PmZ}cQ{FLHT5x_;Gp7QRD >2ak*MsejcSH+bGLcKfAXR^M zF4O*Q&i b<>J$|fO-P_N RknXNS ze549xs&Bf9*iuNCq&DIu5=l$Tzw4qX?v>g zL*)@xlZ|07`QWK=jJNU~Eb;Q~f546X01|Nj370Sv(Rp~+o8(O!7#!R|0)b~LqPTTK z9%=7}O9&f-5fe0fj`EA|pn%V4 97tEme*ZuJCSmWzU^z^(mC_JEC@b%v1PyMBb|A@=9k#lLoy*cNRLU@62C$ z+2p~quRa|)w{H8n _MyZFVP2&KeO8d*3NG2JH`B(-$$O6jfiNZAF%ShV} l zn{dnZtAilj!fgoIvXRjy5Gh?Ocp`txnH?1`t<~@*t(U|kONrh-tvU770>8^zuY#8F zh 2d z{bI|2@MfxBgdL!b8IN8{l~V=b4YzrMLQ* zW;p|enBF>gG|SmoIuhRu)mj;*M-1Pd+l)VT%BP_GT2j73d=1x<8JguRmSBIi48<&E z@W{a(`OK(8=aPjUCWFy??2S@ad)Ea1&ii7
!_**_t z?o9wb AyNSyy-peA$$oC*_KZxy1gMEwcV>&y>C()GHu{V zWf8^~WAjJCUFrspR)(;Dnya) g`Gs!}euEhZ5aS=9!U^eBkXe5D z2+RUXi)+b}7b4;%SOU$rwB3$a$IH8}b>(PBO;1p6OJ=;$C7OYFpg7AJV83Dd#jU h1-NuWuik%E=j7tHgGB80+nVcKi4pS;E8ALO}{hq=Z~6qMSMqHNKwd zpOrHB{KdNT)2A*9=5ia>;aV?*l1eChHB-0^-XwF>mnnWGCL0_@KBc_axpSUwv1>(J zsgmgQx``uXn(&6QGKX)xh$L|(xl` =y5k_tkn3 8wwjdQ$4poG(q0l0?<;NN<*)1hFx#v~9`0^$uQ `ln1BkPjgN(y-9 zz>zX<_Zuv8oYz|KfN{<8(H)iSixsU-7g6tz^7bil#H-h=_8L24WM7rG9&<5SDD_N; zGcmc>9h^!FAxjNnD8o)=E-jN+G`xzZ&K)o!Bzx~s3b@h8!meI@rO1#H9^{m#t={2M z)6?cwT4Y)fU9-;U%oTf|eFn7|FQeB864i2craMG~neHE9y7xLRmyNlfzSagVM*uVG z7kBzwkU&~YT{o%GE0GrB>X;`er(-nJp`zIz^8}#42$9Yex7kFjueo98vju+@vFI*w zrh^qzUk~x~iGjPo9%nfxV^h6}m+i$*;^N|(@b{lt<~sDXJ+Elv0dSbxfO`=eb`640 z#E`d$n~tOILILi~b?9Y1u#H|}%A7xDR33iw!Zy8&SqPqAx7^1rUhCV>dKZrRms+4& zcvI(m${U!cbB0jfWKpt*9)ZITU9{-9A5MMABHLC6NAEq<6nh#o#JRGobmAN6r~c|7 zj@5YmoB#6Jg$+A|fH41nPx{;@@{M1Vk_WU`d8dC0?QBiYZSQkCGef+ntZc+05>p_e z<0$sLHlIQ(6sV6411ibQvCn0P$v8%;_FeKA$W1i??rHUPsC)Bu;0FXA${$DWiS)}m zF_4?57@OA@jHiubh)n;Rdy|5fGL*%xxHjI0-L=T}+Vp>MvO|(yT0T)rX1;E_k2& zGG(X0o;-X<^aC;aCU@(~)2B`eDjsfsGGEv=SC1GMugh)lv}<2-)KyBk(aAPusU@0$ zueiCl7kz)D&xRCHkCBqRaO${s0QQi(Yeazw%aI^|-uM7dP-34yStT2cQI;Ue2g%r3 z9{(MI<$pqWJ+4N6eV<}17yz&U6>vxNi&=aN($Pv+{br$vkYwL)V$y*Tc5)ju+(mVE zL?I}L<5~*J3Yk2+f-Ag=5RlN#f?%HXnS2_PH+VFaW F zsYCvuTu>EfhRWlVHM#5b3Z%pUj7B=>xSMv$3kB12RMJI`)1&d`q=Jfm7yaEx&{s>M z+(j4u?ru|)@4PXlzBc3%AH(uL$!jbeSe9ovP=5%EVMy+H+98KXhTWdP!Q3M}BGPs! zh<$W8P3G73Mb8(KWgYhgA=2eDVawM)|LrpjtU$E*2dMBz<_pnZdA$XUAW$#eyP*jW zsmzWjKACH2at|b4f2DPRG(?v3_I~jcJ$PrPZEg$XXg=r}`4;F%L1a43QZ>=;53x&u zuPbxD0}r&+B}IW2&n7%5;*2`xxq5X*)KGZ{XxEHZ-e+=ew~b*kZK_*YI%Y>yBSddW z7xfrpcjvaSq=T%hh-B>jer-K2o7(`ILS6H~l!LG=#5DUtSdw>V@EFb#rITu w%+{c0_K7bKM~#(3<{R@KyITW@^i)=9`oN$_O~H$ewrkq>?!b zYOJ;+@(L<}lM2dKF3AdyA!#5n-BM{TIE++#hY2Vs&PEhF5t&0G4nU-(-O3pnjL3nT z(-|~hoz#$Pe4qG~u7Xl-1D3$Y$DVot1|;BV_FeId7ESYTQy%*0W-F1 2v7yBTj2{cH(fMTx?KL%$356vVCq{f~)mGsELF^W?4!Z+Dw4)9f~7+uA*m`>msJ zW(-KZ>k{bAcqqIlF3Y#$omR`(1$YZ;cMf5fa{&87rRV?wjqX_i!Ym)kD027U+&ots z5&hb!Q^$@S`ysF8 A9wMj9nn~nXl#X& z4!BPPESELavo>8MrcSfC4WcoT(bH3>?zi4C+_i7*cZ!O={PR?im7nshi)!X%Z3vz> zGx^@0%~#2sYSDxScJyvvH1cq}yEP~(rw3> (u%5br{PX*=r~dC}P! zjiYL!JIC9?q-Xkt9g@KK1(30dso&XcbLSOl|N1FVgn*Mma&jL+YHS7!Q5`FgfbrBU zC^G;hEZYZ<{@MV+Ss*)!Avq^|gC5%~S?_yp3+T9WsXGJ!e!HP55zwRMXpT8)=FPT_ zh5Q&l$h~MQ+zNRu=EgtnIfP+!Wt#f~FxMM|cob1%zy)s%8$mEbLG2vZ5TzZ#7WzwU z`G@jz#@qJn=~7GPNx;MuuH_zq3wVWb7f)T+Gb#`)qzr`?TA-x&mC38pBbrmYpuEHs z*9v!m=7JGuXE`s;JUBFbd`grzewy-ZDM{R4F}%5WV^^u1rkbg#NOGsOn1p#4oj|V& zlx|7~UHp^;t$&e>#MG(!Y5P3w)QwGe# Rl?%W&1ZzV>hE?11Bx{Jit>2d3=Aw$3l=Oe zC;3xE>=5>OQzdmqn|~?Svd;zLRn^tcI?B;miZeL5`iLzz|GBtr&G-4`XI6>*)~~N2 zH`y%RJP h>82Y$Xfq zBaCFx;wDAv!5Kg2&xiknHwlBvfbQ=Fm1kyXtDYR*YK{VV19Q^Xu{+cqZ?+FM;p=Wb z;R~VY4z!T!k97$trS`IxZhf?X;Rsaft7y)6d#=keS7x&J%-FF?(K@Q|F63G}a!Yf| z!bc9OhXHsaC0u11M}2W$s|hb3{d7*2GgR2ujE7o%fEL|8F=x*t`@{e~fBe9X$8j~k z5)jSYt&lHLbMSZGk>7nhj0o8Xo|QC`FrFBqdZneZyt9|S$K+nPK?i9Yfm7Hy5}ro; z7i%)hsKNDdq}NB9YRQI k?U!P4BE>wMr4$Wm)HN>SJCc>t#~Lk&@ u`e(Zo5rPvIK#lF6)=p)QE(*EL2J4K36{-;0?)#$%DMdwdN^|54r1tH_<^v@2|_a zQ4yveHsqp@j&`ZM34pr>tbLU0SOiu%WaQvrD1dN6ymac7n^In3~>4*@lQ-=C2*~ zZGM~?EzDromxr)MhUoAiP~ndn8M5sa1sV*{p|cAGs#YyXAZx6i5(+xWQi7p*pFnSt z2mJv4+~%uu>@ft4bR4a;Lapb68md}PyFX;;rgX#-qx9T~gJ8g$UBN*}zcGxm&zw}o z>ZJo;7;Bd&U=(7t)4?b>poketSL&9Z9KeCHYEVV0zmbubGrkjO HK zC1yeAGt+42KIxK0i;@LP-FNrxZ8$TSlk6jeoDcQrtvgKad1l}gUz9|~eWbaj##BW1 zckG}Id==Fx!y|Ji>Wu}DcXJyK23@nyFQV@)@L^d@8ci$4^p9z*ufe*8yE4!nLXqSL z0rJbrHl~b+%dX59=Fh&(9EPQ3V*L-KGiMa3#w_DiXhNl_#~5JV9~8{#q(^`Oxy?{Z ztr+qlK(0=P<=GJ-=oz>Ux4qLR1i)SWLxLnm! B+TgNC zAmd#A= V|78(4^J8;(WZt^8e0Na67&!2BT^edL0Dc5W?eQ0 z112}SCG(Fm_{Wm@)(q$D&b}9&uas87Cn>z?O((bXPMha*ynjc#AeE%7dDMNRlWX$Z zOn-D@9pP)0yUcHu-|87}ot2_;rKNOJufPp?f+6 !8R4tbBkK2cCD6c-i!AVQFo7_A#8p=9eWE8NWG3hT z>G&T2VE>2v4yKeRhE&)^MIB3CR2H%vm#tekvNd?%@uD?;_Z3vkw?3F?NDH?Jd5Ai! zfLu7|$YF(bs(PE^6c=v)akBz{$Ipl7oj#g!dPzd!9NSeZzfw7(Qn%x`vd4q%EXw%P zdHolwN+i;JZ;Vd>mMSu)JM$|jQAE;Tj1c%ASZnkDzfn `g*4P`foBg`dPiyLweR=&;sVFP%Ier4q{sE{Pa>Jh(m_VoS!cz9e1pwvImueZ62_c{Rkjp~rUM-7a| z`DVdM+XDJ$$G%VEiTBFpoeWj*3P5}RpkIIw4HPGX%1F7Zli2ADbI#GzAw#!LCj_w` zkF-Db$mcgna_iBbaZtPIpv-b=8f^pS@C62iKd8{1l%{#_F5SbbNhhBl;-^NYt$L zP!X+stzKwVk;mx6ZdM8tGLxX`&MG}!!T`$(MIk6WQL9sFhlco{^A1%pJW**t4=E1_ z2`%7`8Wq)h;J_5kV$gYX6`H*6Nmfl9dVqme$#}B%YG&GXBiEFLt?%BPrkFT_!Tl>X zO+PdbnV3en(~jSgULyZs=#GKn^L*^|g`ob&8|wUQz&N`6J>uitPj3@tBfWxZRyIOY znZL~wJUs@cDq%+x2_~cqx2+sUr r*|IzFs0t#VJCgHH`c zW?OEJ7Vi3!J(>9iJo6Z$#)m+K8DjAC8Hg;?`vCjf$D#q?h*n)G1fggda_sjtV7QxX zP=I9xU)8JF3GJny?)HnyM{@hq*9!Srp2A`FzNpOl2(v5vddD*up|SJH!Q>m~z `9szAl)RRejvEAW&KndBJO6y0ORaZpHE$)ZX;$?!YHvXJ|?tV);4!{Nw`1)lQW0 zC+5N2GPQ)Lvzn~0Ecrh{W4*v;iwuKr7!_dpa;8guXJ_X+U8T}#qIdAwq=c0<+?xzx zPZ__dN3ZpPtV20@+#vBfGZ+~U27QUcJ37NKqC@$HW$OnycS2Uk&mH8@Q`{p}@<6bQ z10(C$jy5Kw3~U}|-t>s_G#ag1HU-VzZ~U2m(zLVF^y6_xi-R~c6s4A*>>}ojO`ru9 zTIt?k!cc2h7nrrxyGHr6x1^prRmyJA$flW)G|N5Rq4Amn(qfH$`F355`qhrZ%E2 zTB}6gLigUqH=}DNGz-1=hsSrvKdb)38~^^|L*SnOgpT9)8q^;0o31-e%I*CRj}-7h z@SqlY;_iW@70}A0R^P7~lN}r*W*YM7z+QVFXd}&oo(ablO@rIOE)0+ *dNV)dOI*~(Nu$M6nXi0)11NM=IXV?p jg9(Bq=C=D}~{IIJvfiYbl=m^~V){`5C^SK Q1?fnZG%Vo~fP}LZ19W+q*)d)F*A-4AM5V0zc!O;H*%##M57K+qVzT2_Fg#xDH zI}uG%JkCGv7gOxbr&EG<*O2Sc%Q>i(6KZmDyT&DM%qq|jtwRt?$C!-bTOs>Y$|-b5 z3$8!Q_2kW`Q&-!|_ nG~v5vKTY1v<^cDlTE} t1kL+)!fwXS&&~qvTb|t4pHgjCfI_;FVA;7sB z@!14y1#;|JtQJ^8quvK{4~=-7xD zkK~986fbTC_O4;*3t>PhvyF qeYWgIyg8H z(Mz)yPw0ma?}vsaByh#iN>~JE1Q=d9xvgX4v*TXyBr=Fh{{R*K5P5?FfpxkH^E8JO zT=rtzDPWz_y0G!*U`x{F?I*7|o5m>~p>0U1&v5`-B6XocG_aRqVmXLfBrSd9@JRR0 z jZ1VvKh4uA6{^MBF? mMQZhTQ=|0wgodj1^{1LCMxvn56K7(xDh) z42-BUdKDBv{@^Q7(W1OKXs>}(O_WIPon8W&7G%_fFYq5@fjF@nY7^C<=-p5<-VF90 z4Y NVtr{=~YwI1&)x{HoJ~Tz6XfP<~{!+)6<0#qv`GwG6EKJx7`t+20rgMs0 z RR1tjwzjs4>VfP54rKyvIThvh!cgM84mh2x)UFgzpt}Y>X$c;06TQbb1 zw|!ggXN9fXm7-Nnz&93s#iU^7UP9y#FE=8Ty06J@o)Guo3K>R9wb5P|2Ff6{Fd@Em zt0kmpWgo;7Pe~2T>(IeFAFv|^k>*37!oLSIw*H$GT%M{pxy#myrRfE&&J$G60hkVy zD+Xevr?^u#<^G#^avP{Ur>dZjzhYf>Z*8J25rLv@MJ4pfR$<<185;5j!?9vLH375( zW(y}aKy!$%mWOOeaB JPa5bx>MN$ydN#dW7vt!T(XM+$9w(nK$RK9} z!B4Wla?HC gDWpYe7DkR2GHWV8;Mty0r-ehnD;m6bF)HAdG(El@H53kDwy zKfJu%mIzXTc}VmiiMf-cNgi)vTW&c4^$9{?TYZ?QF9pouiR#qk>~qEXq%;~9BY#Zc zB{!tsp@V%JgSQ`Ts+>Oi36FFUB^Do6G?=>}K%D58HgJragsWm0eH|OE_Q?~4TAQu0 zJ0?<(1V@!twqp+{%qtobOC^I;(!fB-i$U8LyF(|^zy=ymassPtG=S0;@JmT)X{K}i z5Y`()ZDP)y$9(#xcK0tObA|Ps{eOAl^_%dpFo+U=p9*J2+_YZPbQvp9?`*Ja@b>a^ zSDcUC{ Kx $+l{9=$d}d zD5)+u^V)*%x#jBL+|?}1cOBxp7C07>n yEx_te(vTs~!IVXr_&{Q3 zR&%A^xxKx;5f8k=9ESvp2dfQj)XK-Pm+Qs-1rNwc18tW&vWa)KtN3UvFbZj!d{&|} zcT^yWFiU}e#Ov`PZV3iB3m+AJPKan8qSi9InQfU@kIHhTp%YOPTy(2M42I24Qsgf* z4F+}eMXF3nr9Y&QDmoloGobIP{N79xlTFIsY}pmIUZs6*Gv6X1Y>rmtKv9lEu%QSf zw;rd7WP3~1v)XwMDa^%Br~OuT5iLE1)~O=h8W9&gMyt#W3KRQH=G@BXJnr`MbkaPw zw7tspi^u~vZWem_vd?@ SemwfvwWA0 zMt9j HTVWpX zgBW>WkZdEgyP25);wv5_%X437gsUqwy4I6r<)b2g00z^lh5?}kc3`NmYSdgdDO}mO zD^1j+JPr1Yfmx-1HNlRQWsFGKwz1lhit#23Xy@+e3Eis8$i$9zupJZXY ^j 3<)B2Zi;Za|}$r1%oO{&{pZY-J9}2m8k^8FHbz; zwNX$~2jn?K3P)9dbx7LsCIT~}vAjwBYk_|U NkzmPO&&(88q%v|n_$+8_k&ESjKzgj979{; z-IMaVz@_Akrd==}y5Ef6d5LH4$gBwcH3Lzx{BEd%kU}r5cq|-b^p)4%47JvWaP!Gs z90knu{9Jxhkgm-fuvOtvA0nM%% ePN%Z=I}WnPy6yQJOR(q{O}?f5P#VE z>!<6WWUDEmpyeJ|{0-DE*b4pxnk^1R+@O9IEC%mb!Yx@o@3Up$X1`e#$@}+X!QkI^ znRhc5Wv5|?8)8w7>qyQUzP25bC9D;<9o+zVO*pA2rv+NO$8G?v>&qCE)pLy=rj>&p zgO|JlseBNJpjA78tAa`C2uQclgwmF0I8YF@R=hhB*U={c_}RE~%7BgQ&@l{E-kg5K zHSU08lOIFq*CFh)Z~|^(jCAiwmUBZXR2bm Mv3((xT- kJBM#h9 zYQ6Y)?^M#zv7R8^!o+Kw4$T@epQIzm&~DXj8oRHb_LZ)!BS-jH-(^`v(P zIb=O?n?8SYud*v8HFDs|&2_>% zUukVc`B#lR0Q Zf>~jAZDf|mqrM4H1V*!AalXs7a@B%LwDu~e>T7ZBST#5J^~|uY%dOER zOL6trqz@R3$gI$e=+RnsLl~Is@Osa%uMSvs&qAK78N`+2u_qaQQT1PN0Lb<7p=T`J z_*%wazxT)Ie*kcQ_@1^)_!6L7vi?EToAFLA(UA8Gn2HUCa)J#fTnl%w%n(f2u_YFx z=sL1pQJ^#=WS-bf10)!Pk&B*uTPvk35I+TctriM;AXp~lGCq!5xea8_@2MF*N)wHy zP!$;!{dxJ95VfSWj6UtEe9Vm`-*3Xg!a#Ch#8|16nvl|^1x2uyMbT;0+xu@YV~4|3 zYRpnNw 5xKeblf881*=Y7APlup46p _8?Xj0#gO|DR uMfjW0z)->xpiZ+T5#=W?!d IO-qHTi2A-QdD@UyTdSZNYF- >o|uNN)VE1FxNk6dyaP)5)=O?^)Jhz@mlPWFs>=u- z8>G%#lrYM7DO&F8yqjkMHF>{B1H1i{R2#jp?8&6`oOPtqz-VRsTH$>y>`f#ZOw`eZ zY8tEpB6ou<6yG0O7zm~T+G|AU8n!xMrm-(4;eu~^vZ8At%)b9bVZnR-oVOOfdJ-%O zhl+Lr7hUk=Lx$c`|Lfw`>&RjfX(?m!&N9lwG|?2tSec;@rSV^ua4A5Qa8{ixB+6iD zh!NWz2)1w^JqnCD(qjvLE6NTkUnY;%X*A;7{VO*@C0{H2%U0zH;=-yj0igS4>^P8B z!G&n$^KG4-ezdFn*f7z#T8WUwN;9L}oqTR;+?J-UB5qkF(g7<;0Z+Zev@N%MO_}?K zkGzX;C!j3gJ0CIgv*BW~|B+DVeGvEk@WR)3cCY*0?i)}p0W93*Zmm<1amT)W`<5Lr z({v$1Q@U)4VN`KvBYur(oC#?$=tZgnfkS-#++$!DHe_ E$ZFZ2l#9cxiR04Xpdg~J{ur=G^;MV1#4l}}xvf(CSJQzl+ zhJOqI+02ednWm`-W3V0giu38-)R>SJYOLz3toF5hT9}CVMp4~po$}c9PKJU``~y7+ z|0}@w_p!|1&x)KCz8sq@)rt~+?cKhI{Y7*`D66J_=#wjbpMG)J2mj#Wmx~DVcHY}> zF`!`e%f)5xbrT;)=q+4jl%sw&e3k9Ob32!K41aHBaB;!m_TNsQ{_V-)Rc+xu%iGm9 zmt5XKZ{>B#bFbx}Igs^WIt?Lg6Hj6JILb}-a-Tt@@DghZXfSlW=a$pKAglCheGyPp zXs^o72`GX$2F@y2S_?*q+7hApUUe!hyini*^z%{tK4@`Od=2V=%0L1HV1W{2u+!9k zeO40Kd&O>$<+P ORRJRYIsE!$$xPh^qIC;ZeUUD |;ZlA#|Dyl3p4{eQ(JY}SAN+O}~n2qIVZ$(F8xro24u z<)rDPwoq3sz#XC&H6Z^Ajr&~ASRP+vt9iZ(sNYR_$wS*;WGC=r?I+W9psX&G@ {{OUmH}cAg>>=L zW=j0Mxkkf3u^ZIDGOnDKn;tb+T=Hu@7xy(qHH#A{1tv|gk2t6|t!@8@y*G_(^4i*m zv9)UJK YAl*%ZxWe^OIAjlX C^4o_bEtX}>&ge|_G^`-8a~0!?=IT6?W)UF%vp1{Fnt|2(dL z`)j}5J^IS? -Tpg%A6GS3zHCc%V3Eg*M+>;xzz-AkN8iwAQ| zh}8!B04qfqxS?}Mar6)N4~YIriH!s&{5zOFf3E=a6E#x8UtL$o8_*w`zcta`Kg0oJ zMx{XdGhu$naCVA*jd*}wSGdT{zP&Q2R+udIT=RyYbMF1+*(wx91<6k>D;6dvDOhQF z5_uFnV5QX#U!7=wB8AvXF`c!r3U^M7c+g{y=Dc1E^T&keLf-%SBWTLt^Y`ZT{>guR zG|u|ymjL(iFYwO!(eL|g0xsWd9i3 oVej+!4^a z)iv3X4tyEK6sTG#rbW^Yq%YP=u8UR_H{!m#vckOR#;?*yEABxP&y8hodJlryXGc0D z0+z_f_ORu+QAON1R(*j;rLl(IxbXhh0v^^K|6q&0fBJ*RLw_bX^~Ik_7(f1#2{FZg zrXun2Er`F5Lq_Z0!`j0GU{Y;FNtF{mf57?h#m-3KLau*j!^md;i( p;Umv$tUG_1Yd-SPw8IF-RplXKb3D)!NJdi6mV+v*ff{D7t zB!{p>TFMG~hOpXk@pd=Nb(zhgoWx^L-)26bAb9`_^j`tkV>odi5)qup)oBccz+5Yc zN7Du}b8wOpg6~A F(>g%+KxcN`(@iQ>qj;7 BpWt?w%l|uV7!=m@nL_$ zpczk_*U-9hFVpXa_h3lBZ%6t=XkHgF!A+J^w?3)y`Q*GA*AHQ6gO=GSu_b!OQM;?^ zi-?*;Xo*YPrNFu`tFNa%e`vTcCK959Ciu`Le{suSCD|$B;+~w$A6m=R$S9L=%WTKW z*>FS{z3N^MN0PS6j~5h^hfayo043h9FSscTvjphie2ib`%Dfdg&J&RXXG~3^OwAHb za#+`9WH-SVp+7-H_|auO&_0g~;QTX3`R&7faQ7E{o&P72jz0}Uf7{fL>f9ThjO#`M zF2~rtNkelgf$YUwt{V_B6IZf9$I?W52;rt!^%Qti>K!5u_)JFxDp5e}^#P(H^R>Df zVm*(ZsfW4?afp>X-Js^p+N)i1LO#pC(_m7!>$ $m8HB*$EO9hnDs9 08m-9?!it)SGnQRzN^LYvf^RX3JsmS>*^4}m53qsWk8mVv~~)l%bx zq90Ul$>|k-?y$E@pVSz-kcZ*=gQ>$)8savH3Ro3a8ZQO&kJ9$RYf5{f#|O&=7EYZ~ zjW0})n~h)zOBm5O9zgfP4pF=}To5CAmIcGvS(j1r1k3A^y!fcukZknxO-4+_Zm%-m za?Sdg+Z!4wrqjhJUR4Mh@%BMdhGI`3s=d$?fQR)7-eqvQlvf#ZpOV%fQgm||FQmj} zfUzMBGujWvZrd3e-eQQTrg7IY_a{24Em;5$-?^hx_=m;HyB|Xb;2{1Et8gLs*?U;B zQw4;sHTDSxZ%!rinf{pmtgt6aPN2sw;mKtKKoYrt!9bq^CG80yx@}$dOJLnh88tx1 zr*qS~=whlzrB^5DCMc$g1z9KB;h}?+tmWqkQ7^J|pC+>Y$$kT{RK8o4y{2b3e=z|R z-pB0)paC{6P^?WAGy%eCK-K7$xl_=3fVrcWJtQMB?ASfD-` s^1LPipCT~`wy`ZEWT0D zL{hF0bj4r78fu`8&K>I8lg}|;|4VM!uu|C%X)C=DbG7w+EM?TwtgNRrpE`oAVnoap zQ~TKHpQ%%{KDOcIb4VtSxB*$#kI@Uqo;zLAg(STGGA1H`cu|z?u1>aWA#}0;JoU)$ z6z0MQ&j0tfSN3;&jGOFz6>4{*EvIXyl=NS%!wVsQ~q63~+?N WzlevahAQkjotnMx%+UzkTb&?PfePQ!;M@uQbIpTpHwTpyTI__y}61 zV5#2T%LVuu#gresl^ju;_L;<%l2T8tE_PufO9vf~qE+P yM5Tgau8Y@FK2tndnU}iIUYD(E?brdvA>&A`I TOhFt@fn<|kojijoY|XPGhjxTF`iXHmfTF<=2V+MCzk#h+n&p}MPlcQF8vMhm z>`&IYf$}OxmjAL<+~U)?6V3525NMGFue{12+;1cT&dW_&_12kOsPu)>q=Tn)UHFXM zw<_$LbL_L$!}cKMORF~F0O61$oH%3Z#agTCzOBG$wA6T!jVImHUC5P*JdCa4#>0Z- zb)yQCw?1@kZ4IcJo(#$YK~rHOSIGy>0(s4PieBUI!XEGJC=mTb8&^?rtT^IHT22#V zw`bbLk;*h4KlhjLB*SPwo!1L>#{_U!VDqsFN~_&yznTBP-F-wv#0&BltilIj%@#b4 zk)ferl@src9@&Vv*lDmb*9; I=EdhNI!U&nFmN6E@q&FpHf4xTDt|CrFiWX;nBgP2vJ8WoI)+mXJ| z$)|xz$%f8JX6|hOEt>b?v$cBdjCS4PEqCtR#OM1x4I$WjOB40H@1t-Oy&HHFm8y`R zsn(GeAg{3R$R7xJAtdqHSb=u@yduBxz=NS5NTknbEOO;ffoLkQ-Oe4jqyM&_|Mfrr z4)N^A!~%RLck}xFYWky{-qpGSbXIw?tCYg}*Lfg$vva(35_DF9GTkoW19AOsl?f*9 z-Vrmw0P5pcH=a7t4%*SCi>XMbg$%IWBecU3Vw*=`=S|@CfYz-e*4{+-`e-$R_(9x7 z@cx=iTqI6ISOUuxAenzFur)!}zZ0k$ rNZ)>r)yp|+|1VQe}2W~MLy$lhOyyqI`oW5 34isf)OkhL47+7 z0Nntf(}2_eR>etNE_uuQ1|=FXe513Q%{{va@SMP&sl`qBf>{9zgGna5x}gN*jKV1( zvVqf1*8L(5NH&RE8S2#%U2m7Fr4VsMNGm#O^0&xDf{@?E$DTEz8+W&zF+KP;FMV+z z6mA+D8$sCK``~q@ZMc7UcjYrXjLAU($6w-6(?t;>x2KIGa&P^1%5K1?ud4>65jy6) zTa|)`DkicXXG%zhIzL+N??>2$teEOvdUYbA%$FY(-iD&BW*iQE$eS^O##S=V^kAV9 zGt*ViB=_u>j{ToSgjP8{j0K&ZuOrrh%(+mIg{thp872h{j;$D`I4(%m&t&N>?fO;Y z21u-rciC+Ik6ZWslm9ok34aO`{tdv=U@)>Q;#u6=FXl=QYWl_fiT0tPA>dpGJu0sU z!fEV86I$phhZXYXHW0SV;SWr%b~OM|y BnT*0J|t!U_bb}aU45cq;R zE9TARa|}>+w-1^~l^oa#%9_ 4JBKKZKIg};1>H*@95g |)Q|th?BF<8!~kJ280LEfE7s#aSw}oT zV@1307eI=3rTJbi{drEsC~c>{5PJRm925ZeSBfe}?JmE6bN}%%;PA(yIarv#pl<)V zx}V9pEA=j}6m`AzirFJ6EAFc@ysJ$#^QWRdiCk_&42i=^S $3>3|BnNT6dvEoA`D=0SG&)@IdG}uD%$b5DdW*E*~W4-{#MJI?Hl2(A#ckntLJJ z0J5)ft*PaR9fR}PEhEF(xVd~(ho(R1R0HbO)@7d1Nl@rlb--7A3Y`h^2P<1tL~5nF zhpdR!#y-Z8S^=K?^B8U4f82}jpZw@3#Vhf5ScQMxvqi{vK=L_cqm#C$n1#l8`M|X! zJt1l_A4owp41Xr3(1w9d#DOd@anTwKRNp DjgAPDdYq+&@gDVqJ#B)q&v)WI9n9OG$fTKv 8@>i(8%Wqlsq zboaNKcM$0KFQ($7*q}AsHNlXY71k9o1mIqAy}V|%j$+M8dVSfzPdt!mqFoZSjERdH zEAt5elyVH8Dzq-!sMb)fyHHPSRK>DEkq?N&0_xhys#T)7xYUw}2F--o9{{ES$;w~H zO_=vp{xskU5w~buFX$Qx4g?r*UDwIFw8j~CU^al331BXvupr^Cl^1=J#G{^mwoEG= zfk>!VuI}{_V`t|}8_!KWuDJ|BXn~F|4&z>9BWH3mh+cea&_HBCTgsjGU7BNGRAWjX zoeqRSXfKjrP#RFxtQcIkrzW`@H8Kg16P> oT}zmJyZWjRc5S W`KUi3f&|zCmo1yW{G11fDik08gO2^ zg3Wb4-l(^$nFP9e0d6c7SVoWTp 9UJr_ZpnDC^I?4rK6Iu~<~n#oxhU33i9 z^JTeO@n&Q%4+9SS*{X?-bOxRT!B!2*)8u-qCVjNLIuEp>4J`44{oJ!}oPb!FoBGBx zQwvIoU1S{1RFqnb3Kv)G_Wqu|@nIQb;}&y_NKA5lxeMp`!RPq>=Kf!U=Pp5K2GOOy zUU1nt&oV(^XIxbh{XXzg=Do9W7OMZS;^T&%vjy^lFrdjraNageE&yM<96m*}< z;BDPBo^a_R@)s?yO>%c%*t!*;{KuHhiGuy-!oFIYcdheqP$M^|lIv80am>G?yt#|( zm{U1R1O-r1BrS?s$qjR&>irS<>u}Sb9Atls7r2B`-}lGS9)}Rha4^+MW(AIcMGgr$ zHO8=-!4L;j2k3Fz<32wbLV%+>fYAT*=?t-QO+5gMfk#Jm9NuJ2)wxB{lr&sCoy<;5 zYbdCnkct|S$}0suL==i?A3k{|^aE$_oV$odz>^vzPn&X_o`UJC$texnegdLT6iqeE za$5 u%hMiO5X%+{c`wr&j<%o`YS$5G z(JcPW=?s7pN>em}(TM~iV!g)({YV<6>-!K`=nY;=pbet3m(n$8cCe5&U1`f`R#YN5 z^oA1KPeoaK>yE8{U6XlRwVEm 5B&Ye`$l)&_1yD9H%Gvv+y-jaGh&@CH+ zoE7|oI#oF_w4SR8?k^xJcXQ%g2yuC-ZO#m(6-T(8s3~e%Axiyec$*rrs@`LOlCZzE zTr%X?o{%`7z3Gv-ZckWTlr?+-7ur}-Kk;gRf-MaM8-At!aVOJ!B^3mXl9(3|(A8u| zTZx_&G%GT)MGMShl }qq7Lwfi5?w|h+p;4 z|Gb|;g4=7QJ)RwnlC$FblZ;aIiwen>AZ~$bc5GLqFlPvF8(AYUtVfw^Ob#~a@gATr zW+$AknB1Pwv%A{3q)U? c1o{)K%KSRT7NEU!L|fdS$c#g e0an9X$ez^ mtM@;!4nK^;5aK`f@Nhg^6CTRMkE&_A*?^P6f~JyJN@OtN zu7=1~-cx+ux|`9DPd?Lf$pM~ply2h^eW2y;R|MxUulPOtfAyA o?QHlK##hI}g`8X$VbI9 3M3TDG{J@)Uh@G2h9FG9oa20{I9%T$nP{${TgOZR}dWB737{^wI!1$d{NZ=uuhWY zK_u>D#8+_UHOPsYv8Cu0^9o?Sx0GSp_EbpJy&kvE&QdRxO74mENw~|3MyK*r;!6^z zF6CbHjpYdj+%enTZ C{zHi9+T~T|B#fkaR&NhIa`mOS Wx+BmU5u~YHysNpqz;B1qXUw6`1OjiD(@DYH#kS8R1`fwq*a8@#1S!MISF)z6 z32C9{Xe&?gM7gJj2)x82J7u|3&IYwa$lixrT94V*?&m#ESaCvAXC2Yc0*J(W$${m? zx&4OY)f=s;*EZ76!53y&E4LIG`Z?^G>5QPs%Fk))ihE*tEEB!W>#qpXaE1KQ@ZHA` zM}hv$CwWnOxBIxP+~*M=Y}PuE+Q!p$J6O0#4wlcFG_tyUFG5V4pykJOnJ}x_C)?aK z@{(s@wZZr0(ko@$l7v2GcL$q2thdFW4}0Ah(|rky!T`TsA}MD0m>|kZ_7jsFf>KkE zV_|Z)TtcxN-vPe#?BxI#*8Kq9MntZBF5x(-)ubd}(yYu73Fth}9N}o!N}3{rBuLP< zp~W}xE*+ilSJ}nwLTBw^rLEOg_Kh1uFCRXXzKt_wpnmZufg@bv3Y2z@CF!rl{`47J zFGAI|W+S3>!vWTXcvr%jiy@?nuR8qfMBlZ?IW6WXGF1HUV~Rw>!*}aF2o^GL4!YMZ zvL1oFZ^^!D09GCc?&hl0qd%yCzuib5{(I%~EfRki`+j`E+vWP7ScgHKTca?9cn?N4 z-gMvK9b#=OYvUf{i?*k$Rg~Y=g>cuinZk6sC=z}8;P~sgNL0so(x_qyV><0DFKU SPXOU4OPto}b zjF#YrFE^yth#r;`4@k#x(d9+k+Rf?{Z|-k(hOG!#f@npg_PWvOROv53q*r=v+u+H= zgMB^>g_0Za=ta**IMxsEHtrAEams>J+ibs=W#A6LltCJM%OP0tEXT9-p8EGjUOf z2h6iI5;H}M{_srF?dG2ZWoE+9e#sjle9Ky4k)%!zuGBh|Ahr69r=#nA(H7p0u_(4{ z(Kk_diwBo9ko-E9_NNk_D!XOX`V8Y69X;Zj!~!uTNO3LR2xb*#DzKj`G*+{rm8Ow* zyBs9zjqJp!Y}qV}-%S{ko97)t*;R1AZyC9O>iD)7c$nm$g<*{f1A#%?+1j)B>A$|b zHPuicj8aKuqid8#?roEBG*0Bk_B^=!Mkaz6b>d7~6wbD~-)FNh1s~~->W~x;OB%tt zF1Kjp)!R>z7A=Je?~L}a?9i=`<_QY8e+xJDul5UUY7awfQoX2SHnQ$F2`5n&QXU}g z#4rCED=My^*e2qiWgja{RuEKeq-ai6oT4lE-dT)0(=r@&^EjHMD2GhI>Ek4)D_FL> zqC;5gU`6EHt6AGdSW#C-gzNwfWN>ll4)2$0^;>UwKE=UoX|#lFoFn9Vh0Mas1&az> zMyZPFVS0VwUh~7I2b)jRb31eK{v oOcVl)^U#rlCAaDwo>gup~tE8 zSU8$Gae0?J`psEUQT@a?era|(BMJ@=wCUEYm|ZL3c_8c4laH6p#l{?iX?EBmWocP) zWF$O$06*+;vt(aUavTDSU+xvn#aUC~+!f~CLrfOGW;I-%AhuNDVL>dZ3d>wgE~Iw# zo}Zz~jrxYM2%e8#z5mMNrkCyM8b>OEKA&P-f;1&_JfgU+Y`zQ_* JCs%r0$NdmpbfUgy z$I&N@sFk$*ViKyW=7kihsnP=;j$yREIOAgp&;7IY z9vSgZqK#%MAhnLvgeR@OAxW~lK!GM?BsDX?)Dz0(OlPPL@=y6)n&YL~os_7}Ilx+z zmMBomWooU^Pq*=^sEe5Ga--7+lzihT*r @|AuG( b@ z5?MR7uct)s@m*Zi;{AVoc%=XC{w4C`882RxzlRfPt0aow%*CIT(CCixUmriNI6 &@g9FYK?ZNq9cz4e{)y6C&l^_!*2=?a@75(nkZ80U<3jMqcn?N-D^jTAC zsC%}m@hZTjwu*M40}uf0QsZcrxR72qSD#Y7Bo+TXa8O^xgJhFgkte^YAH8!L`SnNx z7;EI;9FY}2(t3AWhzkKvV{7(Xq^3M{ksTV19u7i}|E$ai#fUxy*>eDzymzaF-=-;9 zva>{e&2;~Tnl|~+IOi`(KDCbwXQtN^rUn*X7ZW}9R$5#o@!R#ME4%Sj8Jya$dikYw z_)E-xhqj*l4*e>C{hXx4)+9#;Y{=bZ#>tA}Ys }NDDV9nvCXsirHyvQLY&~Hp%sr zrgm+!N4#8k>r4M5GS<~3yAE0JWy_n1tcc0-5SR!UL*qxZDYTj0kfF2?ZcP7I9R<+3 zIA>Gzcwj1f*k5!E)u9?WRbrO<#p7GI8zzq)jpS~S=zRQBL$IUiR5Y|yg$~=96sv1# zZHEL+FeoTD2x +7I5Fw0M8~?)odzzmKW- z>ash(PlNkjGn9C$?R_?O@y%b#p{C^Ir!&hz8=%A9Tt}Sbp~A(uyNdTe-+UbH08rub zV$!b<9oE*=ZZbKqz3yc1 8uzQSV-I~)%3D5`Ajvz7iB>zCfR|)N5y?vGu1R0i+Od;8}-^JX{zq% zat5P6AVo9c#n-M{Git|A*KC2(E O ;Itvh;b-_l3!-FNdFcyilQGyVuL~R2BWwPMeh& zE;$jg@p=IC+Z(32w9jKUW?FFK>C2hZNXx577&|4+u5!Qc{(R)y@aX+Fb`yLO4sSFg zI M*K3;svfe3 D75_)liLOoibWj0ms_C0}EkbK5+y6EEg)A3P7 zrS#PEh@Vi@e+K3s2I&ue7sq?qY6TrClzgn!&$9{{KR_U!EO!YE*lAvDc95K`hkjV$ zU-#9Ei+Ht}t3S`kq#c3zkn>*YNz_^%h>)G)?}>AMy AQf+B zdhqz%tND?-d_|@6A*yh7m||fmjjcVSb9sokkxo~XD>kD-%nqtZsXG{oQmTI$Z(|2t zYqsH3oRS(CC2p+rAlhaRsY%p2Z5oMN8|Pe_=>Zs_Z}N`62Kt!@RL7^124A`l0!YOg zenG0CG^?7gciAh7P=@(_3E&|S@-P+pdtG3mch09DFra^Lx_^)G^Z!UH@IK0Rc}bxy z#vw7%7%y8=?%72#PS#z$^J+$i$2NBrS_v=jK|)20i-WADTQ2g!hCbp(dx&pb@uA9u zRiXen<2Wex$ipf+^#HX}RhE_j;_30Z@+gC#ansbQ^DnG+Rv)L{FgA{*XcAn&39x8Y zQGH%*y0x|6_pphQcrP+#%*`&e_VXelFDKtju(F&+^2$}VY`L0unDI1)ml5D?2k Y;S zNkdRM1yMmG1Is+a2T1{H>M-O~eem&bi$Bjdtw8j7MtDUW^yQ|)Qgb9~aW4`qc+NiE zfm!-7F0OBg7J8P*qxW`=es6*vJl%Z+iMAbzJQfk$J84=ZU&TQY4}D0a=$kBMy9hpI zwvI#`WS|5Q2m6N&uL2>ZA?!=P$}S3uF`WUQ+qY8TrZy?LnfA4VYE|c8(=ox2vEBrI zb{cWDclK6g`CaG}7JBVjg@l1m%30m)(Qhu_yX$T{y%DkW4^&4cePv=g19a))>*Z6b z>4CkoBBY$??4f1)n)|7~?*e7Vh@mS$ljgr)A5HG<+WbA%;k`}%j5xY!4?hORmV<|f z5$<}FBEolG?K)?AaP%~y(y1)3lqs(`O$Bh(LzkLg{Q|oe2X_t~mbgNCeBt)yVeL%b z1CxxD*Y&E{J>ACw3&+(5^-6P 0VR8`wZ`PHQb{ugmW&Ouoyw1*gt6*(nYE zXxVp|S#NhsjVdLF7@aAR-PNpXe H)uM;Ftp4#46mX4C3U5^v*)jd@97_`uJ@?=m5NPbxzRmIEV7}KJmb|!ZtjeN z>kt|=oD3WFtk2xo+REPc_@u1gg|$Ilm#>$Mk0LhHS`x!B59J?h%B7g)2+V2fS3RMb zkf65QbO6Ew )IDWKiJEh-eIyWCy)~!`a zLNuSJ$DnhW9S7<|EUdqdo6#*8W5^0F{4zQ)*HnFC@e||AX!zEzE!FLbXT^oN#Lz?C z%TzyY7CXUa$tk#+c$ywF-*e~rlFnviaj#|!!?G|cM?W4WJ0r!8qX6J7^|k?y#&)rt zv$UbAbT4CdcL%ukGZQh%NjK#jX3_2FE}T1|ragRVBsRY^jks6{IAR8K-=QqpATI;e z)KfYE`(^bae!W1~{6ihoD;mQR&hK9RpC9gge8K9%{12={xdCHU8H)4r<$6u{9g6A= zeCp8)0hNHOq;dQ_El@e#$u;|d8T4%1gUdgqbk>+l34yqpn5toGZA+`l+{X$bOlMpw zW$iSvI0)Nw52n)Jn39?+R&qwQxeM~urwnKL&c3BTr_$1@UA|q;20ajnSP*Qk5R2$# z_XKEb%uTPF^Qx1A!TQMMT2Nb3 gwne<4kBwE(-A=CAj10eY1YGsXZ#(^V@>x#!7Mo1Ucty-)!?~QH^S^ zzG-E5MAixfUKe=qsh&Y;*X|e|>9*io^t$`R&7r+o;U?7{-K5(l+CB^0X+$&OI(Pc$ zm|^zPS3oh4W8CImq==l2w$&)A?7=eo%hV*=ZQ9UJQW>wVk7Z+nF^9e4QdzGH5)G-W z ?ur$G4{`fbZZBu8$bd+r{}S*5N${ R8cG3EGhAbc2>Q;@z5^JJy)u8 s*TVd;GkWlZaqnAIkl&J zDogKmEIGJboPj;{uEOGU#oX8WyZ#4gX&x$H$0;fuxy9DjT{ickhGdqhmC_i+INQGU zNel (ZELSU&2R;gHEv)$`cGEXrH-ZNM z-#Os;=klTC3r6L+Nys~t0I28Aiq9iuOVth_x5jn*N#-Zd?fPXn>@jg<$h^Y1yx+Ym zU7pdpetmDa{kQOFD~`E+wM+eM*2Oy6+ZWSQ21bKLWrYP$-^Wje$G8WVWfUSp3f<_f zk)(ogIw3qZHr9G@TuG_~d234o2+Qe!>nM<6T`j|wGg1rmZywv$a(sT}oBDl9ON5`C z5GxQf=TcTI2MlkWxR0#GZEH-3GIW!>dIZ&A{F8Z3x8ISq$THWZpAf-u%~cN|_svwx z$8)wlGN=mI#wO1QiZ7S(-i%LY #Q%Ul{&=#juJHfBO1?YUt~B;+W>&Ni;kFup?#+R9 z(+2^=Tz5C2JYd5Ygd=UH6W`76RoPXel_e?P?Ln%xJ0fd>@qW3p-xrMmLg3=FDrBm= zuy}TOcz8F0{BU%1v{4+i5oabj*ohsRlX5T6O?_a^kqE2FZq?qWe) zi;CHXh0bYUl3~+lbL|1=s`qZdUWVvYjCJA0mP9at;i;bPeUZQspJe=|LbHcbLG;_S zUMTM4DiQ0rV79r#>w^pHCkCFopgIoi)ysZ?aH>4=%qBwrDoB!QW2R}tt!|hSBsc3^ zg?p&sYd)`PvE*vR-uC-RZ0?x~RPFe;j_n&bzr+Nv@)y~;N?vhp334f={zct_n$!r{ zT+zy8<_pKVt;&zDp)f10n7!_Dg1KAzvp-hIpcg^UfP-IMVZD5>9gxazHJCU;(Uekk z{INnTq&}SyEE%+OT9mqo7WmJS$tEa^gCMcwTrneazE8|}sCtnD1UaB@_CwFW7kco5 zsp*V|DuBEtC}Wy%v?J@xa}d9B=!v;Y?sJ;AWUV%~P^OBZVcn&1vTrtWc8pHWHn@Hw zQLcUUh)XV{{%CcCl k1Kp-f`l+gf)5j5F( z5_bK_c_kekor?hOaif |usV!K4 |FkH=O~z3NZHyditWacyuoy)-FCb&M%FVQnRHs z0C9yfTv+p-q0?dxeVQs9ivz+<4tb@Xh71@*GjwSAg(3sE7Y~%h8)@KF*w~KqHLMCc z*JZ$#ru=xGj|ZGfTbg5CB02#~%gDv5ebG{Sy!Kr7itf;C1-