diff --git a/packages/bruno-app/src/components/CollectionSettings/Headers/index.js b/packages/bruno-app/src/components/CollectionSettings/Headers/index.js
index 45e3e5834..a03c93506 100644
--- a/packages/bruno-app/src/components/CollectionSettings/Headers/index.js
+++ b/packages/bruno-app/src/components/CollectionSettings/Headers/index.js
@@ -45,7 +45,8 @@ const Headers = ({ collection }) => {
const header = cloneDeep(_header);
switch (type) {
case 'name': {
- header.name = e.target.value;
+ // Strip newlines from header keys
+ header.name = e.target.value.replace(/[\r\n]/g, '');
break;
}
case 'value': {
diff --git a/packages/bruno-app/src/components/CollectionSettings/Vars/VarsTable/index.js b/packages/bruno-app/src/components/CollectionSettings/Vars/VarsTable/index.js
index fd15eee8c..e84ddc081 100644
--- a/packages/bruno-app/src/components/CollectionSettings/Vars/VarsTable/index.js
+++ b/packages/bruno-app/src/components/CollectionSettings/Vars/VarsTable/index.js
@@ -4,7 +4,7 @@ import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
-import SingleLineEditor from 'components/SingleLineEditor';
+import MultiLineEditor from 'components/MultiLineEditor';
import InfoTip from 'components/InfoTip';
import StyledWrapper from './StyledWrapper';
import toast from 'react-hot-toast';
@@ -114,7 +114,7 @@ const VarsTable = ({ collection, vars, varType }) => {
/>
- {
const header = cloneDeep(_header);
switch (type) {
case 'name': {
- header.name = e.target.value;
+ // Strip newlines from header keys
+ header.name = e.target.value.replace(/[\r\n]/g, '');
break;
}
case 'value': {
diff --git a/packages/bruno-app/src/components/FolderSettings/Vars/VarsTable/index.js b/packages/bruno-app/src/components/FolderSettings/Vars/VarsTable/index.js
index b0815c018..69f083594 100644
--- a/packages/bruno-app/src/components/FolderSettings/Vars/VarsTable/index.js
+++ b/packages/bruno-app/src/components/FolderSettings/Vars/VarsTable/index.js
@@ -4,7 +4,7 @@ import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
-import SingleLineEditor from 'components/SingleLineEditor';
+import MultiLineEditor from 'components/MultiLineEditor';
import InfoTip from 'components/InfoTip';
import StyledWrapper from './StyledWrapper';
import toast from 'react-hot-toast';
@@ -113,7 +113,7 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
/>
|
- {
<>
{hoveredRow === index && (
<>
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AdditionalParams/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AdditionalParams/index.js
index 1d2f81bee..93b519464 100644
--- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AdditionalParams/index.js
+++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AdditionalParams/index.js
@@ -4,7 +4,8 @@ import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { IconPlus, IconTrash, IconAdjustmentsHorizontal } from '@tabler/icons';
import { cloneDeep } from "lodash";
-import SingleLineEditor from "components/SingleLineEditor/index";
+import SingleLineEditor from 'components/SingleLineEditor/index';
+import MultiLineEditor from 'components/MultiLineEditor/index';
import StyledWrapper from "./StyledWrapper";
import Table from "components/Table/index";
@@ -205,7 +206,7 @@ const AdditionalParams = ({ item = {}, request, updateAuth, collection, handleS
/>
|
- handleUpdateAdditionalParam({
diff --git a/packages/bruno-app/src/components/RequestPane/QueryParams/index.js b/packages/bruno-app/src/components/RequestPane/QueryParams/index.js
index b5c2c69a7..4a1e37f4c 100644
--- a/packages/bruno-app/src/components/RequestPane/QueryParams/index.js
+++ b/packages/bruno-app/src/components/RequestPane/QueryParams/index.js
@@ -13,7 +13,7 @@ import {
updatePathParam,
setQueryParams
} from 'providers/ReduxStore/slices/collections';
-import SingleLineEditor from 'components/SingleLineEditor';
+import MultiLineEditor from 'components/MultiLineEditor';
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
@@ -168,7 +168,7 @@ const QueryParams = ({ item, collection }) => {
/>
|
- {
/>
|
- {
collection={collection}
highlightPathParams={true}
item={item}
+ showNewlineArrow={true}
/>
{
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const headers = item.draft ? get(item, 'draft.request.headers') : get(item, 'request.headers');
-
+
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
const addHeader = () => {
@@ -36,9 +36,11 @@ const RequestHeaders = ({ item, collection, addHeaderText }) => {
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleHeaderValueChange = (e, _header, type) => {
const header = cloneDeep(_header);
+
switch (type) {
case 'name': {
- header.name = e.target.value;
+ // Strip newlines from header keys
+ header.name = e.target.value.replace(/[\r\n]/g, '');
break;
}
case 'value': {
@@ -50,6 +52,7 @@ const RequestHeaders = ({ item, collection, addHeaderText }) => {
break;
}
}
+
dispatch(
updateRequestHeader({
header: header,
@@ -154,7 +157,6 @@ const RequestHeaders = ({ item, collection, addHeaderText }) => {
}
onRun={handleRun}
autocomplete={MimeTypes}
- allowNewlines={true}
collection={collection}
item={item}
/>
diff --git a/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js b/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js
index cd3f83797..1189747b4 100644
--- a/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js
+++ b/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js
@@ -5,7 +5,7 @@ import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { addVar, updateVar, deleteVar, moveVar } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
-import SingleLineEditor from 'components/SingleLineEditor';
+import MultiLineEditor from 'components/MultiLineEditor';
import InfoTip from 'components/InfoTip';
import StyledWrapper from './StyledWrapper';
import toast from 'react-hot-toast';
@@ -122,7 +122,7 @@ const VarsTable = ({ item, collection, vars, varType }) => {
/>
|
- {
+ if (!this.editor) return;
+
+ // Clear existing markers
+ this._clearNewlineMarkers();
+
+ this.newlineMarkers = [];
+ const content = this.editor.getValue();
+
+ // Find all newlines and replace them with arrow widgets
+ for (let i = 0; i < content.length; i++) {
+ if (content[i] === '\n') {
+ const pos = this.editor.posFromIndex(i);
+ const nextPos = this.editor.posFromIndex(i + 1);
+
+ // Create a widget to display the arrow
+ const arrow = document.createElement('span');
+ arrow.className = 'newline-arrow';
+ arrow.textContent = '↲';
+ arrow.style.cssText = `
+ color: #888;
+ font-size: 8px;
+ margin: 0 2px;
+ vertical-align: middle;
+ display: inline-block;
+ `;
+
+ // Mark the newline character and replace it with the arrow widget
+ const marker = this.editor.markText(pos, nextPos, {
+ replacedWith: arrow,
+ handleMouseEvents: true
+ });
+
+ this.newlineMarkers.push(marker);
+ }
+ }
+ };
+
+ /**
+ * Clear all newline markers
+ */
+ _clearNewlineMarkers = () => {
+ if (this.newlineMarkers) {
+ this.newlineMarkers.forEach((marker) => {
+ try {
+ marker.clear();
+ } catch (e) {
+ // Marker might already be cleared
+ }
+ });
+ this.newlineMarkers = [];
+ }
+ };
+
toggleVisibleSecret = () => {
const isVisible = !this.state.maskInput;
this.setState({ maskInput: isVisible });
@@ -204,13 +277,15 @@ class SingleLineEditor extends Component {
render() {
return (
-
+
- {this.secretEye(this.props.isSecret)}
+
+ {this.secretEye(this.props.isSecret)}
+
);
}
diff --git a/packages/bruno-lang/v2/src/collectionBruToJson.js b/packages/bruno-lang/v2/src/collectionBruToJson.js
index f3925ad62..4d0ce8d6c 100644
--- a/packages/bruno-lang/v2/src/collectionBruToJson.js
+++ b/packages/bruno-lang/v2/src/collectionBruToJson.js
@@ -20,12 +20,22 @@ const grammar = ohm.grammar(`Bru {
keychar = ~(tagend | st | nl | ":") any
valuechar = ~(nl | tagend) any
+ // Multiline text block surrounded by '''
+ multilinetextblockdelimiter = "'''"
+ multilinetextblock = multilinetextblockdelimiter (~multilinetextblockdelimiter any)* multilinetextblockdelimiter
+
// Dictionary Blocks
dictionary = st* "{" pairlist? tagend
pairlist = optionalnl* pair (~tagend stnl* pair)* (~tagend space)*
- pair = st* key st* ":" st* value st*
+ pair = st* (quoted_key | key) st* ":" st* value st*
+ disable_char = "~"
+ quote_char = "\\""
+ esc_char = "\\\\"
+ esc_quote_char = esc_char quote_char
+ quoted_key_char = ~(quote_char | esc_quote_char | nl) any
+ quoted_key = disable_char? quote_char (esc_quote_char | quoted_key_char)* quote_char
key = keychar*
- value = valuechar*
+ value = multilinetextblock | valuechar*
// Text Blocks
textblock = textline (~tagend nl textline)*
@@ -137,10 +147,38 @@ const sem = grammar.createSemantics().addAttribute('ast', {
res[key.ast] = value.ast ? value.ast.trim() : '';
return res;
},
+ quoted_key(disabled, _1, chars, _2) {
+ // unquote and handle disabled prefix
+ return (disabled ? disabled.sourceString : '') + chars.ast.join('');
+ },
+ esc_quote_char(_1, quote) {
+ // unescape
+ return quote.sourceString;
+ },
+ quoted_key_char(char) {
+ // return the character itself
+ return char.sourceString;
+ },
key(chars) {
return chars.sourceString ? chars.sourceString.trim() : '';
},
value(chars) {
+ if (chars.ctorName === 'list') {
+ return chars.ast;
+ }
+ try {
+ let isMultiline = chars.sourceString?.startsWith(`'''`) && chars.sourceString?.endsWith(`'''`);
+ if (isMultiline) {
+ const multilineString = chars.sourceString?.replace(/^'''|'''$/g, '');
+ return multilineString
+ .split('\n')
+ .map((line) => line.slice(4))
+ .join('\n');
+ }
+ return chars.sourceString ? chars.sourceString.trim() : '';
+ } catch (err) {
+ console.error(err);
+ }
return chars.sourceString ? chars.sourceString.trim() : '';
},
textblock(line, _1, rest) {
@@ -152,6 +190,10 @@ const sem = grammar.createSemantics().addAttribute('ast', {
textchar(char) {
return char.sourceString;
},
+ multilinetextblock(_1, content, _2) {
+ // Join all the content between the triple quotes and trim it
+ return content.sourceString.trim();
+ },
nl(_1, _2) {
return '';
},
diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js
index 9258be11b..b7decd9ef 100644
--- a/packages/bruno-lang/v2/src/jsonToBru.js
+++ b/packages/bruno-lang/v2/src/jsonToBru.js
@@ -1,14 +1,10 @@
const _ = require('lodash');
-const { indentString, getValueString } = require('./utils');
+const { indentString, getValueString, getKeyString, getValueUrl } = require('./utils');
const jsonToExampleBru = require('./example/jsonToBru');
const enabled = (items = [], key = "enabled") => items.filter((item) => item[key]);
const disabled = (items = [], key = "enabled") => items.filter((item) => !item[key]);
-const quoteKey = (key) => {
- const quotableChars = [':', '"', '{', '}', ' '];
- return quotableChars.some(char => key.includes(char)) ? ('"' + key.replaceAll('"', '\\"') + '"') : key;
-}
// remove the last line if two new lines are found
const stripLastLine = (text) => {
@@ -50,7 +46,7 @@ const jsonToBru = (json) => {
const isStandard = standardMethods.has(method);
bru += isStandard ? `${method} {` : `http {\n method: ${method}`;
- bru += `\n url: ${url}`;
+ bru += `\n url: ${getValueUrl(url)}`;
if (body?.length) {
bru += `\n body: ${body}`;
@@ -133,7 +129,7 @@ const jsonToBru = (json) => {
if (enabled(queryParams).length) {
bru += `\n${indentString(
enabled(queryParams)
- .map((item) => `${quoteKey(item.name)}: ${item.value}`)
+ .map((item) => `${getKeyString(item.name)}: ${getValueString(item.value)}`)
.join('\n')
)}`;
}
@@ -141,7 +137,7 @@ const jsonToBru = (json) => {
if (disabled(queryParams).length) {
bru += `\n${indentString(
disabled(queryParams)
- .map((item) => `~${quoteKey(item.name)}: ${item.value}`)
+ .map((item) => `~${getKeyString(item.name)}: ${getValueString(item.value)}`)
.join('\n')
)}`;
}
@@ -152,7 +148,7 @@ const jsonToBru = (json) => {
if (pathParams.length) {
bru += 'params:path {';
- bru += `\n${indentString(pathParams.map((item) => `${item.name}: ${item.value}`).join('\n'))}`;
+ bru += `\n${indentString(pathParams.map((item) => `${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
bru += '\n}\n\n';
}
@@ -163,7 +159,7 @@ const jsonToBru = (json) => {
if (enabled(headers).length) {
bru += `\n${indentString(
enabled(headers)
- .map((item) => `${quoteKey(item.name)}: ${item.value}`)
+ .map((item) => `${getKeyString(item.name)}: ${getValueString(item.value)}`)
.join('\n')
)}`;
}
@@ -171,7 +167,7 @@ const jsonToBru = (json) => {
if (disabled(headers).length) {
bru += `\n${indentString(
disabled(headers)
- .map((item) => `~${quoteKey(item.name)}: ${item.value}`)
+ .map((item) => `~${getKeyString(item.name)}: ${getValueString(item.value)}`)
.join('\n')
)}`;
}
@@ -184,7 +180,7 @@ const jsonToBru = (json) => {
if (enabled(metadata).length) {
bru += `\n${indentString(
enabled(metadata)
- .map((item) => `${item.name}: ${item.value}`)
+ .map((item) => `${item.name}: ${getValueString(item.value)}`)
.join('\n')
)}`;
}
@@ -192,7 +188,7 @@ const jsonToBru = (json) => {
if (disabled(metadata).length) {
bru += `\n${indentString(
disabled(metadata)
- .map((item) => `~${item.name}: ${item.value}`)
+ .map((item) => `~${item.name}: ${getValueString(item.value)}`)
.join('\n')
)}`;
}
@@ -360,7 +356,7 @@ ${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken ?? true).toStr
${indentString(
authorizationHeaders
.filter(item => item?.name?.length)
- .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
+ .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
.join('\n')
)}
}
@@ -373,7 +369,7 @@ ${indentString(
${indentString(
authorizationQueryParams
.filter(item => item?.name?.length)
- .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
+ .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
.join('\n')
)}
}
@@ -386,7 +382,7 @@ ${indentString(
${indentString(
tokenHeaders
.filter(item => item?.name?.length)
- .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
+ .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
.join('\n')
)}
}
@@ -399,7 +395,7 @@ ${indentString(
${indentString(
tokenQueryParams
.filter(item => item?.name?.length)
- .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
+ .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
.join('\n')
)}
}
@@ -412,7 +408,7 @@ ${indentString(
${indentString(
tokenBodyValues
.filter(item => item?.name?.length)
- .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
+ .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
.join('\n')
)}
}
@@ -425,7 +421,7 @@ ${indentString(
${indentString(
refreshHeaders
.filter(item => item?.name?.length)
- .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
+ .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
.join('\n')
)}
}
@@ -438,7 +434,7 @@ ${indentString(
${indentString(
refreshQueryParams
.filter(item => item?.name?.length)
- .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
+ .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
.join('\n')
)}
}
@@ -451,7 +447,7 @@ ${indentString(
${indentString(
refreshBodyValues
.filter(item => item?.name?.length)
- .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
+ .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
.join('\n')
)}
}
@@ -508,14 +504,14 @@ ${indentString(body.sparql)}
if (enabled(body.formUrlEncoded).length) {
const enabledValues = enabled(body.formUrlEncoded)
- .map((item) => `${quoteKey(item.name)}: ${getValueString(item.value)}`)
+ .map((item) => `${getKeyString(item.name)}: ${getValueString(item.value)}`)
.join('\n');
bru += `${indentString(enabledValues)}\n`;
}
if (disabled(body.formUrlEncoded).length) {
const disabledValues = disabled(body.formUrlEncoded)
- .map((item) => `~${quoteKey(item.name)}: ${getValueString(item.value)}`)
+ .map((item) => `~${getKeyString(item.name)}: ${getValueString(item.value)}`)
.join('\n');
bru += `${indentString(disabledValues)}\n`;
}
@@ -536,7 +532,7 @@ ${indentString(body.sparql)}
item.contentType && item.contentType !== '' ? ' @contentType(' + item.contentType + ')' : '';
if (item.type === 'text') {
- return `${enabled}${quoteKey(item.name)}: ${getValueString(item.value)}${contentType}`;
+ return `${enabled}${getKeyString(item.name)}: ${getValueString(item.value)}${contentType}`;
}
if (item.type === 'file') {
@@ -544,7 +540,7 @@ ${indentString(body.sparql)}
const filestr = filepaths.join('|');
const value = `@file(${filestr})`;
- return `${enabled}${quoteKey(item.name)}: ${value}${contentType}`;
+ return `${enabled}${getKeyString(item.name)}: ${value}${contentType}`;
}
})
.join('\n')
@@ -644,19 +640,19 @@ ${indentString(body.sparql)}
bru += `vars:pre-request {`;
if (varsEnabled.length) {
- bru += `\n${indentString(varsEnabled.map((item) => `${item.name}: ${item.value}`).join('\n'))}`;
+ bru += `\n${indentString(varsEnabled.map((item) => `${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
}
if (varsLocalEnabled.length) {
- bru += `\n${indentString(varsLocalEnabled.map((item) => `@${item.name}: ${item.value}`).join('\n'))}`;
+ bru += `\n${indentString(varsLocalEnabled.map((item) => `@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
}
if (varsDisabled.length) {
- bru += `\n${indentString(varsDisabled.map((item) => `~${item.name}: ${item.value}`).join('\n'))}`;
+ bru += `\n${indentString(varsDisabled.map((item) => `~${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
}
if (varsLocalDisabled.length) {
- bru += `\n${indentString(varsLocalDisabled.map((item) => `~@${item.name}: ${item.value}`).join('\n'))}`;
+ bru += `\n${indentString(varsLocalDisabled.map((item) => `~@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
}
bru += '\n}\n\n';
@@ -670,19 +666,19 @@ ${indentString(body.sparql)}
bru += `vars:post-response {`;
if (varsEnabled.length) {
- bru += `\n${indentString(varsEnabled.map((item) => `${item.name}: ${item.value}`).join('\n'))}`;
+ bru += `\n${indentString(varsEnabled.map((item) => `${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
}
if (varsLocalEnabled.length) {
- bru += `\n${indentString(varsLocalEnabled.map((item) => `@${item.name}: ${item.value}`).join('\n'))}`;
+ bru += `\n${indentString(varsLocalEnabled.map((item) => `@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
}
if (varsDisabled.length) {
- bru += `\n${indentString(varsDisabled.map((item) => `~${item.name}: ${item.value}`).join('\n'))}`;
+ bru += `\n${indentString(varsDisabled.map((item) => `~${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
}
if (varsLocalDisabled.length) {
- bru += `\n${indentString(varsLocalDisabled.map((item) => `~@${item.name}: ${item.value}`).join('\n'))}`;
+ bru += `\n${indentString(varsLocalDisabled.map((item) => `~@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
}
bru += '\n}\n\n';
@@ -694,7 +690,7 @@ ${indentString(body.sparql)}
if (enabled(assertions).length) {
bru += `\n${indentString(
enabled(assertions)
- .map((item) => `${item.name}: ${item.value}`)
+ .map((item) => `${item.name}: ${getValueString(item.value)}`)
.join('\n')
)}`;
}
@@ -702,7 +698,7 @@ ${indentString(body.sparql)}
if (disabled(assertions).length) {
bru += `\n${indentString(
disabled(assertions)
- .map((item) => `~${item.name}: ${item.value}`)
+ .map((item) => `~${getKeyString(item.name)}: ${getValueString(item.value)}`)
.join('\n')
)}`;
}
diff --git a/packages/bruno-lang/v2/src/jsonToCollectionBru.js b/packages/bruno-lang/v2/src/jsonToCollectionBru.js
index d5aa1c1e0..5016611c0 100644
--- a/packages/bruno-lang/v2/src/jsonToCollectionBru.js
+++ b/packages/bruno-lang/v2/src/jsonToCollectionBru.js
@@ -1,6 +1,6 @@
const _ = require('lodash');
-const { indentString } = require('./utils');
+const { indentString, getValueString, getKeyString } = require('./utils');
const enabled = (items = []) => items.filter((item) => item.enabled);
const disabled = (items = []) => items.filter((item) => !item.enabled);
@@ -30,7 +30,7 @@ const jsonToCollectionBru = (json) => {
if (enabled(query).length) {
bru += `\n${indentString(
enabled(query)
- .map((item) => `${item.name}: ${item.value}`)
+ .map((item) => `${getKeyString(item.name)}: ${getValueString(item.value)}`)
.join('\n')
)}`;
}
@@ -38,7 +38,7 @@ const jsonToCollectionBru = (json) => {
if (disabled(query).length) {
bru += `\n${indentString(
disabled(query)
- .map((item) => `~${item.name}: ${item.value}`)
+ .map((item) => `~${getKeyString(item.name)}: ${getValueString(item.value)}`)
.join('\n')
)}`;
}
@@ -51,7 +51,7 @@ const jsonToCollectionBru = (json) => {
if (enabled(headers).length) {
bru += `\n${indentString(
enabled(headers)
- .map((item) => `${item.name}: ${item.value}`)
+ .map((item) => `${getKeyString(item.name)}: ${getValueString(item.value)}`)
.join('\n')
)}`;
}
@@ -59,7 +59,7 @@ const jsonToCollectionBru = (json) => {
if (disabled(headers).length) {
bru += `\n${indentString(
disabled(headers)
- .map((item) => `~${item.name}: ${item.value}`)
+ .map((item) => `~${getKeyString(item.name)}: ${getValueString(item.value)}`)
.join('\n')
)}`;
}
@@ -243,7 +243,7 @@ ${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken ?? false).
${indentString(
authorizationHeaders
.filter(item => item?.name?.length)
- .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
+ .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
.join('\n')
)}
}
@@ -256,7 +256,7 @@ ${indentString(
${indentString(
authorizationQueryParams
.filter(item => item?.name?.length)
- .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
+ .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
.join('\n')
)}
}
@@ -269,7 +269,7 @@ ${indentString(
${indentString(
tokenHeaders
.filter(item => item?.name?.length)
- .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
+ .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
.join('\n')
)}
}
@@ -282,9 +282,8 @@ ${indentString(
${indentString(
tokenQueryParams
.filter(item => item?.name?.length)
- .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
- .join('\n')
- )}
+ .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
+ .join('\n'))}
}
`;
@@ -295,7 +294,7 @@ ${indentString(
${indentString(
tokenBodyValues
.filter(item => item?.name?.length)
- .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
+ .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
.join('\n')
)}
}
@@ -308,7 +307,7 @@ ${indentString(
${indentString(
refreshHeaders
.filter(item => item?.name?.length)
- .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
+ .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
.join('\n')
)}
}
@@ -321,7 +320,7 @@ ${indentString(
${indentString(
refreshQueryParams
.filter(item => item?.name?.length)
- .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
+ .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
.join('\n')
)}
}
@@ -334,7 +333,7 @@ ${indentString(
${indentString(
refreshBodyValues
.filter(item => item?.name?.length)
- .map((item) => `${item.enabled ? '' : '~'}${item.name}: ${item.value}`)
+ .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)
.join('\n')
)}
}
@@ -355,19 +354,19 @@ ${indentString(
bru += `vars:pre-request {`;
if (varsEnabled.length) {
- bru += `\n${indentString(varsEnabled.map((item) => `${item.name}: ${item.value}`).join('\n'))}`;
+ bru += `\n${indentString(varsEnabled.map((item) => `${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
}
if (varsLocalEnabled.length) {
- bru += `\n${indentString(varsLocalEnabled.map((item) => `@${item.name}: ${item.value}`).join('\n'))}`;
+ bru += `\n${indentString(varsLocalEnabled.map((item) => `@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
}
if (varsDisabled.length) {
- bru += `\n${indentString(varsDisabled.map((item) => `~${item.name}: ${item.value}`).join('\n'))}`;
+ bru += `\n${indentString(varsDisabled.map((item) => `~${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
}
if (varsLocalDisabled.length) {
- bru += `\n${indentString(varsLocalDisabled.map((item) => `~@${item.name}: ${item.value}`).join('\n'))}`;
+ bru += `\n${indentString(varsLocalDisabled.map((item) => `~@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
}
bru += '\n}\n\n';
@@ -381,19 +380,19 @@ ${indentString(
bru += `vars:post-response {`;
if (varsEnabled.length) {
- bru += `\n${indentString(varsEnabled.map((item) => `${item.name}: ${item.value}`).join('\n'))}`;
+ bru += `\n${indentString(varsEnabled.map((item) => `${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
}
if (varsLocalEnabled.length) {
- bru += `\n${indentString(varsLocalEnabled.map((item) => `@${item.name}: ${item.value}`).join('\n'))}`;
+ bru += `\n${indentString(varsLocalEnabled.map((item) => `@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
}
if (varsDisabled.length) {
- bru += `\n${indentString(varsDisabled.map((item) => `~${item.name}: ${item.value}`).join('\n'))}`;
+ bru += `\n${indentString(varsDisabled.map((item) => `~${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
}
if (varsLocalDisabled.length) {
- bru += `\n${indentString(varsLocalDisabled.map((item) => `~@${item.name}: ${item.value}`).join('\n'))}`;
+ bru += `\n${indentString(varsLocalDisabled.map((item) => `~@${item.name}: ${getValueString(item.value)}`).join('\n'))}`;
}
bru += '\n}\n\n';
diff --git a/packages/bruno-lang/v2/src/utils.js b/packages/bruno-lang/v2/src/utils.js
index 8dee48999..5b666cc68 100644
--- a/packages/bruno-lang/v2/src/utils.js
+++ b/packages/bruno-lang/v2/src/utils.js
@@ -8,14 +8,15 @@ const safeParseJson = (json) => {
};
-const indentString = (str) => {
+const indentString = (str, levels = 1) => {
if (!str || !str.length) {
return str || '';
}
+ const indent = ' '.repeat(levels);
return str
.split(/\r\n|\r|\n/)
- .map((line) => ' ' + line)
+ .map((line) => indent + line)
.join('\n');
};
@@ -37,7 +38,7 @@ const getValueString = (value) => {
return '';
}
- const hasNewLines = value?.includes('\n') || value?.includes('\r');
+ const hasNewLines = value.includes('\n') || value.includes('\r');
if (!hasNewLines) {
return value;
@@ -47,9 +48,32 @@ const getValueString = (value) => {
return `'''\n${indentString(value)}\n'''`;
};
+const getKeyString = (key) => {
+ const quotableChars = [':', '"', '{', '}', ' '];
+ return quotableChars.some((char) => key.includes(char)) ? ('"' + key.replaceAll('"', '\\"') + '"') : key;
+};
+
+const getValueUrl = (url) => {
+ // Handle null, undefined, and empty strings
+ if (!url) {
+ return '';
+ }
+
+ const hasNewLines = url.includes('\n') || url.includes('\r');
+
+ if (!hasNewLines) {
+ return url;
+ }
+
+ // Wrap multiline values in triple quotes with 4-space indentation (2 levels)
+ return `'''\n${indentString(url, 2)}\n'''`;
+};
+
module.exports = {
safeParseJson,
indentString,
outdentString,
- getValueString
+ getValueString,
+ getKeyString,
+ getValueUrl
};
diff --git a/packages/bruno-lang/v2/tests/bruToJson.spec.js b/packages/bruno-lang/v2/tests/bruToJson.spec.js
index aa94ccef1..6ad965771 100644
--- a/packages/bruno-lang/v2/tests/bruToJson.spec.js
+++ b/packages/bruno-lang/v2/tests/bruToJson.spec.js
@@ -38,4 +38,88 @@ settings {
expect(output).toEqual(expected);
});
});
+
+ describe('multi-line values', () => {
+ it('parses multi-line values in URL, headers, params, and vars', () => {
+ const input = `
+meta {
+ name: new-line
+ type: http
+ seq: 1
+}
+
+get {
+ url: '''
+ https://httpbin.io/anything?foo=hello
+ world
+'''
+ body: none
+ auth: oauth2
+}
+
+params:query {
+ foo: '''
+ hello
+ world
+ '''
+}
+
+headers {
+ "test header": '''
+ t1
+ t2
+ '''
+}
+
+vars:pre-request {
+ test-var: '''
+ t1
+ t2
+ '''
+}
+`;
+
+ const expected = {
+ meta: {
+ name: 'new-line',
+ type: 'http',
+ seq: '1'
+ },
+ http: {
+ method: 'get',
+ url: 'https://httpbin.io/anything?foo=hello\nworld',
+ body: 'none',
+ auth: 'oauth2'
+ },
+ params: [
+ {
+ name: 'foo',
+ value: 'hello\nworld',
+ enabled: true,
+ type: 'query'
+ }
+ ],
+ headers: [
+ {
+ name: 'test header',
+ value: 't1\nt2',
+ enabled: true
+ }
+ ],
+ vars: {
+ req: [
+ {
+ name: 'test-var',
+ value: 't1\nt2',
+ enabled: true,
+ local: false
+ }
+ ]
+ }
+ };
+
+ const output = parser(input);
+ expect(output).toEqual(expected);
+ });
+ });
});
diff --git a/packages/bruno-lang/v2/tests/getKeyString.spec.js b/packages/bruno-lang/v2/tests/getKeyString.spec.js
new file mode 100644
index 000000000..278a4226a
--- /dev/null
+++ b/packages/bruno-lang/v2/tests/getKeyString.spec.js
@@ -0,0 +1,56 @@
+const { getKeyString } = require('../src/utils');
+
+describe('getKeyString', () => {
+ describe('should not quote keys without special characters', () => {
+ it('should return simple alphanumeric keys as-is', () => {
+ expect(getKeyString('hello')).toBe('hello');
+ expect(getKeyString('world123')).toBe('world123');
+ expect(getKeyString('API')).toBe('API');
+ });
+
+ it('should return keys with hyphens as-is', () => {
+ expect(getKeyString('api-key')).toBe('api-key');
+ expect(getKeyString('content-type')).toBe('content-type');
+ });
+
+ it('should return keys with underscores as-is', () => {
+ expect(getKeyString('api_key')).toBe('api_key');
+ expect(getKeyString('user_name')).toBe('user_name');
+ });
+ });
+
+ describe('should quote keys with special characters', () => {
+ it('should quote keys with colons', () => {
+ expect(getKeyString('key:value')).toBe('"key:value"');
+ expect(getKeyString('disabled:colon:header')).toBe('"disabled:colon:header"');
+ expect(getKeyString(':startsWithColon')).toBe('":startsWithColon"');
+ expect(getKeyString('endsWithColon:')).toBe('"endsWithColon:"');
+ });
+
+ it('should quote keys with spaces', () => {
+ expect(getKeyString('key with spaces')).toBe('"key with spaces"');
+ expect(getKeyString(' leadingSpace')).toBe('" leadingSpace"');
+ expect(getKeyString('trailingSpace ')).toBe('"trailingSpace "');
+ expect(getKeyString('multiple spaces')).toBe('"multiple spaces"');
+ });
+
+ it('should quote keys with curly braces', () => {
+ expect(getKeyString('{braces}')).toBe('"{braces}"');
+ expect(getKeyString('{only-open')).toBe('"{only-open"');
+ expect(getKeyString('only-close}')).toBe('"only-close}"');
+ expect(getKeyString('nested{brace}here')).toBe('"nested{brace}here"');
+ });
+
+ it('should quote keys with double quotes and escape them', () => {
+ expect(getKeyString('nested "quote"')).toBe('"nested \\"quote\\""');
+ expect(getKeyString('"quoted"')).toBe('"\\"quoted\\""');
+ expect(getKeyString('multiple "quotes" here "too"')).toBe('"multiple \\"quotes\\" here \\"too\\""');
+ });
+
+ it('should quote keys with multiple special characters', () => {
+ expect(getKeyString('key: value')).toBe('"key: value"');
+ expect(getKeyString('{key}: "value"')).toBe('"{key}: \\"value\\""');
+ expect(getKeyString('complex:key with {braces}')).toBe('"complex:key with {braces}"');
+ });
+ });
+});
diff --git a/packages/bruno-lang/v2/tests/jsonToBru.spec.js b/packages/bruno-lang/v2/tests/jsonToBru.spec.js
index 56e8ea059..059146469 100644
--- a/packages/bruno-lang/v2/tests/jsonToBru.spec.js
+++ b/packages/bruno-lang/v2/tests/jsonToBru.spec.js
@@ -53,4 +53,87 @@ describe('jsonToBru stringify', () => {
expect(output).toMatch(new RegExp(`timeout: ${input.settings.timeout}`));
});
});
+
+ describe('multi-line values', () => {
+ it('handles multi-line values in URL, headers, params, and vars', () => {
+ const input = {
+ meta: {
+ name: 'new-line',
+ type: 'http',
+ seq: 1
+ },
+ http: {
+ method: 'get',
+ url: 'https://httpbin.io/anything?foo=hello\nworld',
+ body: 'none',
+ auth: 'oauth2'
+ },
+ params: [
+ {
+ name: 'foo',
+ value: 'hello\nworld',
+ enabled: true,
+ type: 'query'
+ }
+ ],
+ headers: [
+ {
+ name: 'test header',
+ value: 't1\nt2',
+ enabled: true
+ }
+ ],
+ vars: {
+ req: [
+ {
+ name: 'test-var',
+ value: 't1\nt2',
+ enabled: true
+ }
+ ]
+ }
+ };
+
+ const output = stringify(input);
+
+ expect(output).toMatchInlineSnapshot(`
+ "meta {
+ name: new-line
+ type: http
+ seq: 1
+ }
+
+ get {
+ url: '''
+ https://httpbin.io/anything?foo=hello
+ world
+ '''
+ body: none
+ auth: oauth2
+ }
+
+ params:query {
+ foo: '''
+ hello
+ world
+ '''
+ }
+
+ headers {
+ "test header": '''
+ t1
+ t2
+ '''
+ }
+
+ vars:pre-request {
+ test-var: '''
+ t1
+ t2
+ '''
+ }
+ "
+ `);
+ });
+ });
});
diff --git a/tests/request/newlines/newlines-persistence.spec.ts b/tests/request/newlines/newlines-persistence.spec.ts
new file mode 100644
index 000000000..ac8f2ecb8
--- /dev/null
+++ b/tests/request/newlines/newlines-persistence.spec.ts
@@ -0,0 +1,87 @@
+import { test, expect } from '../../../playwright';
+import { openCollectionAndAcceptSandbox } from '../../utils/page/actions';
+import { getTableCell } from '../../utils/page/locators';
+
+test('should persist request with newlines across app restarts', async ({ createTmpDir, launchElectronApp }) => {
+ const userDataPath = await createTmpDir('newlines-persistence-userdata');
+ const collectionPath = await createTmpDir('newlines-persistence-collection');
+
+ // Create collection and request
+ const app1 = await launchElectronApp({ userDataPath });
+ const page = await app1.firstWindow();
+
+ await page.locator('.dropdown-icon').click();
+ await page.locator('.dropdown-item').filter({ hasText: 'Create Collection' }).click();
+ await page.getByLabel('Name').fill('newlines-persistence');
+ await page.getByLabel('Location').fill(collectionPath);
+ await page.getByRole('button', { name: 'Create', exact: true }).click();
+
+ const collection = page.locator('.collection-name').filter({ hasText: 'newlines-persistence' });
+ await collection.locator('.collection-actions').hover();
+ await collection.locator('.collection-actions .icon').click();
+ await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();
+ await page.getByPlaceholder('Request Name').fill('persistence-test');
+ await page.locator('#new-request-url').locator('.CodeMirror').click();
+ await page.locator('#new-request-url').locator('textarea').fill('https://httpbin.org/get');
+ await page.getByRole('button', { name: 'Create', exact: true }).click();
+
+ await openCollectionAndAcceptSandbox(page, 'newlines-persistence', 'safe');
+ await page.locator('.collection-item-name').filter({ hasText: 'persistence-test' }).dblclick();
+
+ // Add query param
+ await page.getByRole('tab', { name: 'Params' }).click();
+ await page.getByRole('button', { name: /Add.*Param/i }).click();
+
+ const paramRow = page.locator('table tbody tr').last();
+ await getTableCell(paramRow, 0).locator('input[type="text"]').fill('queryParamKey');
+
+ // Add header with newlines
+ await page.getByRole('tab', { name: 'Headers' }).click();
+ await page.getByRole('button', { name: /Add.*Header/i }).click();
+
+ const headerRow = page.locator('table tbody tr').last();
+ await getTableCell(headerRow, 0).locator('.CodeMirror').click();
+ await getTableCell(headerRow, 0).locator('textarea').fill('headerKey');
+ await getTableCell(headerRow, 1).locator('.CodeMirror').click();
+ await getTableCell(headerRow, 1).locator('textarea').fill('header\nValue');
+
+ // Add Pre Request var with newlines
+ await page.getByRole('tab', { name: 'Vars' }).click();
+ await page.locator('.btn-add-var').first().click();
+ const preReqRow = page.locator('table').first().locator('tbody tr').first();
+ await getTableCell(preReqRow, 0).locator('input[type="text"]').fill('preRequestVar');
+ await getTableCell(preReqRow, 1).locator('.CodeMirror').click();
+ await getTableCell(preReqRow, 1).locator('textarea').fill('pre\nRequest\nValue');
+
+ // Add Post Response var with newlines
+ await page.locator('.btn-add-var').last().click();
+ const postResRow = page.locator('table').nth(1).locator('tbody tr').first();
+ await getTableCell(postResRow, 0).locator('input[type="text"]').fill('postResponseVar');
+ await getTableCell(postResRow, 1).locator('.CodeMirror').click();
+ await getTableCell(postResRow, 1).locator('textarea').fill('post\nResponse\nValue');
+
+ await page.keyboard.press('Meta+s');
+ await app1.close();
+
+ // Verify persistence after restart
+ const app2 = await launchElectronApp({ userDataPath });
+ const page2 = await app2.firstWindow();
+
+ await page2.locator('.collection-name').filter({ hasText: 'newlines-persistence' }).click();
+ await page2.locator('.collection-item-name').filter({ hasText: 'persistence-test' }).dblclick();
+
+ // Verify params persisted
+ await page2.getByRole('tab', { name: 'Params' }).click();
+ await expect(page2.locator('table tbody tr')).toHaveCount(1);
+
+ // Verify headers persisted
+ await page2.getByRole('tab', { name: 'Headers' }).click();
+ await expect(page2.locator('table tbody tr')).toHaveCount(1);
+
+ // Verify vars persisted
+ await page2.getByRole('tab', { name: 'Vars' }).click();
+ await expect(page2.locator('table').first().locator('tbody tr')).toHaveCount(1);
+ await expect(page2.locator('table').nth(1).locator('tbody tr')).toHaveCount(1);
+
+ await app2.close();
+});
diff --git a/tests/utils/page/locators.ts b/tests/utils/page/locators.ts
index 85f383e53..dde7bf971 100644
--- a/tests/utils/page/locators.ts
+++ b/tests/utils/page/locators.ts
@@ -72,6 +72,8 @@ export const buildWebsocketCommonLocators = (page: Page) => ({
}
});
+export const getTableCell = (row, index) => row.locator('td').nth(index);
+
export const buildGrpcCommonLocators = (page: Page) => ({
...buildCommonLocators(page),
method: {
|