fix: support multiline descriptions in example blocks (#6879)

* fix: support multiline descriptions in example blocks

* refactor: use outdentString for example multiline text block parsing

* test: add test case for examples without description field

* test: add jsonToBru conversion test for multiline descriptions

* refactor: generalize descriptionvalue to textvalue in example grammar
This commit is contained in:
gopu-bruno
2026-01-30 23:04:48 +05:30
committed by GitHub
parent f10422cca6
commit 3ddf8e2a8b
6 changed files with 152 additions and 7 deletions

View File

@@ -27,7 +27,7 @@ const ExampleItem = ({ example, item, collection }) => {
// Check if this example is the active tab
const activeTabUid = useSelector((state) => state.tabs?.activeTabUid);
const isExampleActive = activeTabUid === example.uid;
const [editName, setEditName] = useState(example.name);
const [editName, setEditName] = useState(example.name || '');
const [showRenameModal, setShowRenameModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [generateCodeItemModalOpen, setGenerateCodeItemModalOpen] = useState(false);
@@ -230,7 +230,7 @@ const ExampleItem = ({ example, item, collection }) => {
handleConfirm={() => handleRenameConfirm(editName)}
confirmText="Rename"
cancelText="Cancel"
confirmDisabled={!editName.trim()}
confirmDisabled={!editName || !editName.trim()}
>
<div>
<label htmlFor="renameExampleName" className="block font-medium">

View File

@@ -12,6 +12,7 @@ const astBaseAttribute = require('../common/attributes');
* No meta block - everything is at root level: name, description, type, url, etc.
* Supports all body types from request side but response body stays as simple text.
*/
const exampleGrammar = ohm.grammar(`Example {
ExampleFile = (name | description | request | response)*
@@ -41,20 +42,21 @@ const exampleGrammar = ohm.grammar(`Example {
key = keychar*
value = list | multilinetextblock | singlelinevalue
singlelinevalue = valuechar*
// List
list = st* "[" nl+ listitems? st* nl+ st* "]"
listitems = listitem (nl+ listitem)*
listitem = st+ (alnum | "_" | "-")+ st*
// Text Blocks
textblock = textline (~tagend nl textline)*
textline = textchar*
textchar = ~nl any
textvalue = multilinetextblock | singlelinevalue
// Root level properties
name = "name" st* ":" st* valuechar* st*
description = "description" st* ":" st* valuechar* st*
description = "description" st* ":" st* textvalue st*
// Request block
request = nl* "request" st* ":" st* "{" nl* requestcontent+ nl* "}" nl*
@@ -85,9 +87,20 @@ const astExampleAttribute = {
},
description(_1, _2, _3, _4, value, _6) {
return {
description: value.sourceString ? value.sourceString.trim() : ''
description: value.ast ? value.ast.trim() : ''
};
},
textvalue(content) {
return content.ast;
},
multilinetextblock(_1, content, _2, _3, contentType) {
const multilineString = outdentString(content.sourceString);
if (!contentType.sourceString) {
return multilineString;
}
return `${multilineString} ${contentType.sourceString}`;
},
request(_1, _2, _3, _4, _5, _6, _7, requestcontent, _8, _9, _10) {
if (!requestcontent || !requestcontent.ast || !requestcontent.ast.length) {
return {};

View File

@@ -38,7 +38,8 @@ const jsonToExampleBru = (json) => {
}
if (description) {
bru += `description: ${description}\n`;
const descriptionValue = getValueString(description);
bru += `description: ${descriptionValue}\n`;
}
// Request block

View File

@@ -243,6 +243,51 @@ example {
expect(output).toEqual(expected);
});
it('should handle examples without description', () => {
const jsonInput = {
meta: {
name: 'Test API',
type: 'http'
},
http: {
url: 'https://api.example.com/test',
method: 'get'
},
examples: [
{
name: 'Example Request',
request: {
url: 'https://api.example.com/example',
method: 'get'
}
}
]
};
const expected = `meta {
name: Test API
type: http
}
get {
url: https://api.example.com/test
}
example {
name: Example Request
request: {
url: https://api.example.com/example
method: get
}
}
`;
const output = jsonToBru(jsonInput);
expect(output).toEqual(expected);
});
});
describe('Complex examples with auth', () => {
@@ -309,6 +354,48 @@ example {
});
});
describe('Examples with multiline descriptions', () => {
it('should parse examples with multiline descriptions', () => {
const input = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'examples-multiline-description.bru'), 'utf8');
const expected = require('./fixtures/json/examples-multiline-description.json');
const output = bruToJson(input);
expect(output).toEqual(expected);
});
it('should convert examples with multiline descriptions to BRU format', () => {
const jsonInput = require('./fixtures/json/examples-multiline-description.json');
const expected = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'examples-multiline-description.bru'), 'utf8');
const output = jsonToBru(jsonInput);
expect(output).toEqual(expected);
});
it('should parse example without description field', () => {
const bruInput = `meta {
name: Test API
type: http
}
example {
name: Example Request
request: {
url: https://api.example.com/example
method: get
}
}
`;
const output = bruToJson(bruInput);
expect(output.examples).toBeDefined();
expect(output.examples).toHaveLength(1);
expect(output.examples[0].name).toBe('Example Request');
expect(output.examples[0].description).toBeUndefined();
});
});
describe('Examples with multiline strings and contentType', () => {
it('should parse examples with multiline strings and @contentType annotations', () => {
const input = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'examples-multiline-contenttype.bru'), 'utf8');

View File

@@ -0,0 +1,23 @@
meta {
name: Multiline Description Test
type: http
seq: 1
}
get {
url: https://api.example.com/test
}
example {
name: Test Example
description: '''
This is a multiline description.
It spans multiple lines.
And should be parsed correctly.
'''
request: {
url: https://api.example.com/test
method: get
}
}

View File

@@ -0,0 +1,21 @@
{
"meta": {
"name": "Multiline Description Test",
"type": "http",
"seq": "1"
},
"http": {
"method": "get",
"url": "https://api.example.com/test"
},
"examples": [
{
"name": "Test Example",
"description": "This is a multiline description.\nIt spans multiple lines.\nAnd should be parsed correctly.",
"request": {
"url": "https://api.example.com/test",
"method": "get"
}
}
]
}