diff --git a/cypress/integration/rendering/sequencediagram.spec.js b/cypress/integration/rendering/sequencediagram.spec.js index 8e15f3fac..7b4e98b4d 100644 --- a/cypress/integration/rendering/sequencediagram.spec.js +++ b/cypress/integration/rendering/sequencediagram.spec.js @@ -1,4 +1,4 @@ -/// +// import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts'; @@ -68,6 +68,19 @@ context('Sequence diagram', () => { { sequence: { actorFontFamily: 'courier' } } ); }); + it('should render bidirectional arrows', () => { + imgSnapshotTest( + ` + sequenceDiagram + Alice<<->>John: Hello John, how are you? + Alice<<-->>John: Hi Alice, I can hear you! + John<<->>Alice: This also works the other way + John<<-->>Alice: Yes + Alice->John: Test + John->>Alice: Still works + ` + ); + }); it('should handle different line breaks', () => { imgSnapshotTest( ` diff --git a/docs/config/setup/interfaces/mermaidAPI.ParseOptions.md b/docs/config/setup/interfaces/mermaidAPI.ParseOptions.md index fa100744e..c388a4f26 100644 --- a/docs/config/setup/interfaces/mermaidAPI.ParseOptions.md +++ b/docs/config/setup/interfaces/mermaidAPI.ParseOptions.md @@ -19,4 +19,4 @@ The `parseError` function will not be called. #### Defined in -[mermaidAPI.ts:64](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L64) +[mermaidAPI.ts:65](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L65) diff --git a/docs/config/setup/interfaces/mermaidAPI.ParseResult.md b/docs/config/setup/interfaces/mermaidAPI.ParseResult.md index 9f912cc8c..376f29346 100644 --- a/docs/config/setup/interfaces/mermaidAPI.ParseResult.md +++ b/docs/config/setup/interfaces/mermaidAPI.ParseResult.md @@ -18,4 +18,4 @@ The diagram type, e.g. 'flowchart', 'sequence', etc. #### Defined in -[mermaidAPI.ts:71](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L71) +[mermaidAPI.ts:72](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L72) diff --git a/docs/config/setup/interfaces/mermaidAPI.RenderResult.md b/docs/config/setup/interfaces/mermaidAPI.RenderResult.md index b5cc48038..52ef3ec0c 100644 --- a/docs/config/setup/interfaces/mermaidAPI.RenderResult.md +++ b/docs/config/setup/interfaces/mermaidAPI.RenderResult.md @@ -39,7 +39,7 @@ bindFunctions?.(div); // To call bindFunctions only if it's present. #### Defined in -[mermaidAPI.ts:94](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L94) +[mermaidAPI.ts:95](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L95) --- @@ -51,7 +51,7 @@ The diagram type, e.g. 'flowchart', 'sequence', etc. #### Defined in -[mermaidAPI.ts:84](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L84) +[mermaidAPI.ts:85](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L85) --- @@ -63,4 +63,4 @@ The svg code for the rendered graph. #### Defined in -[mermaidAPI.ts:80](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L80) +[mermaidAPI.ts:81](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L81) diff --git a/docs/config/setup/modules/mermaidAPI.md b/docs/config/setup/modules/mermaidAPI.md index d17533a03..d7bfe68ef 100644 --- a/docs/config/setup/modules/mermaidAPI.md +++ b/docs/config/setup/modules/mermaidAPI.md @@ -26,7 +26,7 @@ Renames and re-exports [mermaidAPI](mermaidAPI.md#mermaidapi) #### Defined in -[mermaidAPI.ts:74](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L74) +[mermaidAPI.ts:75](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L75) ## Variables @@ -155,7 +155,7 @@ the cleaned up svgCode #### Defined in -[mermaidAPI.ts:222](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L222) +[mermaidAPI.ts:223](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L223) --- @@ -180,7 +180,7 @@ the string with all the user styles #### Defined in -[mermaidAPI.ts:153](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L153) +[mermaidAPI.ts:154](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L154) --- @@ -203,7 +203,7 @@ the string with all the user styles #### Defined in -[mermaidAPI.ts:199](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L199) +[mermaidAPI.ts:200](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L200) --- @@ -230,7 +230,7 @@ with an enclosing block that has each of the cssClasses followed by !important; #### Defined in -[mermaidAPI.ts:138](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L138) +[mermaidAPI.ts:139](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L139) --- @@ -252,7 +252,6 @@ Put the svgCode into an iFrame. Return the iFrame code `string` - the code with the iFrame that now contains the svgCode - TODO replace btoa(). Replace with buf.toString('base64')? #### Defined in diff --git a/docs/syntax/sequenceDiagram.md b/docs/syntax/sequenceDiagram.md index e39e660e8..0f0d63213 100644 --- a/docs/syntax/sequenceDiagram.md +++ b/docs/syntax/sequenceDiagram.md @@ -206,18 +206,20 @@ Messages can be of two displayed either solid or with a dotted line. [Actor][Arrow][Actor]:Message text ``` -There are six types of arrows currently supported: +There are ten types of arrows currently supported: -| Type | Description | -| ------ | ------------------------------------------------ | -| `->` | Solid line without arrow | -| `-->` | Dotted line without arrow | -| `->>` | Solid line with arrowhead | -| `-->>` | Dotted line with arrowhead | -| `-x` | Solid line with a cross at the end | -| `--x` | Dotted line with a cross at the end. | -| `-)` | Solid line with an open arrow at the end (async) | -| `--)` | Dotted line with a open arrow at the end (async) | +| Type | Description | +| -------- | ------------------------------------------------------------------------ | +| `->` | Solid line without arrow | +| `-->` | Dotted line without arrow | +| `->>` | Solid line with arrowhead | +| `-->>` | Dotted line with arrowhead | +| `<<->>` | Solid line with bidirectional arrowheads (v\+) | +| `<<-->>` | Dotted line with bidirectional arrowheads (v\+) | +| `-x` | Solid line with a cross at the end | +| `--x` | Dotted line with a cross at the end. | +| `-)` | Solid line with an open arrow at the end (async) | +| `--)` | Dotted line with a open arrow at the end (async) | ## Activations diff --git a/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison b/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison index 1d5720707..11b39d232 100644 --- a/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison +++ b/packages/mermaid/src/diagrams/sequence/parser/sequenceDiagram.jison @@ -33,7 +33,7 @@ "actor" { this.begin('ID'); return 'participant_actor'; } "create" return 'create'; "destroy" { this.begin('ID'); return 'destroy'; } -[^\->:\n,;]+?([\-]*[^\->:\n,;]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; } +[^\<->\->:\n,;]+?([\-]*[^\<->\->:\n,;]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; } "as" { this.popState(); this.popState(); this.begin('LINE'); return 'AS'; } (?:) { this.popState(); this.popState(); return 'NEWLINE'; } "loop" { this.begin('LINE'); return 'loop'; } @@ -73,9 +73,11 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili "off" return 'off'; "," return ','; ";" return 'NEWLINE'; -[^\+\->:\n,;]+((?!(\-x|\-\-x|\-\)|\-\-\)))[\-]*[^\+\->:\n,;]+)* { yytext = yytext.trim(); return 'ACTOR'; } +[^\+\<->\->:\n,;]+((?!(\-x|\-\-x|\-\)|\-\-\)))[\-]*[^\+\<->\->:\n,;]+)* { yytext = yytext.trim(); return 'ACTOR'; } "->>" return 'SOLID_ARROW'; +"<<->>" return 'BIDIRECTIONAL_SOLID_ARROW'; "-->>" return 'DOTTED_ARROW'; +"<<-->>" return 'BIDIRECTIONAL_DOTTED_ARROW'; "->" return 'SOLID_OPEN_ARROW'; "-->" return 'DOTTED_OPEN_ARROW'; \-[x] return 'SOLID_CROSS'; @@ -310,7 +312,9 @@ signaltype : SOLID_OPEN_ARROW { $$ = yy.LINETYPE.SOLID_OPEN; } | DOTTED_OPEN_ARROW { $$ = yy.LINETYPE.DOTTED_OPEN; } | SOLID_ARROW { $$ = yy.LINETYPE.SOLID; } + | BIDIRECTIONAL_SOLID_ARROW { $$ = yy.LINETYPE.BIDIRECTIONAL_SOLID; } | DOTTED_ARROW { $$ = yy.LINETYPE.DOTTED; } + | BIDIRECTIONAL_DOTTED_ARROW { $$ = yy.LINETYPE.BIDIRECTIONAL_DOTTED; } | SOLID_CROSS { $$ = yy.LINETYPE.SOLID_CROSS; } | DOTTED_CROSS { $$ = yy.LINETYPE.DOTTED_CROSS; } | SOLID_POINT { $$ = yy.LINETYPE.SOLID_POINT; } diff --git a/packages/mermaid/src/diagrams/sequence/sequenceDb.ts b/packages/mermaid/src/diagrams/sequence/sequenceDb.ts index 7e6c21b3c..19f6d561a 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceDb.ts +++ b/packages/mermaid/src/diagrams/sequence/sequenceDb.ts @@ -328,6 +328,8 @@ export const LINETYPE = { BREAK_START: 30, BREAK_END: 31, PAR_OVER_START: 32, + BIDIRECTIONAL_SOLID: 33, + BIDIRECTIONAL_DOTTED: 34, }; export const ARROWTYPE = { diff --git a/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js b/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js index 97d528df6..87a686129 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js +++ b/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js @@ -516,6 +516,36 @@ Alice->>Bob:Hello Bob, how are you?`; expect(messages.length).toBe(1); expect(messages[0].type).toBe(diagram.db.LINETYPE.DOTTED); }); + it('should handle bidirectional arrow messages', async () => { + const str = ` +sequenceDiagram +Alice<<->>Bob:Hello Bob, how are you?`; + + await mermaidAPI.parse(str); + const actors = diagram.db.getActors(); + expect(actors.get('Alice').description).toBe('Alice'); + expect(actors.get('Bob').description).toBe('Bob'); + + const messages = diagram.db.getMessages(); + + expect(messages.length).toBe(1); + expect(messages[0].type).toBe(diagram.db.LINETYPE.BIDIRECTIONAL_SOLID); + }); + it('should handle bidirectional dotted arrow messages', async () => { + const str = ` + sequenceDiagram + Alice<<-->>Bob:Hello Bob, how are you?`; + + await mermaidAPI.parse(str); + const actors = diagram.db.getActors(); + expect(actors.get('Alice').description).toBe('Alice'); + expect(actors.get('Bob').description).toBe('Bob'); + + const messages = diagram.db.getMessages(); + + expect(messages.length).toBe(1); + expect(messages[0].type).toBe(diagram.db.LINETYPE.BIDIRECTIONAL_DOTTED); + }); it('should handle actor activation', async () => { const str = ` sequenceDiagram diff --git a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts index 43d1db890..42bacd5d6 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts +++ b/packages/mermaid/src/diagrams/sequence/sequenceRenderer.ts @@ -436,7 +436,8 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO type === diagObj.db.LINETYPE.DOTTED || type === diagObj.db.LINETYPE.DOTTED_CROSS || type === diagObj.db.LINETYPE.DOTTED_POINT || - type === diagObj.db.LINETYPE.DOTTED_OPEN + type === diagObj.db.LINETYPE.DOTTED_OPEN || + type === diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED ) { line.style('stroke-dasharray', '3, 3'); line.attr('class', 'messageLine1'); @@ -462,6 +463,13 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO if (type === diagObj.db.LINETYPE.SOLID || type === diagObj.db.LINETYPE.DOTTED) { line.attr('marker-end', 'url(' + url + '#arrowhead)'); } + if ( + type === diagObj.db.LINETYPE.BIDIRECTIONAL_SOLID || + type === diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED + ) { + line.attr('marker-start', 'url(' + url + '#arrowhead)'); + line.attr('marker-end', 'url(' + url + '#arrowhead)'); + } if (type === diagObj.db.LINETYPE.SOLID_POINT || type === diagObj.db.LINETYPE.DOTTED_POINT) { line.attr('marker-end', 'url(' + url + '#filled-head)'); } @@ -1036,6 +1044,8 @@ export const draw = async function (_text: string, id: string, _version: string, diagObj.db.LINETYPE.DOTTED_CROSS, diagObj.db.LINETYPE.SOLID_POINT, diagObj.db.LINETYPE.DOTTED_POINT, + diagObj.db.LINETYPE.BIDIRECTIONAL_SOLID, + diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED, ].includes(msg.type) ) { sequenceIndex = sequenceIndex + sequenceIndexStep; @@ -1416,6 +1426,8 @@ const buildMessageModel = function (msg, actors, diagObj) { diagObj.db.LINETYPE.DOTTED_CROSS, diagObj.db.LINETYPE.SOLID_POINT, diagObj.db.LINETYPE.DOTTED_POINT, + diagObj.db.LINETYPE.BIDIRECTIONAL_SOLID, + diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED, ].includes(msg.type) ) { return {}; @@ -1423,7 +1435,7 @@ const buildMessageModel = function (msg, actors, diagObj) { const [fromLeft, fromRight] = activationBounds(msg.from, actors); const [toLeft, toRight] = activationBounds(msg.to, actors); const isArrowToRight = fromLeft <= toLeft; - const startx = isArrowToRight ? fromRight : fromLeft; + let startx = isArrowToRight ? fromRight : fromLeft; let stopx = isArrowToRight ? toLeft : toRight; // As the line width is considered, the left and right values will be off by 2. @@ -1462,6 +1474,17 @@ const buildMessageModel = function (msg, actors, diagObj) { if (![diagObj.db.LINETYPE.SOLID_OPEN, diagObj.db.LINETYPE.DOTTED_OPEN].includes(msg.type)) { stopx += adjustValue(3); } + + /** + * Shorten start position of bidirectional arrow to accommodate for second arrowhead + */ + if ( + [diagObj.db.LINETYPE.BIDIRECTIONAL_SOLID, diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED].includes( + msg.type + ) + ) { + startx -= adjustValue(3); + } } const allBounds = [fromLeft, fromRight, toLeft, toRight]; diff --git a/packages/mermaid/src/diagrams/sequence/svgDraw.js b/packages/mermaid/src/diagrams/sequence/svgDraw.js index 848455f78..bcc06602f 100644 --- a/packages/mermaid/src/diagrams/sequence/svgDraw.js +++ b/packages/mermaid/src/diagrams/sequence/svgDraw.js @@ -735,7 +735,7 @@ export const insertArrowHead = function (elem) { .attr('markerUnits', 'userSpaceOnUse') .attr('markerWidth', 12) .attr('markerHeight', 12) - .attr('orient', 'auto') + .attr('orient', 'auto-start-reverse') .append('path') .attr('d', 'M -1 0 L 10 5 L 0 10 z'); // this is actual shape for arrowhead }; diff --git a/packages/mermaid/src/docs/syntax/sequenceDiagram.md b/packages/mermaid/src/docs/syntax/sequenceDiagram.md index ae2c7fd02..249f7bde0 100644 --- a/packages/mermaid/src/docs/syntax/sequenceDiagram.md +++ b/packages/mermaid/src/docs/syntax/sequenceDiagram.md @@ -141,18 +141,20 @@ Messages can be of two displayed either solid or with a dotted line. [Actor][Arrow][Actor]:Message text ``` -There are six types of arrows currently supported: +There are ten types of arrows currently supported: -| Type | Description | -| ------ | ------------------------------------------------ | -| `->` | Solid line without arrow | -| `-->` | Dotted line without arrow | -| `->>` | Solid line with arrowhead | -| `-->>` | Dotted line with arrowhead | -| `-x` | Solid line with a cross at the end | -| `--x` | Dotted line with a cross at the end. | -| `-)` | Solid line with an open arrow at the end (async) | -| `--)` | Dotted line with a open arrow at the end (async) | +| Type | Description | +| -------- | ----------------------------------------------------------------------- | +| `->` | Solid line without arrow | +| `-->` | Dotted line without arrow | +| `->>` | Solid line with arrowhead | +| `-->>` | Dotted line with arrowhead | +| `<<->>` | Solid line with bidirectional arrowheads (v+) | +| `<<-->>` | Dotted line with bidirectional arrowheads (v+) | +| `-x` | Solid line with a cross at the end | +| `--x` | Dotted line with a cross at the end. | +| `-)` | Solid line with an open arrow at the end (async) | +| `--)` | Dotted line with a open arrow at the end (async) | ## Activations diff --git a/packages/mermaid/src/mermaid.spec.ts b/packages/mermaid/src/mermaid.spec.ts index 9360f7bab..d03f0ee9d 100644 --- a/packages/mermaid/src/mermaid.spec.ts +++ b/packages/mermaid/src/mermaid.spec.ts @@ -207,7 +207,7 @@ describe('when using mermaid and ', () => { [Error: Parse error on line 2: ...equenceDiagramAlice:->Bob: Hello Bob, h... ----------------------^ - Expecting 'SOLID_OPEN_ARROW', 'DOTTED_OPEN_ARROW', 'SOLID_ARROW', 'DOTTED_ARROW', 'SOLID_CROSS', 'DOTTED_CROSS', 'SOLID_POINT', 'DOTTED_POINT', got 'TXT'] + Expecting 'SOLID_OPEN_ARROW', 'DOTTED_OPEN_ARROW', 'SOLID_ARROW', 'BIDIRECTIONAL_SOLID_ARROW', 'DOTTED_ARROW', 'BIDIRECTIONAL_DOTTED_ARROW', 'SOLID_CROSS', 'DOTTED_CROSS', 'SOLID_POINT', 'DOTTED_POINT', got 'TXT'] `); }); diff --git a/packages/mermaid/src/mermaidAPI.spec.ts b/packages/mermaid/src/mermaidAPI.spec.ts index aa5c7a7fb..1134f8635 100644 --- a/packages/mermaid/src/mermaidAPI.spec.ts +++ b/packages/mermaid/src/mermaidAPI.spec.ts @@ -69,6 +69,7 @@ vi.mock('stylis', () => { import { compile, serialize } from 'stylis'; import { decodeEntities, encodeEntities } from './utils.js'; import { Diagram } from './Diagram.js'; +import { toBase64 } from './utils/base64.js'; /** * @see https://vitest.dev/guide/mocking.html Mock part of a module @@ -176,7 +177,7 @@ describe('mermaidAPI', () => { }); describe('putIntoIFrame', () => { - const inputSvgCode = 'this is the SVG code'; + const inputSvgCode = 'this is the SVG code ⛵'; it('uses the default SVG iFrame height is used if no svgElement given', () => { const result = putIntoIFrame(inputSvgCode); @@ -199,11 +200,10 @@ describe('mermaidAPI', () => { }); it('sets src to base64 version of svgCode', () => { - const base64encodedSrc = btoa('' + inputSvgCode + ''); - const expectedRegExp = new RegExp('src="data:text/html;base64,' + base64encodedSrc + '"'); - + const base64encodedSrc = toBase64(`${inputSvgCode}`); + const expectedSrc = `src="data:text/html;charset=UTF-8;base64,${base64encodedSrc}"`; const result = putIntoIFrame(inputSvgCode); - expect(result).toMatch(expectedRegExp); + expect(result).toContain(expectedSrc); }); it('uses the height and appends px from the svgElement given', () => { diff --git a/packages/mermaid/src/mermaidAPI.ts b/packages/mermaid/src/mermaidAPI.ts index 365467dbd..ee6696af9 100644 --- a/packages/mermaid/src/mermaidAPI.ts +++ b/packages/mermaid/src/mermaidAPI.ts @@ -31,6 +31,7 @@ import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility. import type { DiagramMetadata, DiagramStyleClassDef } from './diagram-api/types.js'; import { preprocessDiagram } from './preprocess.js'; import { decodeEntities } from './utils.js'; +import { toBase64 } from './utils/base64.js'; const MAX_TEXTLENGTH = 50_000; const MAX_TEXTLENGTH_EXCEEDED_MSG = @@ -248,14 +249,13 @@ export const cleanUpSvgCode = ( * @param svgCode - the svg code to put inside the iFrame * @param svgElement - the d3 node that has the current svgElement so we can get the height from it * @returns - the code with the iFrame that now contains the svgCode - * TODO replace btoa(). Replace with buf.toString('base64')? */ export const putIntoIFrame = (svgCode = '', svgElement?: D3Element): string => { const height = svgElement?.viewBox?.baseVal?.height ? svgElement.viewBox.baseVal.height + 'px' : IFRAME_HEIGHT; - const base64encodedSrc = btoa('' + svgCode + ''); - return ``; }; diff --git a/packages/mermaid/src/utils/base64.ts b/packages/mermaid/src/utils/base64.ts new file mode 100644 index 000000000..4a0f5b295 --- /dev/null +++ b/packages/mermaid/src/utils/base64.ts @@ -0,0 +1,6 @@ +export function toBase64(str: string) { + // ref: https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem + const utf8Bytes = new TextEncoder().encode(str); + const utf8Str = Array.from(utf8Bytes, (byte) => String.fromCodePoint(byte)).join(''); + return btoa(utf8Str); +}