Files
DocumentServer-v-9.2.0/sdkjs/tests/common/charts/chartEx-serialize.js
Yajbir Singh f1b860b25c
Some checks failed
check / markdownlint (push) Has been cancelled
check / spellchecker (push) Has been cancelled
updated
2025-12-11 19:03:17 +05:30

661 lines
25 KiB
JavaScript

/*
* (c) Copyright Ascensio System SIA 2010-2023
*
* This program is a free software product. You can redistribute it and/or
* modify it under the terms of the GNU Affero General Public License (AGPL)
* version 3 as published by the Free Software Foundation. In accordance with
* Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect
* that Ascensio System SIA expressly excludes the warranty of non-infringement
* of any third-party rights.
*
* This program is distributed WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For
* details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
*
* You can contact Ascensio System SIA at 20A-6 Ernesta Birznieka-Upish
* street, Riga, Latvia, EU, LV-1050.
*
* The interactive user interfaces in modified source and object code versions
* of the Program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU AGPL version 3.
*
* Pursuant to Section 7(b) of the License you must retain the original Product
* logo when distributing the program. Pursuant to Section 7(e) we decline to
* grant you any rights under trademark law for use of our trademarks.
*
* All the Product's GUI elements, including illustrations and icon sets, as
* well as technical writing content are licensed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International. See the License
* terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
*
*/
QUnit.config.autostart = false;
$(function() {
const api = new Asc.asc_docs_api({
'id-view': 'editor_sdk'
});
AscCommon.g_oTableId.init();
let memory = new AscCommon.CMemory();
// memory.SetXmlAttributeQuote(0x27);
testFileSerizlizeUseParseCompare.skip = function testFileSkip(fileName, base64, ignoreFolders, ignoreFiles, ignoredTags, ignoredAttributes, downloadFile) {
this(fileName, base64, ignoreFolders, ignoreFiles, ignoredTags, ignoredAttributes, downloadFile, true);
}
// If test fails with error
// TypeError: Cannot read properties of null (reading 'toXml')
// then something with api saveDocumentToZip
// try to reload sax-serialize page and select one module
//todo events
setTimeout(startTests, 3000);
function startTests() {
api.InitEditor();
QUnit.start();
QUnit.module("Test main functions");
// QUnit.test("Test api.OpenDocumentFromZip", function (assert)
// {
// const api = new Asc.asc_docs_api({'id-view': 'editor_sdk'});
// api.InitEditor();
// // get Uint8Array
// let vsdx = AscCommon.Base64.decode(Asc.exampleVsdx);
// const openRes = api.OpenDocumentFromZip(vsdx);
// assert.strictEqual(openRes, true, "Check OpenDocumentFromZip");
// });
// QUnit.test("Check api.saveDocumentToZip", function (assert)
// {
// // Read and parse vsdx file
// const api = new Asc.asc_docs_api({'id-view': 'editor_sdk'});
// api.InitEditor();
// let vsdx = AscCommon.Base64.decode(Asc.exampleVsdx);
// const openRes = api.OpenDocumentFromZip(vsdx);
// // Creating .vsdx from api and get Uint8Array to data variable
// api.saveDocumentToZip(api.Document, AscCommon.c_oEditorId.Visio, function (data) {
// if (data) {
// assert.strictEqual(Boolean(data), true, "saveDocumentToZip returned data");
// }
// });
// });
// QUnit.test("Compare files count", function (assert)
// {
// // Read and parse vsdx file
// const api = new Asc.asc_docs_api({'id-view': 'editor_sdk'});
// api.InitEditor();
// let vsdx = AscCommon.Base64.decode(Asc.exampleVsdx);
// const openRes = api.OpenDocumentFromZip(vsdx);
// // Creating .vsdx and get Uint8Array to data variable
// api.saveDocumentToZip(api.Document, AscCommon.c_oEditorId.Visio, function (data) {
// if (data) {
// // Read and parse vsdx file
// const api2 = new Asc.asc_docs_api({'id-view': 'editor_sdk'});
// api2.InitEditor();
// let jsZlib = new AscCommon.ZLib();
// jsZlib.open(data);
// assert.strictEqual(jsZlib.files.length, 23, "Parsed vsdx contains 23 xml files like initial vsdx. Files Count correctly.");
// const openRes2 = api2.OpenDocumentFromZip(data);
// } else {
// return false;
// }
// });
// });
// File contents (strings) before and after parse are the same but
// Exceptions:
// comments, schema tags - schema namespace, schema location,
// hidden characters: /n/t,
// quotes: double or single,
// xml declaration: UTF and utf,
// attributes order (order set like in MS but in generated file with XMLSpy and Liquid studio they are different),
// ignoring long fractional numbers eg 3.1415926535897900073734945 cuts to 3.14159265358979,
// tag auto-generated_for_wildcard,
// files SolutionXML, SolutionsXML, Validation
// and attributes and tags with any type
// in:
// <Cell N='AutoGen' U='string' E='string' F='string' V='string'>
// <RefBy T='string' ID='4730' />
// <RefBy T='string' ID='6554' />
// <RefBy T='string' ID='6015' />
// </Cell>
// line drop doesn't exist in this situation in real file. And Cell handles only one line drop
// because it is considered as text content.
// Only .xml check no .rels check or embeddings
// let ignoreFiles = ["to prepare file check.txt", "theme1.xml"];
let ignoreQuotes = true;
QUnit.module("Test xml serialize use strings compare Altova generated");
testXmlFilesArchieve(Asc.document1Base64, ignoreQuotes);
// QUnit.module("Test xml serialize use strings compare real file");
// testXmlFilesArchieve(Asc.timelineDiagrammVisioSpecificXmlOnly, ignoreFiles, ignoreQuotes);
// QUnit.module("Test file serialize use parse compare");
// let ignoredFolders = ["docProps"];
// ignoredFolders = ignoredFolders.concat(["media", "embeddings"]);
// let ignoredFiles = ["theme[N].xml", "page[N].xml.rels", "recordset[N].xml", "recordsets.xml.rels", "master[N].xml.rels"];
// let ignoredTagsExistence = ["Shapes"];
// let ignoredAttributes = ["xsi:schemaLocation", "xmlns:xsi", "xmlns", "xmlns:r", "xml:space"];
// testFileSerizlizeUseParseCompare("Example vsdx", Asc.exampleVsdx, ignoredFolders, ignoredFiles, ignoredTagsExistence, ignoredAttributes, false);
// testFileSerizlizeUseParseCompare("Basic ShapesA_start", Asc.BasicShapesA_start, ignoredFolders, ignoredFiles, ignoredTagsExistence, ignoredAttributes, false);
// testFileSerizlizeUseParseCompare("generatedVsdx2schema", Asc.generatedVsdx2schema, ignoredFolders, ignoredFiles, ignoredTagsExistence, ignoredAttributes, false);
// testFileSerizlizeUseParseCompare("Timeline_diagram_start", Asc.Timeline_diagram_start, ignoredFolders, ignoredFiles, ignoredTagsExistence, ignoredAttributes, false);
// testFileSerizlizeUseParseCompare("rows_test", Asc.rows_test, ignoredFolders, ignoredFiles, ignoredTagsExistence, ignoredAttributes, false);
// // Some errors still there in module below
// QUnit.module("Comparing many files use parse compare");
// for (let key in Asc) {
// if (key.startsWith('test_file')) {
// let fileBase64 = Asc[key];
// testFileSerizlizeUseParseCompare(key, fileBase64, ignoredFolders, ignoredFiles, ignoredTagsExistence, ignoredAttributes, false);
// }
// }
}
function testXmlFilesArchieve(base64zip, ignoreQuotes) {
let generatedXmlFiles = AscCommon.Base64.decode(base64zip);
let doRemoveLineBreaks = false;
let fileContent = AscCommon.UTF8ArrayToString(generatedXmlFiles, 0, generatedXmlFiles.length);
let testName = "Compare " + "1xml";
let rootTagNameMatchResult = fileContent.match(/<(\w+:)?(\w+)/);
if (rootTagNameMatchResult) {
let rootTagName = rootTagNameMatchResult[2];
let rootTagNameCapitalized = rootTagName.charAt(0).toUpperCase() + rootTagName.slice(1);
let componentClassName = "C" + rootTagNameCapitalized;
let componentClass = AscFormat[componentClassName];
if (componentClass) {
let componentObject = new componentClass(api);
QUnit.test(testName, function (assert) {
testFileSerializeUseStringsCompare(assert, componentObject, fileContent, ignoreQuotes, doRemoveLineBreaks);
});
} else {
QUnit.test(testName, function (assert)
{
QUnit.assert.ok(componentClass, "Class for parse not found");
});
}
} else {
QUnit.test(testName, function (assert)
{
QUnit.assert.ok(false, "rootTagNameMatchResult is false");
});
}
}
function testFileSerizlizeUseParseCompare(fileName, base64, ignoreFolders, ignoreFiles, ignoredTagsExistence, ignoredAttributes, downloadFile, skip) {
let testFunction = skip ? QUnit.test.skip : QUnit.test;
testFunction('File ' + fileName, function (assert)
{
// Read and parse vsdx file
const api = new Asc.asc_docs_api({'id-view': 'editor_sdk'});
api.InitEditor();
let vsdxOriginal = AscCommon.Base64.decode(base64);
// parse
const openResOriginal = api.OpenDocumentFromZip(vsdxOriginal);
let jsZlibOriginal = new AscCommon.ZLib();
jsZlibOriginal.open(vsdxOriginal);
let originalFiles = jsZlibOriginal.files;
// serialize
api.saveDocumentToZip(api.Document, AscCommon.c_oEditorId.Visio, function (data) {
if (data) {
if(downloadFile) {
AscCommon.DownloadFileFromBytes(data, fileName, AscCommon.openXml.GetMimeType("vsdx"));
}
// Read and parse custom vsdx file
// TODO try to parse again custom file
// const api2 = new Asc.asc_docs_api({'id-view': 'editor_sdk'});
// api2.InitEditor();
// const openResCustom = api2.OpenDocumentFromZip(data);
let jsZlibCustom = new AscCommon.ZLib();
jsZlibCustom.open(data);
let customFiles = jsZlibCustom.files;
let exceptionsMessage = format('Ignoring:\nFolders: %s\nFiles: %s\nTags: %s\nAttributes: %s',
ignoreFolders.join(', '), ignoreFiles.join(', '), ignoredTagsExistence.join(', '), ignoredAttributes.join(', '));
// \n doesnt work in success assert to split
exceptionsMessage.split('\n').forEach(function(line) { return assert.ok(true, line);})
originalFiles = originalFiles.filter(function (path) {
let folderIgnored = pathCheckFolderPresence(path, ignoreFolders);
let fileIgnored = isFilePathIgnored(path, ignoreFiles);
return !folderIgnored && !fileIgnored;
});
customFiles = customFiles.filter(function (path) {
let folderIgnored = pathCheckFolderPresence(path, ignoreFolders);
let fileIgnored = isFilePathIgnored(path, ignoreFiles);
return !folderIgnored && !fileIgnored;
});
assert.strictEqual(customFiles.length, originalFiles.length, "Parsed vsdx contains as many xml files as initial vsdx");
originalFiles = originalFiles.sort( function(a, b) { return a.localeCompare(b);});
customFiles = customFiles.sort( function(a, b) { return a.localeCompare(b);});
assert.deepEqual(customFiles, originalFiles, 'Original vsdx has the same file structire as custom vsdx');
let docOriginal = new AscCommon.openXml.OpenXmlPackage(jsZlibOriginal, null);
let docCustom = new AscCommon.openXml.OpenXmlPackage(jsZlibCustom, null);
for (let i = 0; i < originalFiles.length; i++) {
let path = originalFiles[i];
if (originalFiles[i].includes('/')) {
path = "/" + path;
}
let contentOriginal = docOriginal.getPartByUri(path).getDocumentContent();
let contentCustom;
try {
contentCustom = docCustom.getPartByUri(path).getDocumentContent();
} catch (exception) {
// check if TypeError
if (exception.name === "TypeError") {
assert.strictEqual("Cant read file", "File read", format(
"Checking %s failed. Cant read custom file, check files count and file structure checks.",
path));
continue;
}
}
contentOriginal = contentOriginal.trim();
contentCustom = contentCustom.trim();
// global js API DOMParser
const domParser = new DOMParser();
let fileDomOriginal = domParser.parseFromString(contentOriginal, "application/xml");
let fileDomCustom = domParser.parseFromString(contentCustom, "application/xml");
let compareResult = compareDOMs(fileDomOriginal, fileDomCustom);
let compareResultIgnoredTags = getCompareResultIgnoredTagsExistence(compareResult, ignoredTagsExistence);
let compareResultIgnoredAttributes = getCompareResultIgnoredAttributes(compareResultIgnoredTags, ignoredAttributes);
let differences = compareResultIgnoredAttributes.filter(function (compareObject) {
let attrDifs = compareObject.differencesInAttributes;
let wrongAttributes = attrDifs.wrongValuesAttributePairs.length ||
attrDifs.extraAttributes.length || attrDifs.missingAttributes.length;
return compareObject.missingElements.length || compareObject.extraElements.length || wrongAttributes;
});
let message = '';
if (differences.length === 0) {
message = format('Checking %s was successful. Files are equal.', path);
} else {
let differencesString = differencesToString(differences);
message = format('Checking %s was not successful.\nDifferences:\n%s', path, differencesString);
}
assert.strictEqual(differences.length, 0, message);
}
} else {
return false;
}
});
});
}
function addLineBreaks(xmlString) {
// bad line breaks
for (let i = 0; i < 2; i++) {
xmlString = xmlString.replaceAll(/(<\w.+?)<(\w)/g, '$1\n<$2');
}
xmlString = xmlString.replaceAll('></', '>\n</');
return xmlString;
}
function removeLineBreaks(string) {
string = string.replaceAll(/[\r\n]+/g, '');
string = addLineBreaks(string);
return string;
}
function findFirstDifference(str1, str2) {
const minLength = Math.min(str1.length, str2.length);
for (let i = 0; i < minLength; i++) {
if (str1[i] !== str2[i]) {
return `Difference found at position ${i}: char ${str1.charCodeAt(i)} !== char ${str2.charCodeAt(i)}`;
}
}
if (str1.length !== str2.length) {
const longerStr = str1.length > str2.length ? str1 : str2;
return `Strings have different lengths. First difference found at position ${minLength}: ${longerStr[minLength]}`;
}
return "No difference found";
}
function testFileSerializeUseStringsCompare(assert, serializeObj, expecteedXml, ignoreQuotes, doRemoveLineBreaks) {
//fromXml
let context = new AscCommon.XmlParserContext();
let zip = new AscCommon.ZLib();
let rels = new AscCommon.openXml.OpenXmlPackage(zip, null);
let reader = new StaxParser(expecteedXml, rels, context);
serializeObj.fromXml(reader);
//toXml
memory.Seek(0);
memory.context = new AscCommon.XmlWriterContext();
memory.context.clearCurrentPartDataMaps();
context.document = serializeObj;
let filePart = new AscCommon.openXml.OpenXmlPackage(zip, memory);
let data = filePart.getXmlBytes(this, serializeObj, memory);
let content = AscCommon.UTF8ArrayToString(data, 0, data.length);
//compare
let expectedContent = expecteedXml.replace(/\t/g,'');
let resultContent = content;
//todo flag in memeory?
resultContent = resultContent.replace(/&quot;/g,'"');
if (ignoreQuotes) {
resultContent = resultContent.replaceAll("\"", "'");
expectedContent = expectedContent.replaceAll("\"", "'");
}
if (doRemoveLineBreaks) {
resultContent = removeLineBreaks(resultContent);
resultContent = addLineBreaks(resultContent);
expectedContent = removeLineBreaks(expectedContent);
expectedContent = addLineBreaks(expectedContent);
}
let message = `Comparing ${serializeObj.constructor.name} xml stings. ${findFirstDifference(resultContent, expectedContent)}`;
assert.strictEqual(resultContent, expectedContent, message);
}
function getCompareResultIgnoredTagsExistence(compareResult, ignoredTags) {
return compareResult.map(function (compareObject) {
let newMissingElements = compareObject.missingElements.filter(function (missingElement) {
return !ignoredTags.includes(missingElement.nodeName);
});
let newExtraElements = compareObject.extraElements.filter(function (extraElement) {
return !ignoredTags.includes(extraElement.nodeName);
});
return {
tagsCompared: compareObject.tagsCompared,
missingElements: newMissingElements,
equalElements: compareObject.equalElements,
extraElements: newExtraElements,
differencesInAttributes : compareObject.differencesInAttributes
}
});
}
function getCompareResultIgnoredAttributes(compareResult, ignoredAttributes) {
return compareResult.map(function (compareObject) {
let attrDifs = compareObject.differencesInAttributes;
let newMissingAttributes = attrDifs.missingAttributes.filter(function (missingAttribute) {
return !ignoredAttributes.includes(missingAttribute.name);
});
attrDifs.missingAttributes = newMissingAttributes;
let newExtraAttributes = attrDifs.extraAttributes.filter(function (extraAttribute) {
return !ignoredAttributes.includes(extraAttribute.name);
});
attrDifs.extraAttributes = newExtraAttributes;
let newWrongValuesAttributePairs = attrDifs.wrongValuesAttributePairs.filter(function (wrongValuesAttributePair) {
return !ignoredAttributes.includes(wrongValuesAttributePair[1].name); //[1] is equal to [0] here
});
attrDifs.wrongValuesAttributePairs = newWrongValuesAttributePairs;
return {
tagsCompared: compareObject.tagsCompared,
missingElements: compareObject.missingElements,
equalElements: compareObject.equalElements,
extraElements: compareObject.extraElements,
differencesInAttributes : compareObject.differencesInAttributes // changed implicitly
}
});
}
function isFilePathIgnored(path, ignoreFiles) {
let fileName = path.split('/').pop();
let matchedAny = false;
ignoreFiles.forEach(function checkPattern(pattern) {
const regexPattern = pattern.replace('[N]', '\\d+');
const regex = new RegExp(`^${regexPattern}$`, 'i');
matchedAny = matchedAny || regex.test(fileName);
});
return matchedAny;
}
function pathCheckFolderPresence(path, folders) {
let foldersUsed = path.split('/').slice(0, -1);
let isIgnored = foldersUsed.some(function (folder) {
return folders.includes(folder);
})
return isIgnored;
}
function format () {
var args = [].slice.call(arguments);
var initial = args.shift();
function replacer (text, replacement) {
return text.replace('%s', replacement);
}
return args.reduce(replacer, initial);
}
function compareDOMs(originalNode, customNode, result) {
if (typeof result === 'undefined') {
result = [];
}
if (originalNode.children.length !== 0) { // Check if it's an element node
let originalElement = originalNode;
let customElement = customNode;
let differencesInChildren = compareTagsFlat(originalElement, customElement);
let differencesInAttributes = compareAttributes(originalElement, customElement);
result.push({
tagsCompared : originalNode.nodeName,
missingElements : differencesInChildren.missingElements,
equalElements : differencesInChildren.equalElements,
extraElements : differencesInChildren.extraElements,
differencesInAttributes : differencesInAttributes
});
let equalElements = differencesInChildren.equalElements;
// recursive cals
for (let i = 0; i < equalElements.length; i++) {
result = result.concat(compareDOMs(equalElements[i][0], equalElements[i][1]));
}
} else {
// let differencesInValues = {
// originalValue: originalNode.value,
// customValue: customNode.value,
// compareResult: originalNode.value === customNode.value
// }
// ...
}
return result;
}
function compareAttributes(originalElement, customElement) {
let originalAttributes;
let customAttributes;
if (originalElement.attributes) {
originalAttributes = Array.from(originalElement.attributes);
originalAttributes.sort(function (a, b) {return a.name.localeCompare(b);})
} else {
originalAttributes = [];
}
if (customElement.attributes) {
customAttributes = Array.from(customElement.attributes);
customAttributes.sort(function (a, b) {return a.name.localeCompare(b);})
} else {
customAttributes = [];
}
const missingAttributes = [];
const extraAttributes = [];
const wrongValuesAttributePairs = [];
const correctAttributePairs = [];
// Iterate through the children of originalElement and check if they are missing in customElement
originalAttributes.forEach(function (originalAttribute) {
const foundIndex = customAttributes.findIndex(function(customAttribute) {
return customAttribute.name === originalAttribute.name;
});
if (foundIndex === -1) {
missingAttributes.push(originalAttribute);
} else {
// Remove the found child from customElement to handle duplicates
const removedChild = customAttributes.splice(foundIndex, 1)[0];
if (originalAttribute.value === removedChild.value) {
correctAttributePairs.push([originalAttribute, removedChild]);
} else {
wrongValuesAttributePairs.push([originalAttribute, removedChild]);
}
}
});
// Find extra elements present in customElement but not in originalElement
// may be some() check can be omitted
customAttributes.forEach(function (customAttribute) {
if (!originalAttributes.some(function (originalAttribute) {return originalAttribute.name === customAttribute.name;})) {
extraAttributes.push(customAttribute);
}
});
return {
missingAttributes,
extraAttributes,
wrongValuesAttributePairs,
correctAttributePairs
};
}
function compareTagsFlat(originalElement, customElement) {
// indifferent of children.length there may be extra nodes missing nodes and nodes with equal nodeNames
// eg
// Orig Custom Orig Custom
// A D D A A C
// B E E B B D
// C C
// Mis ABC Extra DE Mis DE Extra ABC Mis AB Extra CD
// A D D A A A
// B B C B B B
// C C
// Mis AC Extra D Equ B Mis D Extra AB Equ C Equ AB
// B B
// B
// Mis B (second) Extra - Equ B (first). So in that case we recursively comprate two first B
// TODO: Not checking each B values or attributes. This is a flaw.
// Bcs if 2-nd B in original and 1-st B in custom are absolutely equal then original B 1-rst is missing
// But result is 2-nd B in orig is missing and orig 1-st B is not equal to custom 1-st B
let originalElementChildren = Array.from(originalElement.children);
let customElementChildren = Array.from(customElement.children);
const missingElements = [];
const equalElements = [];
const extraElements = [];
// Iterate through the children of originalElement and check if they are missing in customElement
originalElementChildren.forEach(function (originalChild) {
const foundIndex = customElementChildren.findIndex(function(customChild) {
return customChild.nodeName === originalChild.nodeName;
});
if (foundIndex === -1) {
missingElements.push(originalChild);
} else {
// Remove the found child from customElement to handle duplicates
const removedChild = customElementChildren.splice(foundIndex, 1)[0];
equalElements.push([originalChild, removedChild]);
}
});
// Find extra elements present in customElement but not in originalElement
customElementChildren.forEach(function (customChild) {
if (!originalElementChildren.some(function (originalChild) {return originalChild.nodeName === customChild.nodeName;})) {
extraElements.push(customChild);
}
});
return {
missingElements: missingElements,
equalElements: equalElements,
extraElements: extraElements
};
}
function differencesToString(differences) {
return differences.map(function (differenceObject) {
return differenceToString(differenceObject);
}).join('\n');
}
function differenceToString(difference) {
let result = '';
result += format('\nComparing elements <%s>.', difference.tagsCompared);
if (difference.missingElements.length) {
let missingElements = difference.missingElements.map(function (el) {
return format('<%s>', el.nodeName);
} );
result += format('\nMissing in custom: %s.\n', missingElements.join(', '));
}
if (difference.extraElements.length) {
let extraElements = difference.extraElements.map(function (el) {
return format('<%s>', el.nodeName);
});
result += format('\nExtra in custom: %s.\n', extraElements.join(', '));
}
if (difference.differencesInAttributes.missingAttributes.length) {
let badAttributesElements = difference.differencesInAttributes.missingAttributes.map(function (el) {
return format('%s', el.name);
});
result += format('\nMissing attributes in custom: %s.\n', badAttributesElements.join(', '));
}
if (difference.differencesInAttributes.extraAttributes.length) {
let badAttributesElements = difference.differencesInAttributes.extraAttributes.map(function (el) {
return format('%s', el.name);
});
result += format('\nExtra attributes in custom: %s.\n', badAttributesElements.join(', '));
}
if (difference.differencesInAttributes.wrongValuesAttributePairs.length) {
let badAttributesElements = difference.differencesInAttributes.wrongValuesAttributePairs.map(function (el) {
return format('%s: %s !== %s', el[0].name, el[0].value, el[1].value);
});
result += format('\nWrong values in custom attributes:\n%s.\n', badAttributesElements.join('\n'));
}
return result;
}
});